Site cover image

Site icon image vicevirus’ Blog

Yo, welcome to my blog! I write tech stuff and play CTFs for fun. (still a noob)

Post title icon Hacktheon Sejong 2024 Finals Web Write-up

Introduction


Last week, on the 17th of June, we went to Sejong, South Korea, to compete in Hacktheon Sejong Finals with my teammates from M53 under the name Kopi Cincau (Mechfrog, Zeynarz, and Tzion—thanks for carrying me strong ppl <3) with Trailblazer as our manager (supa kewl manager).

The CTF was on the 18th of June, where we placed 12th in the advanced category. After the event ended, we discovered through the solves that we might have had a better chance of podium in the beginner category (no regrets!). Big big thanks to my teammates for carrying me and working together throughout the competition, you guys are awesome!

There were a bunch of forensics challenges, a little bit of reversing, pwn, web, and misc. Sadly, there was no crypto to satisfy our crypto god's thirst 😞 (he still strong). To be honest, I cannot say if all the challenges were good or not, but at least for web, imo it was average-guessy at best. Overall, South Korea was a good experience in terms of place and culture. Also, it was good to see and meet some of the GCC members again!

Also, not to forget, big thanks to our sponsors and supporters CyberWise Inc., Secure D Consulting Sdn. Bhd., SherpaSec. Without their support, we wouldn’t be able to attend the Hacktheon Sejong Finals.

Web


Re-Act-Toe

Like in the qual, this challenge revolves around reversing a compiled React code. The problem with compiled React is that, it’s very hard to see and understand what is happening in the code. It’s like looking into obfuscated code. But, class names in a compiled React code will always be visible, and you can see where the real logic really is.

For the challenge, we were given a website, which you can play tic-tac-toe.

Image in a image block

The most important logic of the code is ME() and VE(). ME() is where the flag is and VE is where the logic for the game is.

function ME({
    p: e,
    c: t
}) {
    const n = sE(aE),
        r = TE(s => s.y),
        o = p0(s => s.items),
        {
            x: l
        } = h0(),
        i = `${l}$d${o[5]}m be9${r}ns i${n.z} w${o[5]}nd3r`,
        u = `Th${r.y}s to0${o[8]} sh@l1 p${o[1]}s$`;
    return pe.jsxs(pe.Fragment, {
        children: [e === 786456 && pe.jsxs("h5", {
            children: [i, ":)"]
        }), t === 543845 && pe.jsxs("h5", {
            children: ["!", u]
        })]
    })
}
function VE() {
    const [e, t] = Ne.useState(Array(9).fill(null)), [n, r] = Ne.useState("X"), [o, l] = Ne.useState(""), [i, u] = Ne.useState(0), [s, a] = Ne.useState(0), [f, d] = Ne.useState("text-gray-300");
    Ne.useEffect(() => {
        if (y(e) && l(y(e)), n === "X") {
            const E = setTimeout(_, 1e3);
            return () => clearTimeout(E)
        }
    }, [n, e]);
    const v = E => {
            if (e[E] || y(e) || n !== "O") return;
            const p = e.slice();
            p[E] = "O", t(p), r("X")
        },
        _ = () => {
            if (y(e) || n !== "X") return;
            const E = e.map((h, R) => h === null ? R : null).filter(h => h !== null);
            if (E.length === 0) return;
            const p = E[Math.floor(Math.random() * E.length)],
                c = e.slice();
            c[p] = "X", t(c), r("O")
        },
        y = E => {
            const p = [
                [0, 1, 2],
                [3, 4, 5],
                [6, 7, 8],
                [0, 3, 6],
                [1, 4, 7],
                [2, 5, 8],
                [0, 4, 8],
                [2, 4, 6]
            ];
            for (let h of p) {
                const [R, T, N] = h;
                if (E[R] && E[R] === E[T] && E[R] === E[N]) return l(E[R]), d("text-black"), E[R]
            }
            return E.every(h => h !== null) && (l("Draw"), d("text-black")), null
        };
    Ne.useEffect(() => {
        o === "O" ? a(E => E + 1) : o === "X" ? u(E => E + 1) : o === "Draw" && (a(E => E), u(E => E))
    }, [o]);
    const w = () => {
        o && (l(""), t(Array(9).fill(null)), r("X"), d("text-gray-300"))
    };
    return pe.jsxs("div", {
        className: "flex flex-col gap-2 items-center w-72 bg-orange-200 rounded-3xl p-5",
        children: [pe.jsxs("h5", {
            children: [s, " : ", i]
        }), pe.jsx(ME, {
            p: s,
            c: i
        }), pe.jsxs("div", {
            children: [pe.jsxs("div", {
                className: "board-row",
                children: [" ", [0, 1, 2].map(E => pe.jsx(Ji, {
                    value: e[E],
                    onClick: () => v(E)
                }, E)), " "]
            }), pe.jsxs("div", {
                className: "board-row",
                children: [" ", [3, 4, 5].map(E => pe.jsx(Ji, {
                    value: e[E],
                    onClick: () => v(E)
                }, E)), " "]
            }), pe.jsxs("div", {
                className: "board-row",
                children: [" ", [6, 7, 8].map(E => pe.jsx(Ji, {
                    value: e[E],
                    onClick: () => v(E)
                }, E)), " "]
            })]
        }), pe.jsxs("h5", {
            children: ["winner : ", o]
        }), pe.jsx("button", {
            className: `bg-white px-4 py-2 rounded-lg ${f}`,
            onClick: w,
            children: "Play Again"
        })]
    })
}

