LINE CTF 2023 writeup

https://linecorp.com/ja/pr/news/ja/2023/4508

Welcome問題を含めて5問しか解いていないけど、579点で29位。

LINE CTFは問題が難しい。

![score.linectf.me_challenges(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/366c8345-75b7-700c-c504-d7a8acc51f7d.png)

![score.linectf.me_team(capture (1280)).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25276/8c2953e0-8d2d-0596-a865-10b6ebc52739.png)

## Welcome (Misc)

> Welcome to LINE CTF 2023 🎉 Please check our announcement on discord!

> **LINE CTF 2023 is started!**
> All challenges are opened!
> Please enjoy challenges!
> `LINECTF{e305dbd3a5893f92701ef9d125a9f77b}`

`LINECTF{e305dbd3a5893f92701ef9d125a9f77b}`

## Baby Simple GoCurl (Web)

発展問題（Adult Simple GoCurl）があることに気が付かず、コンテスト中はAdult Simple GoCurlの解法で解いていた。ここに書く解法はコンテスト後に解き直したもの。

指定したURLからページを取得するAPIと、内部からしか叩けないフラグを取得するAPIがあるという良くある問題。

`/flag/` はフラグを返すが、リモートのIPアドレスが127.0.0.1でないとエラーになる。

`/curl/` は指定したURLからページを取得して返す。`ClientIP()` が127.0.0.1ではない場合、URLに `flag` や `curl` 、`%` が含まれているとエラーになる。`%` が対策されているのでパーセントエンコードで `%66lag` のように回避することはできない。禁止文字列に `curl` があるので、 `curl` をもう1回叩かせることもできない。

定番の手法として、 http://127.0.0.1:8080/flag にリダイレクトするページを用意して踏ませることが考えられる。しかし、リダイレクト元のページが127.0.0.1という制限が加えられている。

`X-Forwarded-For` でIPアドレスを偽装するという方法も考えられるが、アプリのコードで特におかしなことをはしていないし、Ginのような有名なフレームワークで、デフォルト設定が危険ということもないだろう……。え、通るの！？！？

```shell-session
$ curl -H 'X-Forwarded-For: 127.0.0.1' http://34.146.230.233:11000/curl/?url=http%3A%2F%2F127.0.0.1%3A8080%2Fflag
{"body":"{\"message\":\"= LINECTF{6a22ff56112a69f9ba1bfb4e20da5587}\"}","status":"200 OK"}
```

ソースコードも眺めて「ちゃんと信頼できるプロキシがチェックしているよなぁ」とか思っていたけど、これ、どんなIPアドレスでも信頼する設定だったわ。

```go
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
```

https://github.com/gin-gonic/gin/blob/master/gin.go#L205

手元で動かしたら警告ログが出ていた。デフォルト設定は危険だけど警告を出すという方針か。手元で動かすのは重要。

```
>docker build -t line2023_babygocurl .
 :
>docker run --rm -it --entrypoint bash line2023_babygocurl
gocurl@ee1decc8e499:/usr/local/opt/gocurl$ ./main
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] Loaded HTML Templates (2):
        -
        - index.html

[GIN-debug] GET    /static/*filepath         --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] HEAD   /static/*filepath         --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (3 handlers)
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /curl/                    --> main.main.func2 (3 handlers)
[GIN-debug] GET    /flag/                    --> main.main.func3 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

`LINECTF{6a22ff56112a69f9ba1bfb4e20da5587}`

## Old Pal (Web)

Perl。パスワードと言っているが、要は制約を回避して指定された式を通す文字列を見つけろという問題。

条件は次の通り。

- 長さは20文字未満
- `0-9a-zA-Z_-` のみで構成される
- 数字と英字、記号（`_`, `-`)の各文字種を含まなければいけない
- `/[0-9_-][boxe]/i` にマッチしてはいけない
  - `0x134b0b5` などがNG
- 大量に列挙された関数名っぽい文字列が含まれていてはいけない
- `/[Mx. squ1ffy]/i` にマッチしてはいけない
- `eval("$pw == 20230325")` が真となる

そもそも `20230325` が禁止されているわけではないので、20230325をどうやって作るかではなく、英字と記号をどうやって含めるかという問題だろう。記号は `_` が数字の区切りに使えるので簡単。`2023_03_25` とか。あるいは、`-` を引き算に使っても良い。問題は英字をどうするか。

`eval($pw) == 20230325` ではないのがちょっと気になる。`1 or 1`とか？　でも、空白が使えない。

数値が入ったグロバール変数が何かしらあれば、あとは引き算で何とかなる。何かしらあるだろ。あった。 `__LINE__` 。LINE CTFにふさわしい問題で良い。 `20230326-__LINE__`。

`LINECTF{3e05d493c941cfe0dd81b70dbf2d972b}`

## Fishing (rev)

Windowsの実行可能ファイルの解析。

```
>fishing.exe
Wanna catch a fish? Gimme the flag first: LINECTF{hogefuga}
Too bad! Not even a nibble...
```

デバッガ検知をしている関数を潰し、不正な命令を挟んで `jmp` で飛ばすやつを「邪魔だな」と思いながら進めていく。で、途中で `EXCEPTION_SINGLE_STEP` で止まりまくってどうしようもなくなる。この例外を無視するとステップ実行ができない。

面倒だがコードを読めば良いだろ……と読むと、こういう処理をしていた。不正な命令はそんなに多くはないので、Ghidraである程度逆コンパイルができて助けになった。

```python
P = "LINECTF{hogefuga}"

P = P.encode()
P = [p^0x21 for p in P]
P = [(p-0x22)%256 for p in P]
P = [p^0x11 for p in P]
P = [(p+0x12)%256 for p in P]

def encrypt(P, K):
  S = list(range(256))
  j = 0
  for i in range(256):
    j = (j+S[i]+K[i%len(K)])%256
    S[i], S[j] = S[j], S[i]

  C = []
  i = 0
  j = 0
  for p in P:
    i = (i+1)%256
    j = (j+S[i])%256
    S[i], S[j] = S[j], S[i]
    k = S[(S[i]+S[j])%256]
    C +=[p^k^(j-0x18)%256]
  return C

K = [
  0x23, 0x22, 0x65, 0x4f, 0xa9, 0x5f, 0x50, 0x4e, 0x5d, 0x2d, 0x2c, 0x5d, 0x35, 0x26, 0x31, 0x5d,
  0x26, 0x4d, 0x4d, 0x49,
]

C = [
  0xd0, 0xbe, 0x9f, 0x5a, 0xbd, 0xf0, 0x34, 0xb5, 0xd0, 0x6f, 0xfb, 0xe2, 0x99, 0xba, 0xae, 0xd7,
  0x36, 0xd5, 0x2d, 0xc2, 0x22, 0x45, 0xb0, 0x03, 0x9d, 0x63, 0x66, 0x53, 0xc7, 0x28, 0xcc, 0x2a,
  0x2b, 0x14, 0xbb, 0x09, 0x9b, 0xe3, 0x60, 0x46, 0x3a,
]

P = encrypt(P, K)

if C==P:
  print("OK")
else:
  print("NG")
```

`encrypt` はほぼRC4だが、`^(j-0x18)` がRC4には無い。いやらしい。

`C` から逆算してみても、意味のある文字列にはならなかった。

エラー文を出して終了するだけではなく、こっそり挙動を変えるデバッガ検知もあるのだろうか。最後の比較のところのコードを無限ループに書き換えて、デバッガ無しで動かし、止まったところでアタッチしてみた。`K` が `"m4g1KaRp_ON_7H3_Hook"` という文字列になっていた（！？）。この文字列に変えて逆算してもやっぱりダメ。他にも何かあるのかな……。

暗号化の処理が1文字ずつ。最初のxorや加減算はとうぜんそうだし、RC4は平文が後続に暗号文に影響を与えない。他の問題からフラグのフォーマットは分かっているので、無限ループするようにしたプログラムに `LINECTF{00000000000000000000000000000000}` から `LINECTF{ffffffffffffffffffffffffffffffff}` を入力し、フラグを1文字ずつ拾った。面倒。

```
>fishing.exe
Wanna catch a fish? Gimme the flag first: LINECTF{e255cda25f1a8a634b31458d2ec405b6}
Correct! You get a fish!
```

`LINECTF{e255cda25f1a8a634b31458d2ec405b6}`

## Adult Simple GoCurl (Web)

Baby Simple GoCurlから、`/curl/` で `ClientIP()` が127.0.0.1ならスルーという処理が消え、`flag` や `curl` 、`%` は常に弾かれるようになった。

リダイレクト元127.0.0.1ならリダイレクトができるので、このアプリ自体にリダイレクトさせるしかないけど、リダイレクトするような処理は無いよな……。と色々試していたら、リダイレクトするパターンがあった。末尾の `/` が無かったら、 `/` を付けたパスにリダイレクト。

```shell-session
$ curl -i http://34.84.87.77:11001/flag
HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=utf-8
Location: /flag/
Date: Sun, 26 Mar 2023 12:28:30 GMT
Content-Length: 41

<a href="/flag/">Moved Permanently</a>.
```

リダイレクトしているコードはこれ。

```go
func redirectTrailingSlash(c *Context) {
	req := c.Request
	p := req.URL.Path
	if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
		prefix = regSafePrefix.ReplaceAllString(prefix, "")
		prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")

		p = prefix + "/" + req.URL.Path
	}
	req.URL.Path = p + "/"
	if length := len(p); length > 1 && p[length-1] == '/' {
		req.URL.Path = p[:length-1]
	}
	redirectRequest(c)
}
```

https://github.com/gin-gonic/gin/blob/a889c58de78711cb9b53de6cfcc9272c8518c729/gin.go#L671-L685

`X-Forwarded-Prefix` という謎のHTTPヘッダを見ている。これが答えだろう。

これが何かというと、リバースプロキシで http://example.com/app/hoge を http://app:8080/hoge にリダイレクトしているような状況のとき、リダイレクトされる側のappが/hoge2にリダイレクトすると http://example.com/app/hoge2 ではなく http://example.com/hoge2 になってしまうというのを何とかするためのものらしい。あー、これ困ったことがある。とはいえ、非標準のヘッダだし、この問題を考えるとサポートが続くのかも分からないし、使うのは止めたほうが良いのかな。

ということで、 `url` を `http://127.0.0.1:8080//` に、 `header_key` を `X-Forwarded-Prefix` に、 `header_value` を `/flag` にすると通った。`url` の末尾が `//` なのは、パスが空文字列だとここまで処理が来ないため。

`LINECTF{6a22ff56112a69f9ba1bfb4e20da5587}`