UrchinSec Aware CTF Writeup

この大会は2024/10/26 16:00(JST)~2024/10/28 4:00(JST)に開催されました。
この大会は個人戦。結果は1900点で126人中23位でした。
非常に残念。もう少し上位に行きたかった。

スコアのカテゴリごとの分布はこんな感じ。

1問も解けなかった問題カテゴリはReverse EngineeringとWeb Security。
Reverse Engineeringでもう1問は論理的には合っているはずですが、printableな文字列にならないので、どこか間違っているのか。。。解くことができず残念です。

自分の解けた問題をWriteupとして書いておきます。

Welcome to UrchinSec (Miscellaneous 100)

Discordに入り、#welcomeチャネルのトピックを見ると、フラグが書いてあった。

urchinsec{welcome_to_UrCh1nSe(}

Follow Us (OSINT 100)

https://www.instagram.com/urchinsec_を見てみる。2つ目の画像にコメントがあるので、見てみると、フラグが書いてあった。

urchinsec{d0nt_f0rg3t_tO_f0ll0w_us}

Heart (Secure Code Reviewing 100)

コードの脆弱性のある行と、脆弱性の名前を答える問題。コードは以下の通り。

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/name/<input_name>', methods=['GET'])
def say_name(input_name):
    if request.method == 'GET':
        if input_name is not None:
            return render_template_string(f"Hello {input_name}")

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5555)

9行目で入力値が{input_name}という形で渡されていて、SSTIの脆弱性がある。

urchinsec{9_SSTI}

RedHand (Secure Code Reviewing 100)

コードの脆弱性の名前を答える問題。コードは以下の通り。

<?=`$_GET[0]`?>

Web ShellでOSコマンドを実行できるので、Command Injectionと言える。

urchinsec{command_injection}

Syringe (Secure Code Reviewing 100)

コードの脆弱性のある行と、脆弱性の名前を答える問題。コードは以下の通り。

const express = require('express');
const mysql = require('mysql');
const app = express();

const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'Sup3rStr0ngP@ssw0rd!',
    database: 'syringe_hospital'
});

app.get('/get-patients', (req, res) => {
    const patient_name = req.query.patient_name;

    const query = `SELECT * FROM patients WHERE patient_name = '${patient_name}'`;

    connection.query(query, (error, results) => {
        if (error) throw error;
        res.send(results);
    });
});

app.listen(3000, () => {
    console.log('Server running on http://localhost:3000');
});

15行目で入力値が${patient_name}という形で渡されている。SQL Injectionの脆弱性がある。

urchinsec{15_SQLi}

10 Round (Forensics 100)

RARファイルと思われるが、先頭2~3バイト目が壊れているので、以下のように修復する。

00 00 → 61 72

解凍すると、flagファイルが展開される。

$ file flag    
flag: ARJ archive data, v11, slash-switched, created 27 aug 1980+51, original name: flag.arj, os: Unix
$ mv flag flag.arj

7.zipで解凍すると、flagファイルが展開される。

$ file flag
flag: Zstandard compressed data (v0.8+), Dictionary ID: None
$ mv flag flag.zst
$ zstd -d flag.zst            
flag.zst            : 598 bytes
$ file flag
flag: LZMA compressed data, streamed
$ mv flag flag.lzma
$ xz --format=lzma --decompress flag.lzma
xz: flag: Cannot set the file permissions: Value too large for defined data type
$ file flag
flag: 7-zip archive data, version 0.4
$ mv flag flag.7z
$ 7z x flag.7z

7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
 64-bit locale=en_US.UTF-8 Threads:32 OPEN_MAX:1024

Scanning the drive for archives:
1 file, 598 bytes (1 KiB)

Extracting archive: flag.7z
--
Path = flag.7z
Type = 7z
Physical Size = 598
Headers Size = 130
Method = LZMA2:12
Solid = -
Blocks = 1

Everything is Ok

Size:       464
Compressed: 598
$ file flag.tar.xz
flag.tar.xz: XZ compressed data, checksum CRC64
$ tar Jxfv flag.tar.xz 
flag.zip
$ unzip flag.zip
Archive:  flag.zip
 extracting: flag
$ file flag    
flag: XZ compressed data, checksum CRC64
$ mv flag flag.xz
$ unxz flag.xz  
unxz: flag: Cannot set the file permissions: Value too large for defined data type
$ file flag
flag: bzip2 compressed data, block size = 900k
$ mv flag flag.bz2
$ bzip2 -d flag.bz2
$ file flag
flag: gzip compressed data, was "flag", last modified: Fri Oct 25 05:27:03 2024, from Unix, original size modulo 2^32 47
$ mv flag flag.gz
$ gzip -d flag.gz                              
gzip: flag: Value too large for defined data type
$ file flag
flag: ASCII text
$ cat flag    
urchinsec{d0ubl3_c0mpr3s51on_1s_c00l_1cf5d3a2}
urchinsec{d0ubl3_c0mpr3s51on_1s_c00l_1cf5d3a2}

Log-ical - Part 1 (Forensics 100)

途中から以下のUserAgentでアクセスしているログがある。

Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)

これはnmapで偵察していると思われる。攻撃元IPアドレスは10.0.100.13になっている。またリファラに10.0.100.2が含まれているログがあるので、これがターゲットのIPアドレスと考えられる。

urchinsec{10.0.100.13_10.0.100.2}

Open Letter (Forensics 300)

解凍し、word\settings.xmlを見ると、XMLデータの中に以下が含まれていた。

