The service itself was a ASP.NET Core service. It allows, once registered, to join teams and create operations (i.e., projects) and attach objectives (i.e., tasks) to it. Both elements can be enriched by providing additional information via an online markdown editor or uploaded files. The markdown editor is backed by CodiMD, a collaborative online Markdown editor.
Based on the received gameserver traffic, we learned that the flags were stored in the Markdown documents describing the operations. Once an operation was created by the gameserver, a
ModifyOperationMessage WebSocket request was sent to the backend. Its exact definition can be found in
Let’s have a look at the server-side code handling this message. The code in
Websocket/WebSocketClient.cs contains boilerplate code managing the connections. Incoming messages are dispatched by
Channel.HandleWSMessage() which deserializes the payload to JSON and calls
ScavengePadDbUtils.ModifyOperation is the first interesting method in this chain. It takes care of creating and updating the operation database records. Most importantly, it generates a unique URL path which can be used to access an operation’s markdown document directly. The path is generated as follows:
As we can see above, the path is a SHA256 hash over the operation’s record primary key, concatenated with random integers generated by
ScavengePadUtils.GetRandomInt(). Information about the created operation (including the
OperationPadSuffix) and the created objectives are returned to the client. The gameserver then visits the generated path and inserts the flag.
Flaws in the service
The developers claimed to have placed four different vulnerabilities in this service. Unfortunately, we only managed to find and exploit one during the CTF.
Find the right path
Since the flags are stored in the markdown documents describing the operations, we need a way to learn their unique paths in order to access them directly. As mentioned above, the path is a SHA256 hash over the primary database key and
ScavengePadUtils.GetRandomInt(). Both implementations, the SHA256 and the random generator looks legit and use standard-library primitives:
How can we now compute the hash?
This looks random
Let’s see where else this random generator is used. Besides for the path generation, the generator is used in
This is a REST endpoint which allows us to query
/api/test with a
tasks GET parameter. Once called, the server will spawn
Tasks, all of them querying the random generator for a new integer concurrently. Finally, the server will return a last random integer. We can clearly see that we have an unsynchronized access to
GetRandomInt(). Googling for Random and thread safety leads us to this section in the official docs, which states that “Random objects are not thread safe” and if “you don’t ensure that the Random object is accessed in a thread-safe way, calls to methods that return random numbers return 0”. Nice. I would be really interested in the technical details of this, but time is ticking and we now need to steal some flags.
In the first round of our attack, we query
/api/test with enough
tasks to zero the random generator:
If this returns 0, it means we successfully deactivated the random generator. Further, the SHA256 hash representing the document path now only depends on the primary database key which we can easily brute-force. Then we enumerate all possible paths and check if we find a flag. That’s it! The rest is just boilerplate code to communicate with the server which we learned by observing the gameserver traffic:
Patching the vulnerability
Patching the vulnerability is simple. Use the synchronization primitive of your choice and then clamp the
tasks variable in
TestController to a small number. This is because all spawned tasks now need to wait to acquire the lock and allowing a large (or unlimited number) for
tasks would therefore introduce a DoS vector.
Overall, this service has been claimed to have four different bugs. Unfortunately, we only manged to exploit one of them. The vulnerability revolved around the quite unexpected fact that
System.Random is not thread-safe and will return 0 once it is accessed in an unsynchronized way. Stealing the flags is hence reduced to brute-force the primary key of an operation. Overall this was a very cool and challenging service. I’m looking forward to see more of this kind in the future!