Looking at the code, the flag is in ME() but it took me some time to actually notice that for finals there’s no flag format! To trigger and get the flag, we need to have the score for player “O” set to 786456. And for the second text to trigger, we need to have the score for player “X” set to “543845”.

return pe.jsxs(pe.Fragment, {
    children: [
        e === 786456 && 
        pe.jsxs("h5", {
            children: [i, ":)"]
        }), 
        t === 543845 && 
        pe.jsxs("h5", {
            children: ["!", u]
        })
    ]
})

And ME() is being called in VE() here.

children: [
    pe.jsxs("h5", {
        children: [s, " : ", i]
    }), 
    pe.jsx(ME, {
        p: s,
        c: i
    })
]

Seems impossible at first, but really it’s not.

Solution

My solution is to just copy the React compiled code to your machine, modify the function VE() so that when ME() is called with the value that will trigger the flag (p and c will be set).

function VE() {
    const [e, t] = Ne.useState(Array(9).fill(null)), [n, r] = Ne.useState("X"), [o, l] = Ne.useState(""), [i, u] = Ne.useState(0), [s, a] = Ne.useState(0), [f, d] = Ne.useState("text-gray-300");
    const [p, setP] = Ne.useState(0); // New state for p
    const [c, setC] = Ne.useState(0); // New state for c

    Ne.useEffect(() => {
        if (y(e) && l(y(e)), n === "X") {
            const E = setTimeout(_, 1e3);
            return () => clearTimeout(E)
        }
    }, [n, e]);

    const v = E => {
            if (e[E] || y(e) || n !== "O") return;
            const p = e.slice();
            p[E] = "O", t(p), r("X")
        },
        _ = () => {
            if (y(e) || n !== "X") return;
            const E = e.map((h, R) => h === null ? R : null).filter(h => h !== null);
            if (E.length === 0) return;
            const p = E[Math.floor(Math.random() * E.length)],
                c = e.slice();
            c[p] = "X", t(c), r("O")
        },
        y = E => {
            const p = [
                [0, 1, 2],
                [3, 4, 5],
                [6, 7, 8],
                [0, 3, 6],
                [1, 4, 7],
                [2, 5, 8],
                [0, 4, 8],
                [2, 4, 6]
            ];
            for (let h of p) {
                const [R, T, N] = h;
                if (E[R] && E[R] === E[T] && E[R] === E[N]) return l(E[R]), d("text-black"), E[R]
            }
            return E.every(h => h !== null) && (l("Draw"), d("text-black")), null
        };

    Ne.useEffect(() => {
        o === "O" ? a(E => E + 1) : o === "X" ? u(E => E + 1) : o === "Draw" && (a(E => E), u(E => E))
    }, [o]);

    const w = () => {
        o && (l(""), t(Array(9).fill(null)), r("X"), d("text-gray-300"))
    };

		// MODIFY HERE
    const triggerME = () => {
        setP(786456); // Set p to trigger i
        setC(543845); // Set c to trigger u
    };

    return pe.jsxs("div", {
        className: "flex flex-col gap-2 items-center w-72 bg-orange-200 rounded-3xl p-5",
        children: [pe.jsxs("h5", {
            children: [s, " : ", i]
        }), pe.jsx(ME, {
            p: p, // Pass p to ME
            c: c  // Pass c to ME
        }), pe.jsxs("div", {
            children: [pe.jsxs("div", {
                className: "board-row",
                children: [" ", [0, 1, 2].map(E => pe.jsx(Ji, {
                    value: e[E],
                    onClick: () => v(E)
                }, E)), " "]
            }), pe.jsxs("div", {
                className: "board-row",
                children: [" ", [3, 4, 5].map(E => pe.jsx(Ji, {
                    value: e[E],
                    onClick: () => v(E)
                }, E)), " "]
            }), pe.jsxs("div", {
                className: "board-row",
                children: [" ", [6, 7, 8].map(E => pe.jsx(Ji, {
                    value: e[E],
                    onClick: () => v(E)
                }, E)), " "]
            })]
        }), pe.jsxs("h5", {
            children: ["winner : ", o]
        }), pe.jsx("button", {
            className: `bg-white px-4 py-2 rounded-lg ${f}`,
            onClick: w,
            children: "Play Again"
        }), pe.jsx("button", { // Button to trigger ME
            className: `bg-white px-4 py-2 rounded-lg`,
            onClick: triggerME,
            children: "Trigger ME"
        })]
    })
}

