实战 | 代码审计中的SQL注入和预编译中的SQL注入
Posted 行长叠报
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战 | 代码审计中的SQL注入和预编译中的SQL注入相关的知识,希望对你有一定的参考价值。
大家好,我是小编
今天的文章来自Gcow团队的无法
内容较长
大家静心观看
前言
本文围绕代码审计中的SQL注入进行讲解,以下是几种常见的注入类型:
联合查询注入
布尔盲注
延时注入
-
报错注入
本文主要探讨的主题是代码审计SQL注入与预编译中的SQL注入。
有人或许有疑问 “ 预编译好像不存在SQL注入吧?”
那我们来看看吧!
注:文中程序是笔者自己开发的一套未完成的简单框架。
框架结构:
上图是框架的结构图,我们框架的应用程序在application目录下,框架核心文件在./OW/Lib/Core目录下,先来看下Controller目录:
这个目录下只有一个indexController.class.php文件,这是它的代码:
因为涉及到框架相关,在此普及一下框架中的审计方法助于阅读后文:
一个框架中涉及到的最频繁的不是类就是函数。这里我们简要概述一下函数回溯,函数回溯其实就是一个逆向追踪的过程,根据遇到的函数进行函数的定位,定位到某函数后,读这个函数的功能,而某些函数可以以它的命名就能看出功能。
比如上图中的函数input(),就可以从字面意思理解为输入。可以认为这个函数可以接收外部所传入的无论是get、post还是cookie参数。
这个M()函数可以从框架的角度来看,因为框架都是遵从M(model|模型)V(view|视图)C(controller|控制器),所以这个M函数是获取某个模型的函数。
比如$this->templet,直观上看这是一个成员属性也就是类中的变量,但这个变量可以调用函数,所以它并非只是一个变量或者字符,说明这个成员属性的类型是一个object。
当我们无法确定这到底是变量还是数组的时候可以用var_dump()系统函数或者用框架中的dump()函数打印出来。
我们来看看截图:
可以看到有一个object关键字,说明这是一个对象。
或许有人会问“框架中的dump和系统中的var_dump函数不一样吗?”
当然是一样的,我们或许是为了美化输出或许是为了安全考虑所以需要重新定义一些函数。
回到刚才的代码:
因为是框架,所以读取的代码是以函数为单位进行的,并非是从文件的第一行读到最后,而是从Index()函数看着走,12-18行。
可以看到调用input()函数并将该函数的返回值赋值给了变量id,既然可以从函数的字面意思来进行理解该函数的作用,那么这个函数就是用来接收用户输入的参数。
因为框架中载入了Global.fun.php文件,所以这是一个全局函数文件。
可以看到input函数就在其中。
Input 函数分析:
我们分析一下这个函数:
1. 进入函数后,首先将$result、$method、$key、$rex这四个变量赋值为false;
2. 将$input以分割为数组,将分割出来的数组传入count函数后判断这个数组中的元素是否有两个:
如果有两个的情况下那么使用list函数将以点分割的$input数组分别赋值给$method、$key;
如果不满足判断条件则直接将$input赋值给$key;
3. 将$key以空格分割为数组并且分别赋值给$key、$rex(这里$rex的作用是过滤指定的某些字符):
判断$rex是否存在,不存在那么将$rex默认赋值为/w;
判断$method是否存在,不存在默认赋值为get;
4.在30-50行中定义了一个匿名函数,经过分析,这个匿名函数中有两个形参,分别为$value、$rex;
5.进入函数后首先赋值$result为false,随后进入switch判断语句中:
当$rex为/d的时候将$value中除数字之外的字符全部替换为空;
当$rex为/s的时候将$value中除0-9A-Za-z_之外的字符替换为空;
当$rex为/c的时候,直接将$value赋值给$result;
如果$rex无值的情况下那么直接将$value除0-9A-Za-z之外的字符替换为空;
6.最后匿名函数返回$result。
我们继续分析:
定义了一个匿名函数,这个函数的作用是判断$_GET、$_COOKIE、$_POST中传入的键是否存在,存在就不用赋值为默认值,不存在就赋值为默认值。
函数定义的同时定义了4个形参继承了上边的$filter匿名函数。
进入这个函数后首先判断$key数组中是否存在$method这个键:
若不存在:将$value,$rex传入到$filter匿名函数中,并将匿名函数的返回值赋值给$method[$key]中;
若存在:将$method[$key],$rex传入到$filter匿名函数中并将返回值赋值给$method[$key]。
我们接着往下分析:
进入一个switch判断;
$method为get的时候将$_GET,$key,$value,$rex赋值到$by_value中;
$method为post的时候将$_POST,$key,$value,$rex赋值到$by_value中;
$method为cookie的时候将$_COOKIE$key,$value,$rex赋值到$by_value中;
最后将$result返回。
可以理解为将用户可以操作的外部参数进行过滤。
分析结束,回到起点:
经过分析可以看到/c是没有过滤的,所以这可能是注入的前奏。
13行看到名为M的函数可理解为Model,这个函数应该是实例化某个模型后返回一个对象。
分析函数:
1. 首先进入函数后定义一个变量modeldir(模型目录),后将变量modelfile赋值为拼接之后的model文件名;
2. 判断$modelfile文件是否存在,若存在就包含$modelfile文件;因为框架中使用到了命名空间,所以需要以命名空间的形式来实例化这个对象
3. 若不存在$modelfile文件就抛出异常并传入{$model} file does not exist 字样。
4. 然后将模型文件定义到./application/index/model/IndexModel.class.php,从控制器index中我们了解到它实例化了这个indexmodel类并且调用了index()函数;
5. 进入这个函数后首先判断$id中是否包含;
若存在那么将$id以,分割为数组;
若不存在那么直接将$id变化为数组;
6. 我们接着进入下一行可以看到这里有一个$this->link的成员属性;它可以调用函数,说明这是一个对象,但是在当前页面中并没有看到定义了成员属性link,我们可以看到第三行定义类的时候有一个关键字为extends的,这个类继承了命名空间OWLibCore中的Model类。
7. 我们跟踪到./OW/Lib/Core/Model.class.php
8. 进入这个类中可以看到有一个构造函数。函数中调用了link方法,link方法中判断了数据库操作类是mysql还是mysqli,从而引入相对应数据库操作类。
DATABASE这个常量是在./OW/Lib/Conf/site.conf.php文件中。
Mysql_DB.class.php 分析:
database这个键的值为mysql:
接下来返回到indexModel.class.php中:
调用了name函数,我们进入./OW/Lib/Core/Mysql_DB.class.php的name方法:
可以看到这个方法是用来定义表名的,随后类本身。
调用了where方法,我们进入where方法看看:
1. 进入该方法后首先用func_get_args()函数获取传递到当前函数的参数并且赋值给变量$args;
2. 判断是否设置了$args[1];
3. 若设置了,进入这个判断体中,删除掉$args[0];
4. 定义一个名为$arg的空数组,用foreach将数组$args中的键和值取出来,循环体中将$args[{$key}]赋值到$arg数组的一个元素中;
5. 跳出循环,以将数组$arg中的每一个元素连接成一个字符串,随后使用一个assert函数将格式化的字符串赋值给$where变量;
6. 将传入的参数拼接到SQL语句中,并将SQL语句赋值给成员属性sql,若$where是数组的时候则进入该判断体中;
7. 进入判断体后定义$column、$in、$value并赋值为空,接着使用foreach循环将数组where中的键和值取出来,进入循环体中;
判断键是否为字符串型数据,若是,那么将$col赋值给$column;
判断$val是否等于IN或者NOT IN,若等于这两个之一那么将$val赋值给$in;
若不满足判断条件,那么将$in赋值为IN;
判断$val是否为数组,如果是数组那么就将$val中的元素连接成字符串,接着就跳出循环,将获取到的语句拼接到定义好的SQL语句中并赋值给成员属性sql;
8. 若不满足上述的条件就直接将$where拼接到定义好的SQL语句中并赋值给成员属性sql,最后返回当前类。
看到这里,我们得知这个方法中几乎没有过滤,就只有那个格式化字符串带些过滤的意思,所以我们返回indexmodel中的index方法:
最后这里调用了select方法:
首先判断是否设置了成员属性sql;
若没有设置,那么我们直接将系统定义的sql语句赋值给成员属性sql,再返回类中的方法fetch。
我们看看这个fetch方法:
进入这个函数后首先调用类中的方法Excepton;从字面意思来看,这是一个异常处理的函数,我们进入这个函数:
进入函数后首先调用mysqli中的query函数查询sql语句,将返回值赋值给$result;
接下来判断$result为空的时候抛出异常;
将执行的SQL语句以及mysql执行的错误信息和错误行数传入到这个异常处理类中;
最后返回$result。
我们回到fetch方法中:
将成员属性sql赋值给成员属性SQL,然后删除成员属性中的sql,order,limit变量;
使用mysqli中的fetch_all方法将查询到的所有结果返回。
分析到这里可以明确的看到indexmoel中的index方法存在SQL注入,从外到内过滤的东西只有一点,最主要的是这个index控制器中的index方法获取值的时候使用的过滤方式不正确。
注入测试:
先看这个注入:
访问:
http://localhost/?s=index/index/index/id/3’
可以从它返回的信息看出来这是一个数字型的SQL注入。
注入原因是因为未按照正确的获取方式来接收外部数据导致的。
我们将/c改为/d:
这样就可以将这个SQL注入给过滤掉了。
接着我们看看字符型的sql注入:
如果是字符型的注入的情况下,并且程序自身有将危险字符转义的时候,必须满足先遣条件数据库编码为GBK,这样就存在一个款字节注入,我们将数据库编码改改。
我们来看看我们的字符型的注入:
可以很明显的看到这里存在字符型并且是宽字节的注入。
访问:
http://localhost/?s=index/index/String/title/a%df%27
现在在代码审计中很难在框架中看到宽字节注入,要修复的话直接用/s就好。
可以看到这个宽字节已经被我们给修复了,接着来看看验证登陆函数中的注入:
现在简单的分析一下这个函数的作用:
首先进入函数中将外部获取到的logintoken并将base64字符串解码并且以制表符分割为数组并且用$uid、$password分别接收这个这个数组中的两个元素;
然后将$uid传入到get_user这个模型函数中,可见这里对加密字符串很友好,几乎没有过滤。
那么get_user方法是否对$uid进行了过滤?
可见都很友善,没有过滤。
我们构造下
pyload:Cookie:logintoken=MScJNjU0ZHM2NWE=
这里验证登陆并非一定是一个SQL注入,当程序没有返回值没有错误信息返回的时候,不可能测试出SQL注入。
所以我们大概估计一个用户id,将返回的users中的password和我们传入的password进行对比,所以这里还可以进行用户密码的爆破,还是无限制爆破 。
从这个案例我们得知不能对即将解密的加密字符串太过友善,否则反受其害。
我们接着来:
这里由于获取的参数id会传入url解码函数中,所以这也造成了一个注入,只需要对某一个字符或字符串进行双重url编码即可。
访问:
http://localhost/?s=index/index/articles/id/3%2527
这里的%25%27是双重编码,浏览器请求的时候会将%27编码后的结果解码为%27,然后传入程序中,程序再将这%27解析为单引号,这样会造成了一些意料之外的漏洞。
预编译注入:
我们再来看看预编译中的漏洞,为各位解答一下疑问。
预编译这东西存在的SQL注入是由于开发者对于sql语句嵌入了用户所输入的值,预编译这东西只能将用户输入的传到数据中
比如id=?
那么这个问号就是应该嵌入用户所输入的值。
比如:select * from $table where id=?
这条语句出现了一个致命的弱点——造成了SQL注入,那么这个$table就会将它当作语句来处理,这是个致命的关键。
我们一起来看看:
这里从外部分别获取了key和id,分别将这两个元素传入$where数组中,并传入模型data函数中。
我们看看data函数:
这里将传入的值预编译了。
使用get_last_sql()函数查看最后执行的SQL语句:
注入测试:
页面中的SQL语句:
操作一下这个id看看有没有反应:
访问:
http://localhost/?s=index/index/data&id=3%27
当然在获取值的时候就已经将它定义好了,我们把它改成/c。
再次访问:
还是无法操作这个id,我们来看看这个key:
访问:
http://localhost/?s=index/index/data&id=3%27&key=a
可以看到用户所输入的值不应该用来嵌入到语句中,而是由开发者来定义这些字段等等。并且每一个注入点所适用的注入类型,比如这些注入都有返回值和错误信息。所以可以用两种最简单的形式来注入:报错注入、联合查询注入。
报错注入:
(这里我们使用updatexml函数进行报错注入)
访问:http://localhost/?s=index/index/data&id=3&key=id=3%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))%23
http://localhost/?s=index/index/data&id=3&key=id=3%20and%20(updatexml(1,concat(0x7e,(select%20table_name%20from%20information_schema.tables%20where%20table_schema=database()%20limit%200,1),0x7e),1))%23
这样依次改变limit中的值就好了。
再来看看联合查询注入:
http://localhost/?s=index/index/data&id=3&key=id=-3%20union%20select%201,group_concat(table_name),3,4,5,6,7,8,9,10,11%20from%20information_schema.tables%20where%20table_schema=database()%23
好了,本篇文章就到此结束了,SQL注入是个奇妙的东西,出现的类型也不止上述中的注入,还有二次注入可参考:http://www.only-wait.cn/?p=128
更多精彩
*IDEA值得分享 | 转载注明出处
以上是关于实战 | 代码审计中的SQL注入和预编译中的SQL注入的主要内容,如果未能解决你的问题,请参考以下文章