Minos 7: Takedown

Challenge Name:

Minos 7: Takedown

Category:

Malware

Challenge Description:

Tid til at få K-Pop Gremlin Hunters på banen, it's takedown time.

Gad vide om backenden gemmer på en selvdestruktionsmetode til nødsituationer, vi kan udnytte, der ikke er eksponeret i panelet. Hmm... håber ikke dine reversing skills er for rustne.

https://tryhackme.com/jr/minos 

This final challenge strongly hints at:

Approach

At this point, we already have:

So the question becomes:

Is there functionality in the backend that is not exposed via the panel UI?

The hint suggests yes, and that we need to reverse the backend itself.

Finding the backend binary

Using our existing RCE, I modified the custom restart script to enumerate executables related to minos:

echo "== Candidate executables named *minos* under /usr (depth 5) =="
find /usr -maxdepth 5 -type f -perm -111 -iname '*minos*' 2>/dev/null || echo "no *minos* executables under /usr"
echo

The output revealed two relevant binaries:

== Candidate executables named *minos* under /usr (depth 5) ==
/usr/local/minos/bin/minos
/usr/local/minos/bin/minos-server

I downloaded both and inspected them, turns out that the first is the panel website. The second is a C2 server that corresponds to the C2 client in tasks 1-4.

Inspecting the server binary

Using the arbitrary file download vulnerability, I extracted the server binary: Minos server executable

To avoid unnecessary efforts, I started with a simple strings pass instead of a full decompile: Minos server executable strings output

This already paid off.

Discovering the Takedown page

Inside the strings output, I found an entire HTML page embedded verbatim: takedown.html

If we managed to call the page, the Decoding box would change to our flag:

Panel destroyed page

The page itself does not contain the flag directly, but it does contain a JavaScript decoder:

        // Decode the flag using XOR cipher
        function decodeFlag(encodedHex, key) {
            const bytes = [];
            for (let i = 0; i < encodedHex.length; i += 2) {
                bytes.push(parseInt(encodedHex.substr(i, 2), 16) ^ ((i / 2) + 1) % 256);
            }
            return bytes.map(b => String.fromCharCode(b)).join('');
        }

What this does

So now we know:

Finding the hidden endpoint

Searching further through the strings output for API paths, I found a long concatenated list of routes that included this suspicious entry:

/api/v2/private/secret/op-endgame/takedown

That path is:

Calling the takedown endpoint

Step 1: Basic request

GET /api/v2/private/secret/op-endgame/takedown

Response:

405 Method Not Allowed
allow: POST

Step 2: POST without body

POST /api/v2/private/secret/op-endgame/takedown

Response

415 Unsupported Media Type
Expected request with `Content-Type: application/json`

Step 3: POST with empty JSON

POST /api/v2/private/secret/op-endgame/takedown
Content-Type: application/json

{}

Response:

{"success":false,"data":{"error":"Invalid confirmation","code":"INVALID_REQUEST"}}

So the endpoint exists, but expects a confirmation value.

Recovering the encoded flag

Searching again through the strings output for related error messages, I found this sequence:

messageBot deleted successfullybot_id.infoevent src/api/handlers.rs:1243minos_backend::api::handlerssrc/api/handlers.rsevent src/api/handlers.rs:1254event src/api/handlers.rs:1249event src/api/handlers.rs:937event src/api/handlers.rs:932confirmInvalid confirmationFailed to delete bots: Failed to delete tasks: 4f41307f7532693b65557f38663d6b20667c4c207b72486b2a686d2f6f416b144a47135216545a<!DOCTYPE html>

That last part is clearly hex-encoded data.

Decoding the Flag

Using the XOR logic from the embedded JavaScript, I recreated the decoder in Python:

def decode_flag(encoded_hex: str) -> str:
    out = []
    for i in range(0, len(encoded_hex), 2):
        b = int(encoded_hex[i:i+2], 16)
        k = ((i // 2) + 1) % 256
        out.append(chr(b ^ k))
    return "".join(out)

print(decode_flag("4f41307f7532693b65557f38663d6b20667c4c207b72486b2a686d2f6f416b144a47135216545a"))

Output:

NC3{p4n3l_t4k3d0wn_4nd_s3rv3r_t4ke0v3r}

Flag

NC3{p4n3l_t4k3d0wn_4nd_s3rv3r_t4ke0v3r}

The intended path (Post-solve insight)

After speaking with the challenge creator, it turns out the intended approach was to fully reverse the Rust backend binary.

If you do that, you find the expected JSON payload:

{"confirm":"DELETE_ALL_BOTS_AND_PANEL"}

The server responded with

{"success":true,"data":{"message":"Congratulations, you destroyed the panel, checkout the splashscreen"}}

Reloading the front page then displays the proper “Panel destroyed” splash screen:

Panel destroyed

Reflections and Learnings