T5: An Avalanche of Web Apps
You got caught in the avalanche. I compromised most of your web applications without being detected. You can't compete with a leopard's speed.
Last updated
You got caught in the avalanche. I compromised most of your web applications without being detected. You can't compete with a leopard's speed.
Last updated
The following post by 0xb0b is licensed under CC BY 4.0
We find the keycard for the fifth Side Quest hidden in the task of the 25th day of the TryHackMe Advent Of Cyber. In the following task, it gives us a hint to listen to the second peguins advices: The second penguin gave pretty solid advice. Maybe you should listen to him more.
Day 19: I merely noticed that you’re improperly stored, my dear secret!
The task itself is about game hacking and introduces us to frida-trace. However, when starting the application in the context of frida-trace, the function call _Z14create_keycardPkc()
is immediately noticeable.
There is also a corresponding js file created by frida in __handlers__
after running the application with firda-trace, which was not there before.
We follow the hint and try to buy as much advice as possible from the second penguin. To do this, and to avoid having to update our coin count using the in-game PC, we use frida-trace to give the function a negative argument for the price. This allows us to buy as many advices as we want.
After several hints we get the following. A sequence that reminds us of the last appearance of Cyber Side Quest 2023.
We enter the sequence with the error keys and get the message incorrect password.
Lets apply what we learned from the room and set the return value to true to bypass the password check. And after re-entering the sequence, we get a secret phrase, but no keycard.
We could have found this sequence using strings on the binary.
We still need the keycard. Let us copy the TryUnlockMe
binary and the corresponding libaocgame.so
library to our attacker's machine. We can use netcat to do this:
We set up a listener on our machine that writes incoming data to the TryUnlockMe file:
Next, we write the data of the TryUnlockMe binary into the connection we are listening to on our target machine.
We do it vice versa for the library. Set up a listener on the attacker machine to receive the data.
And send the data of the file using netcat.
We now have both files on our system.
We decompile both binaries with Binaryninja. We find nothing useful on the TryUnlockMe binary.
But in the libaocgame.so
library we find a function to create the keycard.
By examining the function, we can see that it writes to a file.
Now that we have the library, we can use it in a C program to call the create_keycard()
function to write the file to our system.
This C program was developed by Aquinas. All rights and credit are attributed to him:
After compiling and running the program, we have the zip file, but it is password protected.
Fortunately, we retrieved a passphrase before from the game, which is the password for the zip file. After extracting it, we get the fifth keycard.
We can deactivate the firewall with the password of the keycard. We can pass the value to a website on port 21337
.
We start with an Nmap scan and find four other open ports in addition to port 21337
. Port 22
SSH, port 53
dns and on port 80
and 3000
we have a web server. An Apache 2.4.58
web server on port 80 and a nodejs web server on port 3000
.
From the detailed Nmap version scan we can find a redirect to http://thehub.bestfestivalcompany.thm. We add this to our /etc/hosts
for now.
A scan for other VHOSTs will not return anything useful.
But there is a DNS server running on the machine. We use dig to query it for the A record of thehub.bestfestivalcompany.thm
and find a different IP, 172.16.1.3
.
Let us do some reverse lookups on the address range of 172.16.1.0
. We find another entry for 172.16.1.2
, its npm-registry.bestfestivalcompany.thm
.
We are trying a DNS zone transfer for the domain bestfestivalcompany.thm
on the DNS server. A DNS zone transfer allows DNS records to be replicated from a primary DNS server to a secondary DNS server. This allows us to discover multiple subdomains for bestfestivalcompany.thm
.
We add these to our /etc/host
file and re-enumerate.
At thehub-uat.bestfestivalcompany.thm
on port 3000
we now find a website presenting a team. Scrolling down you can also find a contact form. But more about that later.
There is a Verdaccio instance running at npm-registry.bestfestivalcompany.thm
. Verdaccio is a lightweight, open source, Node.js-based private npm registry that allows to host and manage own package repositories. Looks like this could be used later for something like a dependency confusion attack. But let's move on to the contact form we found.
We go back to thehub-uat.bestfestivalcomapny.thm:3000
and test the contact form for some blind XSS.
To do this, we use a payload that connects back to our web server, and for each field and for each field we chose a different directory so that we can tell which field is vulnerable when someone checks the message we send.
We set up a Python web server and get a connection back for the message field.
Nice, we are now able to inject some XSS. To test different payloads, we will make a new contact request with the following payload, which will be loaded from our web server, so we do not need to make multiple requests and just change the content of the script.
With the following payload, we try to request the page the user is seeing on reviewing the contact we made, since we are not able to steal a cookie to get an authenticated session.
WA request is sent to our web server containing the content of the current page.
And we see that there are two more pages, /wiki
and /contact-response
s available.
We update the script.js
to fetch the wiki page.
And we see another link to /wiki/new
to create a new wiki entry.
We will now try to get the contents of /wiki/new
...
... and see a form for submitting a request to create a new wiki entry. It is interesting to note that the content we can provide can be markdown.
With the following payload, we force the reviewer of our message to create a new wiki entry. While playing around, we test for other vulnerabilities. In our control we have the title and the markdownContent field values.
During testing, we also test some SSTI payloads such as the following
With the post request we get a response back. A link to /wiki/1
was created.
We examine the wiki entry created...
And see that our payload containing {{7*
7}}
fully errors, and on ${7*
7}
we see the *
got substituted to </i>. The reason for {{7*7}}
throwing an error might be that indeed SSTI is present, but the substitution takes place first, so {{7</i>7}}
errors.
Lets test it with {{7+7}}
, to see if +
gets also substituted, or {{7+7}}
evaluated.
We get a response after creating a new wiki entry.
We fetch the new wiki entry.
And we see, that {{7+7}}
gets evaluated to 14
. SSTI seems to be present.
The next thing to try is a simple SSTI payload that downloads and executes a reverse shell.
After adding the wiki entry we get a response back...
... and a connection back to our previously set up listner. We upgrade the shell.
We are on 172.16.1.3
a Docker container. The flag can be found in the container's root directory.
The Verdaccio instance on npm-registry.bestfestivalcompany.thm does not only have some official packages. Scrolling through the list of available packages, there is one from McSkidy, a markdown converter. We can inspect the package and download the source.
Reviewing the source code, we could easily see the substitution taking place, converting the markdown to HTML and the potential risk of SSTI.
With a shell on 172.16.1.3
, we start by enumerating the container to hopefully jump to another container or escape to the host.
In the /etc/host
we can see that the current hostname matches up with the entry for 172.16.1.3...
Since we are in a docker container with the IP 172.16.1.3
we try to test if the other docker containers like 172.16.1.2
are reachable and if so, what services are running on them. For 172.16.1.2
, we can detect four open ports with a static nmap binary.
We search for SSH keys, but do not find any. However, there is an entry in the authorized_keys
file for the user git
from fcdevhub
.
We examine the various projects in /app/
and find a .git folder in the /app/bfc_thehubuat
folder.
The config itself does not reveal anything useful for now. But lets try to dump the git and inspect it further.
For the subsequent phases, we use ligolo to relay traffic between the docker container and our attacker machine to make the internal and external services of the docker container accessible from the container to our attacker machine.
Ligolo-ng is a simple, lightweight and fast tool that allows pentesters to establish tunnels from a reverse TCP/TLS connection using a tun interface (without the need of SOCKS).
First, we set up a TUN (network tunnel) interface called ligolo and configuring routes to forward traffic for specific IP ranges (240.0.0.1
, 172.16.1.0/24
) through the tunnel.
Next, we download the latest release of ligolo-ng. The proxy and the agent are in the amd64 version.
On our attack machine, we start the proxy server.
Next, we run the agent on the target machine to connect to our proxy.
We get a message on our ligolo-ng proxy that an agent has joined. We use session
to select the session and then start it.
We are now able to reach the machines on networks 172.16.1.0/24
and the machines services itself. We now start a python web server in the assets folder to make the .git folder available.
With the web server on the Docker container, which is now available via Ligolo-ng, we have the .git
folder available and can dump the git repository using the gitdumper tool.
We look at the history using git log and find some commits.
We examine each commit with git show. On the second commit we see a deleted SSH key.
The first commit added this key and the public key.
With the following restore commands we are able to restore the keys. From the public key we are able to retrieve the possible user.
We try to use this private key to log in as git via ssh to all possible docker containers available, but are successful with the host. The host is running gitolite3. Gitolite is an open source git repository hosting system and we see five readable repositories. Unfortunately git is not available on the docker container, so we have to continue on our host.
We now get the repostories via git for further analysis.
The service listens port 3000
and uses JWKS for authentication. Something we noticed on 172.16.1.2
.
The JSON Web Key Set (JWKS) mechanism o that service periodically retrieves public keys from a remote server /jwks.json and validates the structure of the keys to ensure that they can be used to verify JWTs. When a JWT is received, it retrieves the corresponding public key from JWKS to verify the token's signature and authenticate the user. The token must contain the username mcskidy-adm and be signed with the values provided by jwks.json.
The peculiarity here seems to be that the JWKS is obtained from http://thehub-uat.bestfestivalcompany.thm:3000/jwks.json
. We have already compromised this and found the jwks.json in the /assets
folder.
Now that we have control of the JWKS, we can create our own token to authenticate to the service and use the features it provides.
After authentication we have the following three options:
The restart-service funciton defines an Express route that allows restarting a service on a remote host via SSH. It expects host
(the target machine) and service
(the service name to restart) in the request body. The function authenticates the request using a token, creates a RemoteManager
instance with SSH configuration, and calls its restartService
method, returning success or error details as JSON.
The modify-resolv function defines an Express route to modify the resolv.conf
file on a remote host via SSH. It requires host
(the target machine) and nameserver
(the new nameserver entry) in the request body. The route authenticates the request, uses a RemoteManager
instance to update the file, and responds with success or error details as JSON. We can use it to change the DNS server to be used for the target.
The reinstall-node-modules function defines an Express route to reinstall node_modules
for a specific service on a remote host via SSH. It requires host
(the target machine) and service
(the target service) in the request body. The route authenticates the request, uses a RemoteManager
instance to reinstall the dependencies, and responds with success or error details as JSON.
Since we can replace the jwks.json
file in /app/bfc_thehubuat/assets
, we have control over the services. This allows us to change the DNS server for a target machine and reinstall its node js modules.
The idea now is to setup our own Verdaccio instance, to provide a malicious package, that when installed pops a reverse shell upon a new installation of modules.
We change the nameserver to be one in our control that resolves npm-registry.bestfestivalcompany.thm
to the our Verdaccio instance.
First, we get access to the service, by providing an own jwks.json. We recall the results from before.
We retrieve the jwks.json
currently in use from thehub-uat.bestfestivalcompany.thm
.
We are also preparing a request to the service on 172.16.1.2:3000
. This is still available via our ligolo-ng session. We make a simple get request and forward it to the Burp Suite repeater module.
There we change the request method to a POST request and make a first attempt. Without a valid Authorization Header we get the error Unauthorized.
Next, we create a Python script to generate a jwks file with a token containing the username mcskidy-adm
.
We run the script and use the output to create a new jwks.json file, and add the authorization header to our request.
The jwks.json
looks like the following.
We simply remove the old one and fetch the new one from our web server.
After a minute, the JWKS parameters are refreshed and we can make an authorized request.
Now we need to run and configure a DNS server. We will use dnsmasq.
We still let thehub-uat.bestfestivalcompany.thm
resolve to 172.16.1.3
, but npm-registry.bestfestivalcompany.thm
resolve to our attacker's 10.14.90.235
.
Next, we restart the dnsmasq service,...
And test that it resolves correctly with the following command.
Now we setup Verdaccio to provide a malicious package. We install Verdaccio.
And run the an instance running on 0.0.0.0:4873
like the on on 172.16.1.2
. (Detected via Nmap before)
To publish new packages we need a user, which we can add with the following command. Some newer instances of npm require --auth-type=legacy
.
We have almost everything prepared. We change the name server from 172.16.1.2
to ours with the following request to /modify-resolv
.
To test if this worked properly, we try to reinstall the modules. If we get a connection back to our Verdaccio instance, it worked.
We see some requests made to our verdaccio instance. We also see which modules will be reinstalled. So we have chosen one of them as our malicious package. But we also see that if we do not make the packages available locally, they will be fetched from https://registry.npmjs.org/
.
To circumvent the fetch from https://registry.npmjs.org/
we edit our Verdaccio config.yaml
file. There we have to comment out the following lines:
Then we prepare a package that will install a script. The package has the same name as one of the previously installed packages. The script contains our reverse shell.
We publish the package.
And can inspect it on our Verdaccio instance.
We set up a listener on port 4445
and request for another reinstallation of the modules.
The Verdaccio will receive a connection back, but will stop after our package.
We get a connection back to our listener and we are root
on 172.16.1.2
.
The flag can be found in the root directory of the container.
In the /app/admint
directory of the machine, we find another pair of keys.
We can use this key to connect to the host again, revealing some more repositories, which we now have write access to admdev
. Forthermore hooks_wip
looks like a promising repository.
We clone the hooks_wip
repository.
In this we find a post-receive
script, a git hook that logs commit messages or branch deletions to a specific file. The post-receive
script is vulnerable to command injection because it interpolates commit_message
directly into a bash -c
command.
Let's check if this hook exists in the writeable repository webdav. We write something and add it. Next, we make a commit message containing a curl request to our web server in a command substitution and push the changes.
After the push we get a connection back to our web server.
Now we change something again and this time we download and execute a reverse shell.
Once we have pushed our changes we get a connection back to our web server.
... and to our listener. We are now user git
on the host.
The third flag can be found in the home directory of the user git
.
We are allowed to run /usr/bin/git --no-pager diff --help as root without a password using sudo. The command /usr/bin/git --no-pager diff
performs a Git diff operation, but it disables the default use of a pager, such as less
, for viewing the output. Theoretically, we could now read arbitrary files with /usr/bin/git --no-pager diff /dev/null /path/to/file/to/read
.
If we could be in a pager like less, we could escape via !/bin/sh
in the context of root. But the tag --no-pager
is used, which circumvents it. Strangely, the tag --help
without providing further parameters for diff does bring us to a pager we can escape. After running !/bin/sh
...
We are root
and can read the final flag in the home directory of the root
user.
Like mentioned before, we could also read now arbitrary files, since it is executed with root permission.
For example, we could read the /etc/shadow
file.
But we cannot read the id_rsa
in /root/.ssh/
because it is not there. Fortunately, the authorized_keys
file is present, and we find some other key types in use.
We try to read /root/.ssh/id_ecdsa
and do find a private key.
We copy the key, change the permissions and use it to connect to the host as root
.