Run it on your local browser, then click on TRIGGER ME button. Tbh, I think it would be faster to set p and c to the number directly but nvm.

Image in a image block

Flag: Wi$d0m be9ins in w0nd3r:)

ASM

This is a web assembly challenge which was solved by Mechfrog. Full credits to him. I am just copying what he wrote.

Running the decompilation on the binary, we see

Image in a image block

There is an encoded string, and an encoding algorithm. We think to get the flag, we just need to implement a decoding algorithm and decode the given string.

Let's analyse the encoding algorithm

The following is the index file

Image in a image block

From the index file, we know the two parameters ab should be set to 0 and len of original string respectively. Further more, memory is being set to the encoded input,

This line

var d:int = (a + b - c + 1)[0]:ubyte;

basically means taking the a + b - c + 1 index from the memory

Solution

Code out the decoding algorithm in python and get the flag

def decode(encoded_message):
    decoded_message = b''
    index = 0

    while index < len(encoded_message):
        d = encoded_message[index]
        original_byte = (d - 1) ^ index
        decoded_message += bytes([original_byte])
        index += 1

    decoded_message = decoded_message.rstrip(b'\x00')
    return decoded_message

encoded_message = b64decode("NUo3TFx9OGZmO0A/TlA+WVAlJE0iJX9E")
decoded_message = decode(encoded_message)
print(decoded_message[::-1])
Flag: Th15_15_W3BA553mb1y_H4H4

Hello_RestAPI

This challenge took most of my time to solve due to its guessiness (all my homies hate sourceless web challenges). Props to Mechfrog for helping me with this challenge in the last minute effort.

We were given nothing but a page which returns JSON data of hello, RestAPI!

Image in a image block

Yeah, just exactly that. Of course my first instinct is to fuzz it, but sadly my first wordlist didn’t catch anything.

I was trying everything, and by everything.. I really mean everything. There was no lead in sight.

Luckily, around the last few hours, fuzzed again and changed the wordlist as final effort of hope, and found an endpoint /download