Here is the Admin password: urchinsec{w0rd2z1p_zip2w0rd_c9f2d3a0}
urchinsec{w0rd2z1p_zip2w0rd_c9f2d3a0}

Box (Cryptography 100)

スキュタレー暗号と推測し、4文字ごとに改行し縦に読む。

b__o
oltx
xio␣
_k_␣
ieb␣
box_i_like_to_box
urchinsec{box_i_like_to_box}

Destination (Cryptography 100)

ASCIIコードをマイナス1して、デコードする。

>>> s = '118 115 100 105 106 111 116 102 100 124 66 84 68 74 74 96 117 115 53 111 116 103 49 115 110 96 50 99 105 57 102 54 126'
>>> ''.join([chr(int(c) - 1) for c in s.split(' ')])
'urchinsec{ASCII_tr4nsf0rm_1bh8e5}'
urchinsec{ASCII_tr4nsf0rm_1bh8e5}

Shifty Business (Cryptography 100)

暗号の各値を右シフトでnの各値の和だけシフトすると、フラグの各文字のASCIIコードを2乗したものになる。このことを使って復号する。

#!/usr/bin/env python3
from gmpy2 import iroot

with open('output.txt', 'r') as f:
    enc = eval(f.read())

flag = ''
for c in enc:
    v = c >> (16 + 32 + 64 + 128)
    code, success = iroot(v, 2)
    assert success
    flag += chr(code)
print(flag)
urchinsec{1t's_4all_ab0u+_e45y_3ncRyPT10N_e4c8a1}

Tr3ppl3 Stuffs (Cryptography 300)

n1とn2のGCDはpなので、q, rも算出することができる。あとはn3, n2, n1をモジュロとしたRSA暗号の復号を順に行っていけばフラグになる。

#!/usr/bin/env python3
from Crypto.Util.number import *

with open('public-key.txt', 'r') as f:
    params = f.read().splitlines()

n1 = int(params[0].split(' ')[1])
n2 = int(params[1].split(' ')[1])
n3 = int(params[2].split(' ')[1])
e = int(params[3].split(' ')[1])
c = int(params[4].split(' ')[1])

p = GCD(n1, n2)
q = n1 // p
r = n2 // p
assert q * r == n3

ns = [n1, n2, n3]
phis = [(p - 1) * (q - 1), (p - 1) * (r - 1), (q - 1) * (r - 1)]

for i in range(len(ns)):
    d = inverse(e, phis[2 - i])
    c = pow(c, d, ns[2 - i])

flag = long_to_bytes(c).decode()
print(flag)
URCHINSEC{Wh00ps_tr1ppl3_RS4_15_N0t_3v3n_h4rd_s0met1mes}

WarmUp (Cryptography 300)

暗号処理の概要は以下の通り。

・message: 未知
・knapsack = generate_knapsack()
 ・knapsack = [1, 2]
 ・以下6回繰り返し
  ・knapsackにknapsackの和にプラス1した値を追加
 ・knapsackを返却
・m = 257
・n: -1000以上1000以下ランダム整数
・ciphertext = encrypt_message(message, knapsack, m, n)
 ・bits = convert_to_bits(message)
  ・bits: messageを1文字ずつ8桁の2進数文字列にし、1ビットずつにしたものの配列
  ・bitsを返却
 ・chunk_size: knapsackの長さ
 ・chunks: bitsをchunk_sizeの長さごとにした配列
 ・ciphertext = []
 ・chunksの各chunkについて以下を実行
  ・chunkの長さがchunk_sizeより小さい場合
   ・chunkに[0]をパディング
  ・c_value: knapsackとchunkの各値の掛け算の和
  ・encrypted_value = (c_value * n) % m
  ・ciphertextにencrypted_valueを追加
 ・ciphertextを返却
・ciphertextを出力

フラグが"urchinsec{"から始まることを前提にnを割り出す。あとはフラグの各文字でブルートフォースで暗号が一致するものを割り出し、復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *

def generate_knapsack():
    knapsack = [1, 2]
    for i in range(6):
        knapsack.append(sum(knapsack) + 1)
    return knapsack

def convert_to_bits(message):
    bits = []
    for char in message:
        char_bits = bin(ord(char))[2:].zfill(8)
        bits.extend([int(b) for b in char_bits])
    return bits

knapsack = generate_knapsack()
m = 257

with open('out.txt', 'r') as f:
    ciphertext = eval(f.read().split(': ')[1])

flag_head = 'urchinsec{'
bits = convert_to_bits(flag_head)
chunk_size = len(knapsack)
chunks = [bits[i:i + chunk_size] for i in range(0, len(bits), chunk_size)]

c_value = sum(k * b for k, b in zip(knapsack, chunks[0]))
n = ciphertext[0] * inverse(c_value, m) % m
for i in range(1, len(chunks)):
    c_value = sum(k * b for k, b in zip(knapsack, chunks[i]))
    tmp_n = ciphertext[i] * inverse(c_value, m) % m
    assert tmp_n == n

flag = ''
for i in range(len(ciphertext)):
    for code in range(32, 127):
        bit = bin(code)[2:].zfill(8)
        chunk = [int(c) for c in list(bit)]
        c_value = sum(k * b for k, b in zip(knapsack, chunk))
        encrypted_value = (c_value * n) % m
        if encrypted_value == ciphertext[i]:
            flag += chr(code)
            break
print(flag)
urchinsec{w000oow!!!_M4st3r_H0w_d1d_y0u_g3t_it????}