• BLOG

  • Some security precautions when writing CTF challenges (EN)

    sam. 15 juillet 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    These last weeks, I was working on organizing a "Capture The Flag" (CTF) contest within my company. This CTF was part of a security awareness training, and it was, in my opinion, funnier and more entertaining than a classic e-learning session or a phishing campaign.

    WTF is a CTF?

    For those who don't know what a CTF is, it is a set of games and challenges that you have to accomplish in order to obtain a flag. The more flag you obtain, the more points you have. Obviously, the goal is to have as much point as possible.

    In my case, the CTF was created for IT people (mostly, but not only, developers), so most of the challenges required scripting skills.

    For convenience purposes, many challenges are written in PHP, enabling me to write more of them very quickly, on a shared environment. However, the language is often irrelevant, because we did not want to test "language-specific" skills, but "logic-specific" skills.

    Example of a challenge

    For instance, let's have a look at this (really easy) challenge:

    chall.php

    <?php
    session_start();
    if (!isset($_SESSION['result'])) {
        $_SESSION['result']=md5(rand());
        print "Please send " . $_SESSION['result'] . " to /chall.php?result={XXX} in less than 1s";
    
    }
    elseif(time()-$_SESSION['time']>1){
        print "Time's up!";
        session_destroy();
    }
    elseif($_GET['result']==$_SESSION['total']){
        print "Good game ! The flag is : {QWERTY1234}";
    else {
        print "Try again...";
    }
    

    It is a really simple challenge, where the player is given a random string that he will have to send back to another URL very quickly. In order to do that, the best solution would be to script this.

    But sometimes ...

    However, the source code of a challenge is rarely given, so sometimes, players do not look in the right direction, and they might end up using tools or automated scans on the server.

    Using these types of tools in challenges is considered as not "fair-play":

    • At best, it is cheating (using SQLMap to exploit a SQL injection works, but what is the point doing this? This is not a blog "tutorial", it is an awareness training);
    • It is sending a lot of unnecessary requests (and increase the server workload) on a shared environment containing different challenges and used by other challengers.

    The worst case scenario would be to end up with availability problems for other challenges and challengers... That is not really cool!

    Obviously, you cannot have a Web Application Firewall (WAF) in front of your server, because the goal is to exploit vulnerabilities, and if you block all requests containing a payload, players cannot play!

    So I ended up implementing some solutions to protect my challenges.

    Protections against request flooding

    In order to prevent users from flooding the server with HTTP requests, I want to implement a to limit the number of queries per second.

    I think 10 queries in 10 seconds is a good limit: - Overall, it is equivalant to 1 request per second; - It allows a quick burst of 10 requests, if needed.

    Now, let's find a way to implement this limit. I thought about using iptable, but the server is listening in HTTPS and we cannot count requests if challengers do not close the TLS session.

    We could also use an nginx module, but players sharing the same IP address (if they use a corporate web proxy, they share the same IP address) could block each other.

    A last solution, the one chosen, would be to limit the amount of requests using session cookie:

    <?php
    if(!$_SESSION['timeframe']) {
        $_SESSION['timeframe']=(int)(mktime()/10);
        $_SESSION['nb']=0;
    }
    
    $cur_timeframe = (int)(mktime()/10);
    
    if($cur_timeframe==$_SESSION['timeframe']) {
        $_SESSION['nb']+=1;
    }
    else {
        $_SESSION['timeframe']=(int)(mktime()/10);
        $_SESSION['nb']=0;
    }
    
    if($_SESSION['nb'] > 10) {
      $_SESSION['banned'] = mktime() + 60;
    }
    
    if($_SESSION['banned'] and $_SESSION['banned']>mktime()) {
      echo "Too many request, wait ".($_SESSION['banned']-mktime())."s";
      die();
    }
    

    It works with the following logic: - If your session does not contain a timeframe variable: - Create a timeframe session variable with current epoch time divided by 10. - Create a nb session variable (which will contains the number of request performed in the last 10 seconds), set to 0. - Create a variable cur_timeframe with current epoch time divided by 10.

    • If the session timeframe and the current time frame is the same:
    • We increment the nb session variable.
    • If not:
    • We are in a different timeframe, we can reset nb to 0.

    • If nb > 10:

    • The player performed too many request, he should be blocked for 60s, let's save this information in banned session variable.

    • If banned is set and the block time hasn't passed yet:

    • Prevent the user that he's blocked, and exit the script.

    Protection against multiple session

    This protection against request flooding is far from perfect. It is even very simple to bypass: you just need to remove all cookie for all your request, so the server think each request is on a new session, so each request is allowed.

    In order to prevent our mechanism against this flaw, we need to secure session so that players cannot create "automatically" a session. In order to do this, we will use a captcha mechanism (ex: Google reCaptcha).

    We implement it like with the following code:

    <?php
    session_start();
    if(!$_SESSION['notbot']) {
        if(!$_POST['g-recaptcha-response']) {
            ?>
            <script src='https://www.google.com/recaptcha/api.js'></script>
            Beware, this website is protected against request flooding (Max. 10 requests per 10 sec.!
            <form method="POST">
                <div class="g-recaptcha" data-sitekey="{RECAPATCHA_API_CODE}"></div>
                <input type="submit" />
            </form>
            <?php
            die();
        }
        else {
            $post_data = http_build_query(array(
                'secret' => "{RECAPATCHA_API_SECRET}",
                'response' => $_POST['g-recaptcha-response'],
                'remoteip' => $_SERVER['REMOTE_ADDR']));
            $opts = array('http' => array(
                'method'  => 'POST',
                'header'  => 'Content-type: application/x-www-form-urlencoded',
                'content' => $post_data));
            $context  = stream_context_create($opts);
            $response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
            $result = json_decode($response);
            if (!$result->success) {
                echo "You are a bot ?";
                die();
            }
            $_SESSION['notbot']=true;
        }
    }
    

    Protections against scanners

    Now that we are "protected" against request spamming, let's protect the challenge against some scanner.

    We cannot really "protect" against scanner, as it is always possible to mimic human behavior on the Internet. However, we can easily implement a first layer of security by analyzing the user-agent of the browser:

    <?php
    $agent = filter_input(INPUT_SERVER, 'HTTP_USER_AGENT');
    if (strpos($agent, 'w3af') !== false ||
        strpos($agent, 'dirbuster') !== false ||
        strpos($agent, 'nikto') !== false ||
        strpos($agent, 'fimap') !== false ||
        strpos($agent, 'nessus') !== false ||
        strpos($agent, 'whatweb') !== false ||
        strpos($agent, 'Openvas') !== false ||
        strpos($agent, 'jbrofuzz') !== false ||
        strpos($agent, 'webshag') !== false ||
        strpos($agent, 'sqlmap') !== false)
    {
          echo "Scanner detected !";
          $_SESSION['banned'] = mktime() + 60;
          die();
    }
    

    (The banned session variable will be handled by the previous anti request flooding script)

    Making it a library

    We can than mix all those script in one script :

    chall_defender.php

    <?php
    session_start();
    
    $agent = filter_input(INPUT_SERVER, 'HTTP_USER_AGENT');
    if (strpos($agent, 'w3af') !== false ||
        strpos($agent, 'dirbuster') !== false ||
        strpos($agent, 'nikto') !== false ||
        strpos($agent, 'fimap') !== false ||
        strpos($agent, 'nessus') !== false ||
        strpos($agent, 'whatweb') !== false ||
        strpos($agent, 'Openvas') !== false ||
        strpos($agent, 'jbrofuzz') !== false ||
        strpos($agent, 'webshag') !== false ||
        strpos($agent, 'sqlmap') !== false)
    {
          echo "Scanner detected !";
          $_SESSION['banned'] = mktime() + 60;
          die();
    }
    
    if(!$_SESSION['timeframe']) {
        $_SESSION['timeframe']=(int)(mktime()/10);
        $_SESSION['nb']=0;
    }
    
    $cur_timeframe = (int)(mktime()/10);
    
    if($cur_timeframe==$_SESSION['timeframe']) {
        $_SESSION['nb']+=1;
    }
    else {
        $_SESSION['timeframe']=(int)(mktime()/10);
        $_SESSION['nb']=0;
    }
    
    if($_SESSION['nb'] > 10) {
      $_SESSION['banned'] = mktime() + 60;
    }
    
    if($_SESSION['banned'] and $_SESSION['banned']>mktime()) {
      echo "Too many request, wait ".($_SESSION['banned']-mktime())."s";
      die();
    }
    
    if(!$_SESSION['notbot']) {
        if(!$_POST['g-recaptcha-response']) {
            ?>
            <script src='https://www.google.com/recaptcha/api.js'></script>
            Beware, this website is protected against request flooding (Max. 10 requests per 10 sec.!
            <form method="POST">
                <div class="g-recaptcha" data-sitekey="{RECAPATCHA_API_CODE}"></div>
                <input type="submit" />
            </form>
            <?php
            die();
        }
        else {
            $post_data = http_build_query(array(
                'secret' => "{RECAPATCHA_API_SECRET}",
                'response' => $_POST['g-recaptcha-response'],
                'remoteip' => $_SERVER['REMOTE_ADDR']));
            $opts = array('http' => array(
                'method'  => 'POST',
                'header'  => 'Content-type: application/x-www-form-urlencoded',
                'content' => $post_data));
            $context  = stream_context_create($opts);
            $response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
            $result = json_decode($response);
            if (!$result->success) {
                echo "You are a bot ?";
                die();
            }
            $_SESSION['notbot']=true;
        }
    }
    

    And integrate the script within our previous challenge:

    chall.php

    <?php
    include("chall_defender.php");
    if (!isset($_SESSION['result'])) {
        $_SESSION['result']=md5(rand());
        print "Please send " . $_SESSION['result'] . " to /chall.php?result={XXX} in less than 1s";
    
    }
    elseif(time()-$_SESSION['time']>1){
        print "Time's up!";
        session_destroy();
    }
    elseif($_GET['result']==$_SESSION['total']){
        print "Good game ! The flag is : {QWERTY1234}";
    else {
        print "Try again...";
    }
    
  • Comments