Late on Sunday night, I attempted the Opabina series of challenges in Google’s CTF. These challenges relied on the Protocol Buffers stack. Long story short, a Protocol Buffer is a language-independent definition of a given data structure (which can be reasonably complex with some in-built logic), and Google provides a tool to create “wrapper code” around whichever language you’re using.
The challenge started reasonably simply: we were man-in-the-middling a connection, and the MitM tool we used communicated via a variant of protocol buffers (32-bit little endian integer size, protocolbuffer.SerializeToString()). The actual Protocol Buffer definition was as follows:
package main; message Exchange { enum VerbType { GET = 0; POST = 1; } message Header { required string key = 1; required string value = 2; } message Request { required VerbType ver = 1; // GET required string uri = 2; // /blah repeated Header headers = 3; // Accept-Encoding: blah optional bytes body = 4; } message Reply { required int32 status = 1; // 200 or 302 repeated Header headers = 2; optional bytes body = 3; } oneof type { Request request = 1; Reply reply = 2; } }
The first step was to compile the provided protocol buffer file, using “protoc”. I compiled to Python output, to assist in the rest of the challenge.
Opabina 1: Token Fetch
The first challenge was relatively simple: upon connecting to the challenge endpoint using an SSL-wrapped socket, you were presented with a Protocol Buffer, representing an HTTP request to /not-token. To win, you simply replied with the same request, except to /token:
There’s two gotcha’s to this:
- Firstly, the server sends you data in the format (little_endian_length,marshalled_data). If you forget about the length field, and de-serialize the entire data stream: it *may* work, and it *may* leave you with an object that looks correct, but re-serializing the data will leave you with a broken object.
- You couldn’t just send a serialized reply object: you had to send a serialized Exchange() object with a reply property/sub-object.
At this point (1am Monday morning), I thought the CTF was finished so I played a bit of Dark Souls 3. A few Yhorm the Giant kills later, and I realized the CTF finished at 3am, so I got back to work. Onwards!
Opabina 2: Downgrade
The downgrade attack was a lot more complex – fetching /protected/secret provoked a WWW-Authorize response, asking for Digest authentication. Unfortunately, we had no idea of the username and password.
Initially, I tried to guess the password, based on the realm (“In the realm of the hackers”) – I tried “electron:phoenix” and a few other variations, but this proved fruitless.
My second approach was to downgrade the user’s authentication to Basic, allowing us to retrieve the user’s password, as follows:
Client: "I'd like /protected/secret" Server: "Authenticate, Digest please" --- MITM ---> "Authenticate, WWW-Basic please" Client: "I'd like /protected/secret,Authentication: Basic _base64_" ---- MITM ----> "Authenticate, Digest XYZ"
A quick test later, and we are rewarded with a set of credentials:
The next part of this is setting up the Digest authentication, but fortunately, we can get some help from Github:
Opabina 3: Redirect
Opabina 3 was a real piece of work – with no real clue aside from the title, and the directive to retrieve “/protected/secret”, I set to work:
Simply modifying the initial request to “/protected/secret” doesn’t work: the client is challenged for Digest authentication, and happily provides it, for “/protected/not-secret”, resulting in an HTTP 401 Unauthorized status.
At this point, I tried a number of dead-ends from web application security – I tried a localhost referrer, bad Host headers and even things like /robots.txt. Eventually, I realized the correct approach was to trick the client in to legitimately requesting “/protected/secret”,
The key to this was to simply do nothing and let the user retrieve “/protected/not-secret”, and at that point, redirect the user to “/protected/secret” via an HTTP 302 message (i.e. MitMing the server response to replace the page with a redirect):
Of the Opabina challenges I solved, I found this one the most enjoyable, as this isn’t a type of challenge I had seen too often.
Source code is here: it’s easy enough to reconstruct the other attacks using the nice_send and nice_recv functions implemented. Note that the source code contains some test content from Opabina 2 / Downgrade: I got lazy and cut it off with sys.exit(0).
Great job Google at putting this challenge up and making me learn Protocol Buffers!