Dojo challenge #38 - Xmas wishlist, winners and writeup

January 23, 2025

The Dojo challenge, Xmas wishlist, asked participants to perform a format string injection, read the system environment variables and reveal the flag.

💡 Want to create your own monthly Dojo challenge? Send us a message on Twitter!

The winners

Congrats to YoyoDavelion, kharaone and 3c4d for the best write-ups 🥳

The swag is on its way! 🎁

Subscribe to our X and/or LinkedIn feeds to be notified of upcoming challenges.

Read on to find out how one of the winners managed to solve the challenge.

TALKIE PWNII #3: VIDEO CHALLENGE WRITE-UP

OVERALL BEST WRITE-UP

We want to thank everyone who participated and reported to the Dojo challenge. Below is the best write-up overall:

————– START OF YoyoDavelion‘s REPORT —————

DESCRIPTION

The use of Externally-Controlled Format String vulnerability occurs when an attacker can control the format string used in functions like printf(), str.format(), or similar mechanisms. This can lead to unintended behavior, such as information disclosure, code execution, or crashes, depending on how the format string is handled.

EXPLOITATION

When attempting to identify and exploit security flaws in an application, one of the most crucial steps is conducting a thorough analysis of the source code. A solid understanding of how the application works provides insight into potential attack vectors that might be vulnerable.

By carefully examining the code, you can identify areas where input from untrusted sources (such as user input or external systems) is processed. These inputs often serve as entry points for malicious actors to manipulate the application’s behavior.

CODE ANALYSIS

The first lines of the code of the application (check below) give some information about the libraries that are used. It also shows that Jinja2 is used. Jinja2 is a template engine for the Python language used to render templates. In this case, it is used to render a template with the main HTML code of the application, but this part of the code is not really important in this case.

import os, re, tomllib, datetime
from urllib.parse import unquote
from dataclasses import dataclass
from jinja2 import Environment, FileSystemLoader
template = Environment(
    autoescape=True,
    loader=FileSystemLoader('/tmp/templates'),
).get_template('index.tpl')
os.chdir("/tmp")

The next we can find in the code is the definition of the class Factory, which contains a property and some methods that handles the main application functionality.

class Factory:
    def __init__(self):
        self.gifts = {
            "hoodie": 1337,
            "socks": 0,
            "candy": 0,
        }

    def getGift(self, item) -> str:
        return str(self.gifts[item])

    def buyGift(self, givenWishlist) -> str|ValueError:
        if givenWishlist == "":
            return ""
        try:
            wishlist = tomllib.loads(givenWishlist).get('wishlist')

            if "price" in wishlist.keys() and int(wishlist["price"]) != 0:
                raise self.makeError("The given price: {price}$ is to much, we only offer free gifts at the moment!", price=wishlist["price"])

            if "item" in wishlist.keys() and wishlist["item"] in self.gifts.keys():
                return "Your have selected: {item}. The gift is on its way!".format(item=wishlist["item"])

        except Exception as e:
            return self.makeError(str(e))

        return self.makeError("The gift was lost among all the Christmas presents!")

    def makeError(self, message, **kwargs) -> ValueError:
        return ValueError(message.format(self, **kwargs))

Going step by step in the definitions of the Factory class we can find the following things:

A property called gifts, which is a python dictionary that contains information about products, specifically the name and the price. Data in python dictionaries are stored in key and value elements, being hoodie,socks and candy the keys and 1337,0 and 0 the values. This property is inside the __init__ method, which is a constructor that will be automatically called when an object of the class is created.

def __init__(self):
        self.gifts = {
            "hoodie": 1337,
            "socks": 0,
            "candy": 0,
        }

The next we find is a method called getGift. This method doesn't have much functionality, it just returns a value of a given product name but is not even used in the code of the application.

def getGift(self, item) -> str:
        return str(self.gifts[item])

The next method defined is buyGift. This method handle the main functionality of the application.

The method expects a given argument called: givenWishlist.

