ångstromCTF 2019 の writeup

はじめに

ångstromCTF 2019 にチームNekochanNano!で参加しました.
結果は1590ptsで59位でした.
以下は,私が解いた問題のwriteupです.

The Mueller Report

非常に大きなPDFファイルが渡されます. 以下のコマンドによってactf{というキーワードを含む文字列検索したところ,FLAGが見つかりました.

❯❯❯ strings full-mueller-report.pdf | grep actf{
actf{no0o0o0_col1l1l1luuuusiioooon}

Blank Paper

PDFをダウンロードして開こうとしますが,うまく開けません.
問題文によると,ファイルの一部のバイトが抜け落ちているようなので,hexdumpでバイナリを見てみると,ファイルの先頭が00 00 00 00になっています.

$ hexdump blank_paper.pdf | head
0000000 00 00 00 00 2d 31 2e 35 0a 25 bf f7 a2 fe 0a 33
0000010 31 20 30 20 6f 62 6a 0a 3c 3c 20 2f 4c 69 6e 65
0000020 61 72 69 7a 65 64 20 31 20 2f 4c 20 33 35 34 38

一般的にファイルには,そのファイルの種類を識別するためのMagic Numberという識別子がファイルの先頭に付与されています.
ファイルの種類を識別するためのfileコマンドは,このMagic Numberをもとに結果を出力します.

PDFファイルにおいては,Magic Number25 50 44 46と定められています.
そこで,もとのファイルの先頭部分を25 50 44 46に書き換えてファイルを閲覧すると,FLAGを取得することができます.

actf{knotveryinteresting}

Paper Bin

datファイルが渡されます.
binwalkを実行すると,大量のファイルが含まれていることがわかります.

$ binwalk paper_bin.dat

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
222           0xDE            PDF document, version: "1.4"
298           0x12A           Zlib compressed data, best compression
3578          0xDFA           Zlib compressed data, best compression
7820          0x1E8C          Zlib compressed data, best compression
12776         0x31E8          Zlib compressed data, best compression
17453         0x442D          Zlib compressed data, best compression
21935         0x55AF          Zlib compressed data, best compression
...

foremostを用いて,中に含まれているファイルを抽出してみます.

$ foremost paper_bin.dat

すると,大量のPDFファイルが抽出されます.

$ cd output/pdf
$ ls
00000000.pdf 00001384.pdf 00002824.pdf 00004240.pdf 00005728.pdf 00007088.pdf 00008616.pdf 00009896.pdf 00011232.pdf 00012472.pdf
00000688.pdf 00002112.pdf 00003496.pdf 00004944.pdf 00006448.pdf 00007912.pdf 00009256.pdf 00010576.pdf 00011880.pdf 00013224.pdf

このPDFファイルを1つ1つみていくと,FLAGが含まれたPDFファイルを見つけることができます.

actf{proof_by_triviality}

Paper Trail

pcapngファイルが渡されます. Wiresharkpcapngファイルを開いてみると,IRCプロトコルで何らかの情報をやり取りしているパケット群であることがわかります.

Scapyを用いてパケットのペイロード部分を抽出してみます.

from scapy.all import *

def main():
    pkts = rdpcap("./paper_trail.pcapng")

    for pkt in pkts:
        if "Raw" not in pkt:
            continue

        print(pkt["Raw"].load.decode(), end="")



if __name__ == '__main__':
    main()

すると,出力中にactf{...のようなメッセージが見て取れます.

❯❯❯ python solver.py
ifconfig: interface vboxnet does not exist
ifconfig: interface vboxnet does not exist
ifconfig: interface vboxnet does not exist
ifconfig: interface vboxnet does not exist
PRIVMSG defund :I have to confide in someone, even if it's myself
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :I have to confide in someone, even if it's myself
PRIVMSG defund :my publications are all randomly generated :(
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :my publications are all randomly generated :(
PRIVMSG defund :a
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :a
PRIVMSG defund :c
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :c
PRIVMSG defund :t
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :t
PRIVMSG defund :f
:defund!~defund@ec2-18-209-123-192.compute-1.amazonaws.com PRIVMSG defund :f
PRIVMSG defund :{

その部分だけ抽出するような以下のスクリプトによって,FLAGが抽出できました.

from scapy.all import *

def main():
    pkts = rdpcap("./paper_trail.pcapng")

    for pkt in pkts[6:]:
        if "Raw" not in pkt:
            continue

        msg = pkt["Raw"].load.decode()

        if msg.startswith("PRIVMSG"):
            _, flag = msg.split(":")
            print(flag.strip(), end="")


if __name__ == '__main__':
    main()
❯❯❯ python solver.py
actf{fake_math_papers}

Just Letters

AlphaBetaというプログラミング言語Wikinc 54.159.113.26 19600というコマンドが提示されます.

nc 54.159.113.26 19600を実行すると,以下のような内容が表示されました.

❯❯❯ nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:

どうやら,FLAGはメモリの先頭に格納されているようです.
つまり,以下のような処理を組み立てられると,FLAGが取得できそうだと考えられます.

pointer = 0
while(true){
    register = memory[pointer]
    print(register)
    pointer++
}

Wikiの内容を読み解いていくと,以下の命令が使えそうだとわかります.

Y     sets the register to 0
G     sets register 1 to the memory at the memory pointer
C     sets register 3 to the value of register 1
L     outputs a character to the screen
S     adds 1 to the register

なぜなら,以下のAlphaBetaプログラムによって,最初に考えた処理を構築できるからです.

YGCLSGCLSGCLSGCLSGCLSGCLSGCLS...

pointer = 0                     ; Y
while(true){
    register = memory[pointer]  ; G C
    print(register)             ; L
    pointer++                   ; S
}

このAlphaBetaプログラムを入力したところ,FLAGが取得できました.

❯❯❯ nc 54.159.113.26 19600
Welcome to the AlphaBeta interpreter! The flag is at the start of memory. You get one line:
> YGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLSGCLS
actf{esolangs_sure_are_fun!}

Lithp

Lispっぽいプログラムが渡されます.

;LITHP

(defparameter *encrypted* '(8930 15006 8930 10302 11772 13806 13340 11556 12432 13340 10712 10100 11556 12432 9312 10712 10100 10100 8930 10920 8930 5256 9312 9702 8930 10712 15500 9312))
(defparameter *flag* '(redacted))
(defparameter *reorder* '(19 4 14 3 10 17 24 22 8 2 5 11 7 26 0 25 18 6 21 23 9 13 16 1 12 15 27 20))

(defun enc (plain)
    (setf uwuth (multh plain))
    (setf uwuth (owo uwuth))
    (setf out nil)
    (dotimes (ind (length plain) out)
        (setq out (append out (list (/ (nth ind uwuth) -1))))))


    
(defun multh (plain)
    (cond
        ((null plain) nil)
        (t (cons (whats-this (- 1 (car plain)) (car plain)) (multh (cdr plain))))))

(defun owo (inpth)
    (setf out nil)
    (do ((redth *reorder* (cdr redth)))
        ((null redth) out)
        (setq out (append out (list (nth (car redth) inpth))))))

(defun whats-this (x y)
    (cond
        ((equal y 0) 0)
        (t (+ (whats-this x (- y 1)) x))))

;flag was encrypted with (enc *flag*) to give *encrypted*

encrypted, flag, reorderという変数と,enc, multh, owo, whats-thisという関数が定義されています.
他のdotimesconssetfといったキーワードは一般的なLispに用いられるものなので,リファレンスをもとに処理の流れを追いました.

このプログラムをPython風に変換すると以下のようになります.

encrypted = [8930, 15006, 8930, 10302, 11772, 13806, 13340, 11556, 12432, 13340, 10712, 10100, 11556, 12432, 9312, 10712, 10100, 10100, 8930, 10920, 8930, 5256, 9312, 9702, 8930, 10712, 15500, 9312]
flag = None
reorder = [19, 4, 14, 3, 10, 17, 24, 22, 8, 2, 5, 11, 7, 26, 0, 25, 18, 6, 21, 23, 9, 13, 16, 1, 12, 15, 27, 20]

def enc(plain):
    uwuth = multh(plain)
    uwuth = owo(uwuth)
    out = None

    for ind in range(len(plain)):
        out = uwuth[ind] // -1
        print(out)

def multh(plain):
    if plain == None:
        return None
    if True:
        return whats_this(plain[0]-1, plain[0]) + multh(plalin[1:])

def owo(inpth):
    # inpthをreorderの順に並べかえ

def whats_this(x, y):
    if y == 0:
        return 0
    else:
        return x + whats_this(x, y-1)

これらをもとに,平文を復号する以下のスクリプトを実装しました.

encrypted = [8930, 15006, 8930, 10302, 11772, 13806, 13340, 11556, 12432, 13340, 10712, 10100, 11556, 12432, 9312, 10712, 10100, 10100, 8930, 10920, 8930, 5256, 9312, 9702, 8930, 10712, 15500, 9312]
plain = None
reorder = [19, 4, 14, 3, 10, 17, 24, 22, 8, 2, 5, 11, 7, 26, 0, 25, 18, 6, 21, 23, 9, 13, 16, 1, 12, 15, 27, 20]

encrypted_ordered = [0] * len(encrypted)

for e, r in zip(encrypted, reorder):
    encrypted_ordered[r-1] = e

encrypted_ordered.insert(0, encrypted_ordered[-1])
encrypted_ordered = encrypted_ordered[:-1]

for e in encrypted_ordered:
    for x in range(1, 200):
        if (x-1) * x == e:
            print(chr(x), end="")

print()

このスクリプトを実行すると,FLAGが得られます.

❯❯❯ python solver.py
actf{help_me_I_have_a_lithp}

Scratch It Out

project.jsonというあまりJSONファイルが渡されます.
JSONを見てみると,"name": "Sprite1""currentCostume": 0,といった特徴的なデータが見て取れます.
それらのキーワードで検索をかけると,ビジュアルプログラミング言語である`Scratchの情報が見つかります.
よって,このJSONファイルはScratchのプログラムをエクスポートしたものだと考えました.

さて,Scratchはプログラムをimport/exportする際にsb2というファイル形式を用います.
sb2jsonの関係は以下のようになっています.

json -(zip)-> sb2
sb2 -(unzip)-> json

以下のコマンドで,jsonファイルをZIP圧縮し,拡張子をsb2に設定します.

❯❯❯ zip project.sb2 project.json

こうしてできたproject.sb2をScratchにimportさせると,ScratchプログラムによってFLAGが出力されます.

actf{Th5_0pT1maL_LANgUaG3}

No Sequels

以下のようなNode.jsのサーバプログラムが示されます.

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

...

router.post('/login', verifyJwt, function (req, res) {
    // monk instance
    var db = req.db;

    var user = req.body.username;
    var pass = req.body.password;

    if (!user || !pass){
        res.send("One or more fields were not provided.");
    }
    var query = {
        username: user,
        password: pass
    }

    db.collection('users').findOne(query, function (err, user) {
        if (!user){
            res.send("Wrong username or password");
            return
        }

        res.cookie('token', jwt.sign({name: user.username, authenticated: true}, secret));
        res.redirect("/site");
    });
});

プログラムから,MongoDBに対してユーザが入力した値からqueryを構築し,findOneによって結果が見つかった場合にのみJWTがセットされて/siteにリダイレクトされることがわかります.
ここで,入力値の型チェックを正しく行なっていないことから,JSONを入力値としたNoSQLiが可能だと考えました.
例えば,以下のようなデータをJSON形式でPOSTすると,usernamepasswordが両方とも空の任意の行にマッチするクエリが構築できてしまいます.

{
    "username": {
        "$ne": ""
    },
    "password": {
        "$ne": ""
    }
}

このようなデータをPOSTしたところFLAGが取得できました.

actf{no_sql_doesn't_mean_no_vuln}