Luckily, we do not need to bruteforce the params needed for /download endpoint because it will show us what is missing in the response. The endpoint requires file_name and POST request to make it work. From here, I thought it’s probably some kind of local file read.

Try to read /etc/passwd and it works!

Image in a image block

Nice, we got a lead finally.

Solution

This is where the real challenge comes actually. Apparently the server is blocking proc, environand shadow. Which then I tried to bypass the restrictions with multiple stuff, even fuzzed the whole Linux files to see if there’s anything interesting I could access. Again, I spent a lot of time here, going back and forth trying out forensics challenge (which I suck so bad) and no interesting files were found nor any bypass was found.

Interestingly, during the last hour, I found a working bypass for /proc/self and we can read what kind of command line arguments of the currently running process.

/dev/fd/../cmdline

# this is same as /proc/self/cmdline
Image in a image block

Reading manage.py , it seems to be referring to a REST API built with Django framework.

Web frameworks typically have default files and structured folders (because that's what they are built for). Maybe we can read some interesting files?

Then, I asked ChatGPT to show me a directory structure of an example Django project.

myproject/
    manage.py
    myproject/
        __init__.py
        settings.py
        urls.py
        wsgi.py
        asgi.py
    app1/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
        tests.py
        views.py
    templates/
    static/

Well, it seems like there are really some juicy files that we can read. But how do we know the project name or the app name?

Big props to mechfrog here, he did some educated guess and found there’s an api/ folder and this is where we start to go back and forth getting every information we can about the REST API with very little time left.

Here is the result of reading api/settings.py . We got the secret from here.

Image in a image block
Image in a image block

Checking api/urls.py , there seems to be api/api.py file available.

Image in a image block

Checking api/api.py , we found all the endpoints, including the juicy /g3tfl4g endpoint.

Image in a image block

This flag seems to be guarded by JWTAuth() which is part of the Ninja JWT plugin.

@api.get("/g3tfl4g", auth=JWTAuth())
def get_flag(request):
    return os.getenv("FLAG")

Next thing to do is to craft a valid JWT, but how is the Signature generated in this case?

Looking into the source code of Ninja JWT plugin, we found that it’s using the secret as the key.

SIGNING_KEY: str = Field(settings.SECRET_KEY)

Our solution is to just start our own local Django project with the same secret as the challenge server. Also add in /login endpoint to craft the JWT session for us.

# myapp/api.py

from ninja_jwt.controller import NinjaJWTDefaultController
from ninja_extra import NinjaExtraAPI
from ninja_jwt.authentication import JWTAuth
from django.http import StreamingHttpResponse, HttpResponse
import os
from ninja import Schema
from ninja_jwt.tokens import RefreshToken
from django.contrib.auth import authenticate

api = NinjaExtraAPI(
    version='1.0.0',
    docs_url=None,
)
api.register_controllers(NinjaJWTDefaultController)

@api.get("/g3tfl4g", auth=JWTAuth())
def get_flag(request):
    return os.getenv("FLAG")

class LoginSchema(Schema):
    username: str
    password: str

@api.post("/login")
def login(request, data: LoginSchema):
    user = authenticate(username=data.username, password=data.password)
    if user is not None:
        refresh = RefreshToken.for_user(user)
        return {
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }
    else:
        return HttpResponse("Invalid credentials", status=401)

Testing out the local endpoint first to get our JWT session crafted. Get and use the access token.

Image in a image block
Image in a image block
okay, seems to work fine on local

Testing JWT session cookie on the challenge server and we got the flag just in time before the CTF ends!

Image in a image block
Flag: JWT_with_S3CR3T_K3Y     

Side note: There’s an explanation by the organizer for this challenge after the CTF. We found out that you are not supposed to find that it’s Django through reading /dev/fd/../cmdline but instead guess through some certain OpenAPI docs/endpoint (IDK WHERE) and see that it’s running Django Ninja plugin.

Photo dumps!


Image in a image block
Image in a image block
Image in a image block
Image in a image block
Image in a image block
Image in a image block