The service itself was a python flask service. It has similar functionality as provided by CyberChef, i.e., manipulate strings with various encodings. Such manipulation is referred to as a recipe. Recipes contain the different steps which should be applied to the raw string, which is called an ingredient.
Based on the received gameserver traffic, we learned that the flags were stored inside such recipes.
Flaws in the service
We managed to find two vulnerabilities in the service, one which we managed to exploit and another one which we could only verify on our system but not on the hosted infrastructure.
Flaw 1: Deserialization
Given this simple import statements, we were already intrigued and expected some form of deserialization vulnerability. Pickle is notoriously known to introduce an RCE when used with user-provided input.
Quickly searching for the usage of the load function hints us at the functionality for importing recipes and unveils a call of load to a file which we can directly upload to the Web application. The beginning of this routine merely checks the uploaded file name not to include slashes and ascertains a pattern of foo.recipe, which we will need to conform with when exploiting. Other than that, we find that load is called on the file which reads the content of given file and deserializes it. This means we can craft a serialized object that upon deserialization executes our code:
Now when pickle encounters an object which it does not know, i.e., a custom exploit class, it will try to get help from the developer to correctly deserialize the object. Using the __reduce__ function is one such indication, in which the return value of the reduce function will be used by pickle. More specifically, when there is a tuple returned, pickle will treat the first parameter as callable and the remaining (up to four) as arguments to the callable. Now this allows us to take os.system and pass it some command which we want to execute on a shell. Being able to execute arbitrary commands enables us to gather all the flags and exfiltrate them all at once. To exfiltrate the flags, we make use of the fact that the static folder is served directly, which allows us to create files containing all the flags and later on requesting those files separately.
Patching the vulnerability
Now patching the vulnerability would generally mean getting rid of pickle, e.g., making use of JSON, however, we wanted to do something fun and introduced signatures to validate that only exported recipes from us would be able to be imported again.
Adding our signature to all exported files and later on validating the signature allows us only to accept recipes originating from us, thus no maliciously crafted serialized objects with the reduce function.
Flaw 2: Eval in Python
Now the second exploit is one that we could confirm while running the server locally. However, our exploits did not work on the infrastructure used for hosting the machines, i.e., inside the docker.
Essentially, the functionality used to add encoding/decoding functions to recipes performs a lookup of the function in the global variables to get a reference to it.
In python there exists, besides the apparent developer created global objects, an object called __builtins__. This allows us to get a reference to pythons version of eval.
This function, once we assign it to a recipe, will pass the base string of the recipe to eval, thus, introducing yet another RCE into the service.