The HTB x Uni CTF 2020 - Qualifiers have just finished and I wanted to write-up some of the more interesting challenges that we completed.

As with many of the challenges the full source code was available including the files necessary to build and run a local docker instance of the service.


The application is a simple flask web app that takes screenshots of websites and returns the cached image to the user. It has protections to prevent requests of resources from localhost that can be circumvented using a DNS rebinding attack.

Walk Through

The first endpoint is at /cache and accepts a url (JSON over POST) and will take a screenshot of the url provided using a headless web browser and return the image.

@api.route('/cache', methods=['POST'])
def cache():
    if not request.is_json or 'url' not in request.json:
        return abort(400)
    return cache_web(request.json['url'])

def flag():
    return send_file('flag.png')

The second is at /flag and simply returns the flag.png image, however there is a check that refuses to server this unless the request is from The challenge seems simple enough but requesting from the cache reveals that the cache blocks all IPs that equate to localhost.

def cache_web(url):
    scheme = urlparse(url).scheme
    domain = urlparse(url).hostname
    if scheme not in ['http', 'https']:
        return flash('Invalid scheme', 'danger')
    def ip2long(ip_addr):
        return unpack("!L", socket.inet_aton(ip_addr))[0]
    def is_inner_ipaddress(ip):
        print("IP (1): %s"%ip)
        ip = ip2long(ip)
        return ip2long('') >> 24 == ip >> 24 or \
                ip2long('') >> 24 == ip >> 24 or \
                ip2long('') >> 20 == ip >> 20 or \
                ip2long('') >> 16 == ip >> 16 or \
                ip2long('') >> 24 == ip >> 24
        if is_inner_ipaddress(socket.gethostbyname(domain)):
            return flash('IP not allowed', 'danger')
        return serve_screenshot_from(url, domain)
    except Exception as e:
        return flash('Invalid domain', 'danger')

def is_from_localhost(func):
    def check_ip(*args, **kwargs):
        if request.remote_addr != '':
            return abort(403)
        return func(*args, **kwargs)
    return check_ip

There are a couple hints inside this challenge, one is in the web page's title 'Rebind me' and another inside an image in the source:

Hint image contained in the in the source code

TOCTOU stands for Time of Check Time of Use, and along with the DNS rebinding hints it's apparent that we need to rapidly modify the DNS binding for a domain. For the first check to pass, the resolved IP address must not be local, however the second time the IP is resolved it needs to be local so that the headless browser will give a screenshot of the flag.

We can provide the following URL to the web applications caching service in order to complete the challenge, remembering to add 1337 as this is the port the web app is listening on inside the container:

This will now result in 3 different outcomes:

  1. FAIL The first check resolves to localhost which fails as it's not allowed in the cache_web function.
  2. FAIL - Both checks resolve to a remote address and the screenshot fails as the docker container does not allow internet traffic.
  3. WIN - The first check resolves to a remote address and the second one to localhost and we will get the flag!
First check fails and we get IP not allowed
The final flag after successfully completing a DNS rebinding TOCTOU attack on the web application

This gives us the flag for this challenge, which we have to type out manually...


Other Challenges

HTB CTF Write-up: Gunship
The HTB x Uni CTF 2020[] - Qualifiers havejust finished and I wanted write-up some of the more interesting challenges thatwe completed. As with many of the challenges the full source code was available including thefiles necessary to bui…
HTB CTF Write-up: Cargo Delivery
Cargo Delivery was a Python command line application that uses AES CBC encryption and is vulnerable to a padding oracle attack.