Writeup by r2uwu2 from PBR | UCLA (as part of SuperDiceCode)
A fresh web scale Key Value Store just for you 🥰
Solves: 8
Points: 588
We are given a go http server. Upon requesting to the server, a "frontend" assigns us a session id. It then starts a unique Key-Value (KV) store service corresponding to the session id and proxies our traffic to the service.
The KV store has two useful routes /get
and /set
. Performing a GET /get?name=owo
will get the value corresponding to the key of owo
. To set owo
to uwu
, we can perform a GET /set?name=owo&value=uwu
.
The relevant code for /get
is below.
For /set
, code is the same except it writes to a file with permissions 0o777
.
checkPath(name)
checks whether name
contains .
or flag
.
In go, files returned by os.Open
must be manually closed. This is usually done by opening files with following pattern:
However, as we see, the file is never closed. This could lead to potential exploits through accessing previously opened files through reading /proc/self/<fd>
.
On line 47, there is some sneaky going on in if err = checkPath(name); err != nil
. In go, varname := value
declares a value and varname = value
re-assigns to the value. Since the closest previous declaration of err
is on line 36 (outside the handler function), err
is shared by all invocations to /get
on the same session.
If we can cause below order of execution, we can set err = nil
and bypass the check.
I first tried to trigger the data race by sending requests as fast as possible using python requests
library. I created two persistent connections: one to set err = nil
between err = checkPath("aflag")
and err != nil
and another connection to repeatedly send a forbidden path aflag
.
If I bypass checkPath("aflag")
, I should receive an error message Open :(
. However, network jitter is too high and the two requests were not arriving close enough together to cause a race condition.
When trying to cause race conditions in remote servers, sending http requests really fast in parallel is often thwarted by network jitter. To get around these limitations, we can be smarter about how we send our requests by looking to the lower layer networking protocols that http requests use.
If our server uses HTTP/2, we can perform a single-packet attack. HTTP/2's core feature is that it allows us to send multiple http streams over the same tcp connection. It works by sending part of multiple streams in each tcp packet.
Luckily (at least for hackers), this opens up attacks capable of delivering many http requests at nearly the same time. If we send all but the last byte for each http request we want to make, the server will be in a state storing all of our requests. We can then send a single packet containing the last byte for each request. This will cause all requests to be delivered at the same time.
a quick diagram i drew in xournal++ showing the single-packet attack
This attack is the most well documented, so I checked whether it was possible to perform.
Unfortunately, in the devtools, it seems like the server only supports up to HTTP/1.1. Since we cannot use HTTP/2, we cannot perform this attack.
With HTTP/1.1, our hopes are not completely lost. We can do a similar but much less effective version of the single packet attack called the last-byte sync attack.
We can start many tcp connections to the server and send all but the last byte of each request corresponding to a connection. Then, we can send the last byte in parallel across each connection.
Since each request's last byte is in a separate tcp packet, network jitter may act to deliver the requests at different times. However, since we are only sending a 1-byte packet instead of a ~100-byte packet, the network jitter is significantly reduced.
Stages of a last byte sync attack. Requests are more likely to be delivered at same time.
I couldn't find any implementations of this rare attack, so I had to rawdog tcp connections and write the solve script from scratch. After many attempts of coding and staring at wireshark, I was not able to trigger a race condition.
Then, all of a sudden, I got the below response from requesting /get?name=oflag
We had bypassed the checkPath()
constraint and now have arbitrary read!
I put in arc's file path to get below solve script:
After around 10 seconds of racing, I got the response:
After quickly submitting the flag, I found out that I blooded the challenge hehe.