First the application will check if this argument is empty, and if it is, it will return an empty string, otherwise, it will load the argument with the tomllib library. This library is used for parsing TOML (Tom's Obvious, Minimal Language) configuration files. It provides a way to read and interpret TOML files directly into Python objects such as dictionaries. TOML is a configuration file format that is easy to read and write for humans. It is commonly used for configuration purposes in software projects.

After trying to load TOML data in the givenWishlist argument, it is also trying to access the key called wishlist inside the given data and storing the content inside the wishlist variable.

With the given data stored in the variable wishlist, the application will check if there is a key called price inside the data and will verify if the value associated with this key is different from 0. If this value is different from 0, the application will raise an error through the method makeError that we will analyze later in this post.

On the other hand, the application also validates if a item key exists on the given data and if its value is valid. This is done by checking if the value is inside the defined values at the gifts property previously analyzed. If both conditions are met, the application will return a string saying Your have selected: {item}. The gift is on its way! replacing item with the value of the item key.

This two validations of the keys: price and item are done in a try-except flow. This means that if there is an error at any point in these validations, the program flow will go through the except definition. This definition will save the program error in a variable called: e. With the error stored in the e variable, the application will send this error to the makeError method and return the result of this call.

Finally, if none of the earlier conditions or exception handling blocks are met, the program flow will go through the last return in the below code. For example, one practical case could be: a valid wishlist is given, but expected keys are missing. This last flow will return the output of the makeError function, giving the message: The gift was lost among all the Christmas presents! as argument.

def buyGift(self, givenWishlist) -> str|ValueError:
        if givenWishlist == "":
            return ""
        try:
            wishlist = tomllib.loads(givenWishlist).get('wishlist')

            if "price" in wishlist.keys() and int(wishlist["price"]) != 0:
                raise self.makeError("The given price: {price}$ is to much, we only offer free gifts at the moment!", price=wishlist["price"])

            if "item" in wishlist.keys() and wishlist["item"] in self.gifts.keys():
                return "Your have selected: {item}. The gift is on its way!".format(item=wishlist["item"])

        except Exception as e:
            return self.makeError(str(e))

        return self.makeError("The gift was lost among all the Christmas presents!")

The last method defined in the Factory class is the makeError previously mentioned. This method is used to create and return a ValueError with a message that is formatted using .format(), however, this creates a risk, as in Python is possible to access object properties through placeholders. A placeholder in programming refers to a part of a string (or other data structure) that is temporarily used to represent a value that will be provided later. These placeholders are represented in Python as curly braces {}, so, for example: a="test"; mystring = "Hello {}".format(a) in this example, the variable mystring will have the value: Hello test as the curly braces will be replaced with the a variable.

As in the below code, at the format() part, self is included, it will refer to the current object instance, so if an attacker is able to inject curly braces with properties inside , it will be possible to format the message with internal data of the object. For example, {.gifts} will be formatted with the value of the gifts property. This creates an attack vector that will be exploited later.

def makeError(self, message, **kwargs) -> ValueError:
        return ValueError(message.format(self, **kwargs))

After the definition of the Factory class, few lines of code are left. This remaining code is used to load the user input in the application logic.

First, an object of the class Factory is created and saved at the factory variable. Then the buyGift method is called, giving as an argument the URL-decoded input of the user through the input POST parameter, and the returned value is saved at the message variable. After that, the type of message is compared to render the message returned in different ways; this is used to show the message with green colors when the request is successful and with red colors when some issue happens.

factory = Factory()
message = factory.buyGift(unquote(""))

if type(message) == str:
    print( template.render(message=message) )
else:
    print( template.render(error=message) )

CODE ANALYSIS CONCLUSION

Having understood the application logic, we know that in order to use the application we need to send valid TOML data. As per the TOML standards and the application validations, a valid payload could be something like wishlist={"item"="hoodie","price"=0}.This payload gives a successful response.

Now that we know how to send valid data to the application, we know there is a vulnerable function, the makeError one, so we need to find a way to give that function a malicious payload to exploit it.

A good entry point is the except flow we analyzed previously, as it is sending the error to the makeError method. If we find a way to control what error is coming to this except flow we will be able to inject our payload there.

except Exception as e:
            return self.makeError(str(e))

As analyzed before, to reach this except flow, we will need some of the below lines of code to fail.

A good way to make the application crash will be with the int() function, this function is used to convert a value into an integer number, so if we give it a random string with characters different that numbers it will crash, as it is supposed to work with numeric values. The best part of this crash is that python will include on the error our value given to the int() function. With the following testing payload we can verify that: wishlist={"item"="hoodie","price"="test"}

As shown in the above image, we find a way to control the error sent to makeError method. Now we could access object properties and more by sending curly braces that will be formatted.

With the following payload it was possible to retrieve the gifts property through the web: wishlist={"item"="hoodie","price"="{.gifts}"}

PROOF OF CONCEPT

With this injection it is possible to access internal data of the python code. As os library was imported, it was possible to access to the environ property of this library by accessing __init__ method and__globals__ property. The following payload was used: wishlist={"item"="hoodie","price"="{.__init__.__globals__[os].environ}"}

Internal environment variables were exposed and the FLAG was found: FLAG{Xma5_g1fts_f0r_3v3ry0ne!!!}

RISK

An attacker could exploit this vulnerability to disclose internal information like environment variables, object properties, code variables and more, creating a high impact on the confidentiality.

REMEDIATON

To fix this vulnerability several measures can be taken:

  • Implement a regex validation to price parameter to allow only numbers, so it will not crash.
  • Avoid direct user input in format strings
  • Sanitize user input to avoid curly braces be interpreted at the makeError request.

————– END OF YoyoDavelion‘s REPORT —————