长亭雷池waf的六种绕过方法

长亭雷池waf的六种绕过方法

2026年03月10日·23 分钟阅读·347 次阅读·4 点赞·1 条评论

前情提要:本文主要针对post型请求的绕过进行讲解

测试环境准备

首先要准备两台服务器,一台安装雷池waf,另一台搭建web应用,作为雷池的防护对象,这里我使用两台Ubuntu24 Server

雷池安装

官网:雷池 WAF 帮助文档 - 免费安装(推荐)

安装雷池之前,确保已安装docker

安装命令:bash -c "$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/manager.sh)"

随后便可访问雷池web页面

clipboard_1773057150949.png

宝塔安装

官网:宝塔面板下载,免费全能的服务器运维软件

Ubuntu安装命令:wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh && sudo bash install_panel.sh ed8484bec

随后进入宝塔面板,根据指引安装LNMP基础环境

测试环境搭建

在宝塔搭建我们的测试网站,这里使用sqli-labs作为测试网站。

添加站点,随便输一个喜欢的域名,其余默认

clipboard_1773057677028.png

将sqli-labs网站文件上传至网站目录,并修改数据库配置文件

clipboard_1773057757675.png

clipboard_1773057793905.png

接着来到雷池的防护应用,添加应用。

因为我们测试环境没有证书,所以将https删除,并在上游服务器输入我们sqli-labs的服务器ip地址,随后保存

clipboard_1773058020133.png

最后设置Windows本地的hosts文件,让刚才宝塔设置的域名能够正确解析到宝塔ip

以管理员运行PowerShell,执行:notepad C:\Windows\System32\drivers\etc\hosts,添加域名解析,保存退出

clipboard_1773058270263.png

随后浏览器访问sqli-labs域名(有可能会自动变为https访问,改为http),初始创建一下数据库

clipboard_1773058379403.png

随后我们来到第一关测试一个union查询,发现访问被拦截,说明雷池已经生效,环境配置成功

clipboard_1773058546543.png

雷池拦截逻辑

雷池的拦截方式不同于传统 WAF采用的规则匹配方式,而是由智能语义分析算法驱动,雷池能够真正“理解”用户输入的内容,从而更精准地识别攻击行为。

智能语义分析算法由以下四个步骤组成:

第一步:词法分析 雷池对 HTTP 载荷内容进行深度解码处理,将各种编码形式(如 URL 编码、Base64 编码、HTML 实体编码等)的输入还原为原始内容。这一步骤确保了攻击者无法通过简单的编码变形来绕过检测。

第二步:语法分析 系统识别输入内容的编程语言类型(如 SQL、JavaScript、PHP、Python 等),然后匹配相应的语法编译器。这使得雷池能够按照语言的语法规则来解析输入,而不是简单地搜索特征字符串。

第三步:语义分析 在完成语法分析后,系统深度解析输入的语义内容,理解这段代码实际上在做什么、是正常的业务逻辑还是恶意的攻击行为。

第四步:威胁模型匹配 最后,系统将分析结果与威胁模型进行匹配,获得威胁评级。根据评级结果,WAF 决定是阻断还是允许该访问请求。

http请求头Content-Type详解

什么是 Content-Type

在 HTTP 请求中,Content-Type 是一个非常重要的请求头,它告诉服务器发送的数据是什么格式的。就像我们在写信时需要注明信封上的格式一样,HTTP 请求也需要告诉服务器“信封”里装的是什么类型的数据。常见的格式有两种:multipart/form-data 和 application/x-www-form-urlencoded,它们分别适用于不同的场景。

application/x-www-form-urlencoded

这是最传统的表单数据提交方式,也是 HTML 表单的默认编码类型。当你使用 GET 方法提交表单时,数据会被编码成键值对的形式,追加在 URL 后面(Query String)。如果是 POST 方法,数据则会被放在请求体中,但编码方式保持不变。

这种编码方式的规则很简单:所有的键值对都用 & 符号连接,而每个键值对内部,键和值之间用 = 符号连接。同时,一些特殊字符会被进行 URL 编码(Percent-Encoding),比如空格会被替换成 %20,中文会被转换成百分号编码的格式。

这种方式的优点是简单直观,适合传输简单的文本数据。但它的缺点也很明显:不适合传输二进制数据(比如文件上传),因为二进制数据很难用这种文本编码方式表示。

multipart/form-data

当你需要上传文件,或者需要发送既有文本又有文件的混合数据时,multipart/form-data 就是更好的选择。这种格式可以将不同类型的数据组合在一起,每个部分都有自己独立的 Content-Type 和 Content-Disposition 头信息。

multipart/form-data 的工作原理是使用特定的边界(boundary)来分隔不同的部分。每个部分都以边界开始,以边界结束,中间包含该部分的元数据(字段名、文件名等)和实际的数据内容。例如,一个包含用户名和头像文件的上传请求,请求体可能看起来像这样:

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
 
张三
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
 
[二进制图片数据]
------WebKitFormBoundary7MA4YWxkTrZu0gW--

这种格式的优势在于:它可以同时传输文本和二进制数据,而且每个部分都可以独立指定自己的内容类型(Content-Type),比如图片可以是 image/png,文档可以是 application/pdf。这使得它成为文件上传的首选格式。

然而,multipart/form-data 格式不仅可以用来上传文件,还可以用来传递常规的 POST 参数

filename参数的有无

在正常的文件上传数据包中,在请求体的boundary包裹内容中,通常有filename这一参数,例如:

 
------TestBoundary
Content-Disposition: form-data; name="upload_file";filename="test.txt"
 
file content here
------TestBoundary--

当该参数默认存在时,php会将请求视为文件上传,$_FILES变量会被赋值,$_POST变量则为空。

但是当我们手动删除filename参数时(准确地来说是filename= 这9个字符缺一不可,替换任意一个字符,或在其中添加一个字符都不可),php则会将请求视为post,原本的文件内容则会被传入$_POST作为post传递的参数,$_FILE参数则为空

下面是Minimax给出的php脚本,用来更好地验证上述现象

<?php
/**
 * PHP multipart/form-data 解析测试
 * 测试filename参数对PHP解析的影响
 * 
 * 使用方法:
 * 1. 通过BurpSuite或curl发送测试请求
 * 2. 或直接访问页面查看说明
 */
 
header('Content-Type: text/html; charset=UTF-8');
 
// 判断请求方法
$method = $_SERVER['REQUEST_METHOD'];
 
if ($method === 'GET') {
    showTestGuide();
} else {
    handlePostRequest();
}
 
/**
 * 显示测试指南
 */
function showTestGuide() {
    $phpVersion = PHP_VERSION;
  
    echo "<!DOCTYPE html>\n";
    echo "<html>\n<head>\n";
    echo "<meta charset='UTF-8'>\n";
    echo "<title>PHP Multipart 解析测试</title>\n";
    echo "<style>\n";
    echo "body { font-family: Consolas, monospace; padding: 20px; background: #1e1e1e; color: #d4d4d4; }\n";
    echo "h1 { color: #4ec9b0; }\n";
    echo "h2 { color: #ce9178; margin-top: 30px; }\n";
    echo "pre { background: #252526; padding: 15px; border-radius: 5px; overflow-x: auto; }\n";
    echo ".test-case { margin: 20px 0; padding: 15px; border-left: 3px solid #4ec9b0; }\n";
    echo ".warning { color: #dcdcaa; }\n";
    echo ".success { color: #4ec9b0; }\n";
    echo ".error { color: #f14c4c; }\n";
    echo "code { background: #264f78; padding: 2px 5px; border-radius: 3px; }\n";
    echo "</style>\n";
    echo "</head>\n<body>\n";
  
    echo "<h1>PHP Multipart/form-data 解析测试</h1>\n";
    echo "<p>PHP Version: <strong>$phpVersion</strong></p>\n";
  
    echo "<h2>测试用例</h2>\n";
  
    echo "<div class='test-case'>\n";
    echo "<h3>测试1: 有filename属性</h3>\n";
    echo "<pre>POST /test.php HTTP/1.1\n";
    echo "Host: localhost\n";
    echo "Content-Type: multipart/form-data; boundary=----TestBoundary\n\n";
    echo "------TestBoundary\n";
    echo 'Content-Disposition: form-data; name="upload_file"; filename="test.txt"' . "\n\n";
    echo "file content here\n";
    echo "------TestBoundary--</pre>\n";
    echo "<p class='warning'>预期结果:数据进入 \$_FILES 数组</p>\n";
    echo "</div>\n";
  
    echo "<div class='test-case'>\n";
    echo "<h3>测试2: 无filename属性</h3>\n";
    echo "<pre>POST /test.php HTTP/1.1\n";
    echo "Host: localhost\n";
    echo "Content-Type: multipart/form-data; boundary=----TestBoundary\n\n";
    echo "------TestBoundary\n";
    echo 'Content-Disposition: form-data; name="param_name"' . "\n\n";
    echo "param value\n";
    echo "------TestBoundary--</pre>\n";
    echo "<p class='success'>预期结果:数据进入 \$_POST 数组</p>\n";
    echo "</div>\n";
  
    echo "<div class='test-case'>\n";
    echo "<h3>测试3: 空filename (filename=\"\")</h3>\n";
    echo "<pre>POST /test.php HTTP/1.1\n";
    echo "Host: localhost\n";
    echo "Content-Type: multipart/form-data; boundary=----TestBoundary\n\n";
    echo "------TestBoundary\n";
    echo 'Content-Disposition: form-data; name="empty_file"; filename=""' . "\n\n";
    echo "some content\n";
    echo "------TestBoundary--</pre>\n";
    echo "<p class='warning'>预期结果:数据进入 \$_FILES 数组,但 error=4 (UPLOAD_ERR_NO_FILE)</p>\n";
    echo "</div>\n";
  
    echo "<div class='test-case'>\n";
    echo "<h3>测试4: 有filename但无Content-Type</h3>\n";
    echo "<pre>POST /test.php HTTP/1.1\n";
    echo "Host: localhost\n";
    echo "Content-Type: multipart/form-data; boundary=----TestBoundary\n\n";
    echo "------TestBoundary\n";
    echo 'Content-Disposition: form-data; name="file"; filename="test.txt"' . "\n\n";
    echo "content\n";
    echo "------TestBoundary--</pre>\n";
    echo "<p class='success'>结果:仍然进入 \$_FILES 数组(filename是唯一判断标准)</p>\n";
    echo "</div>\n";
  
    echo "<h2>快速测试</h2>\n";
    echo "<p>点击以下链接进行快速测试:</p>\n";
    echo "<ul>\n";
    echo "<li><a href='?test=1' style='color: #4ec9b0;'>测试1: 有filename</a></li>\n";
    echo "<li><a href='?test=2' style='color: #4ec9b0;'>测试2: 无filename</a></li>\n";
    echo "<li><a href='?test=3' style='color: #4ec9b0;'>测试3: 空filename</a></li>\n";
    echo "</ul>\n";
  
    // 处理快速测试链接
    if (isset($_GET['test'])) {
        echo "<h2>测试结果</h2>\n";
        $testNum = intval($_GET['test']);
  
        if ($testNum === 1) {
            // 模拟有filename的请求
            simulateMultipartRequest('test_field', 'test.txt', 'test content');
        } elseif ($testNum === 2) {
            // 模拟无filename的请求
            simulateMultipartRequest('test_field', null, 'test content');
        } elseif ($testNum === 3) {
            // 模拟空filename
            simulateMultipartRequest('test_field', '', 'test content');
        }
    }
  
    echo "</body>\n</html>\n";
}
 
/**
 * 模拟multipart请求并显示解析结果
 */
function simulateMultipartRequest($fieldName, $filename, $content) {
    // 实际上这里无法真正模拟,因为PHP已经在请求时解析了
    // 我们只能通过显示预期行为来说明
  
    echo "<div class='test-case'>\n";
  
    if ($filename !== null && $filename !== '') {
        // 有filename
        echo "<p class='warning'>模拟请求:Content-Disposition: form-data; name=\"$fieldName\"; filename=\"$filename\"</p>\n";
        echo "<p><strong>预期行为:</strong></p>\n";
        echo "<pre>\$_POST = empty\n";
        echo "\$_FILES = array('$fieldName' => array(\n";
        echo "    'name' => '$filename',\n";
        echo "    'type' => 'application/octet-stream',\n";
        echo "    'tmp_name' => '/tmp/phpXXXXXX',\n";
        echo "    'error' => 0,\n";
        echo "    'size' => " . strlen($content) . "\n";
        echo "))</pre>\n";
    } elseif ($filename === '') {
        // 空filename
        echo "<p class='warning'>模拟请求:Content-Disposition: form-data; name=\"$fieldName\"; filename=\"\"</p>\n";
        echo "<p><strong>预期行为:</strong></p>\n";
        echo "<pre>\$_POST = empty\n";
        echo "\$_FILES = array('$fieldName' => array(\n";
        echo "    'name' => '',\n";
        echo "    'type' => '',\n";
        echo "    'tmp_name' => '',\n";
        echo "    'error' => 4,  // UPLOAD_ERR_NO_FILE\n";
        echo "    'size' => 0\n";
        echo "))</pre>\n";
    } else {
        // 无filename
        echo "<p class='success'>模拟请求:Content-Disposition: form-data; name=\"$fieldName\"</p>\n";
        echo "<p><strong>预期行为:</strong></p>\n";
        echo "<pre>\$_POST = array('$fieldName' => '$content')\n";
        echo "\$_FILES = empty</pre>\n";
    }
  
    echo "</div>\n";
}
 
/**
 * 处理POST请求并显示解析结果
 */
function handlePostRequest() {
    $contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '';
  
    echo "<!DOCTYPE html>\n";
    echo "<html>\n<head>\n";
    echo "<meta charset='UTF-8'>\n";
    echo "<title>解析结果</title>\n";
    echo "<style>\n";
    echo "body { font-family: Consolas, monospace; padding: 20px; background: #1e1e1e; color: #d4d4d4; }\n";
    echo "h1 { color: #4ec9b0; }\n";
    echo "h2 { color: #ce9178; }\n";
    echo "pre { background: #252526; padding: 15px; border-radius: 5px; overflow-x: auto; }\n";
    echo ".result { margin: 20px 0; }\n";
    echo ".post { color: #9cdcfe; }\n";
    echo ".files { color: #c586c0; }\n";
    echo ".input { color: #ce9178; }\n";
    echo ".server { color: #6a9955; }\n";
    echo ".warning { color: #dcdcaa; }\n";
    echo ".success { color: #4ec9b0; }\n";
    echo ".highlight { background: #264f78; padding: 2px 5px; border-radius: 3px; }\n";
    echo ".conclusion { margin-top: 30px; padding: 20px; border-radius: 5px; }\n";
    echo ".conclusion.post { background: #1e3a5f; border-left: 4px solid #9cdcfe; }\n";
    echo ".conclusion.files { background: #3d1e5f; border-left: 4px solid #c586c0; }\n";
    echo "</style>\n";
    echo "</head>\n<body>\n";
  
    echo "<h1>PHP Multipart 解析结果</h1>\n";
  
    // 请求信息
    echo "<div class='result'>\n";
    echo "<h2>请求信息</h2>\n";
    echo "<pre class='server'>";
    echo "Content-Type: " . htmlspecialchars($contentType) . "\n";
    echo "Request Method: " . $_SERVER['REQUEST_METHOD'] . "\n";
    echo "Content-Length: " . (isset($_SERVER['CONTENT_LENGTH']) ? $_SERVER['CONTENT_LENGTH'] : 0) . " bytes\n";
    echo "</pre>\n";
    echo "</div>\n";
  
    // php://input
    $phpInput = file_get_contents('php://input');
    echo "<div class='result'>\n";
    echo "<h2>php://input 原始数据</h2>\n";
    echo "<pre class='input'>";
    if (empty($phpInput)) {
        echo "<span class='warning'>(empty - multipart/form-data时php://input不可用)</span>\n";
    } else {
        echo htmlspecialchars($phpInput) . "\n";
    }
    echo "</pre>\n";
    echo "</div>\n";
  
    // $_POST
    $post = $_POST;
    echo "<div class='result'>\n";
    echo "<h2>\$_POST 变量</h2>\n";
    echo "<pre class='post'>";
    if (empty($post)) {
        echo "<span class='warning'>(empty)</span>\n";
    } else {
        var_export($post);
        echo "\n";
    }
    echo "</pre>\n";
    echo "</div>\n";
  
    // $_FILES
    $files = $_FILES;
    echo "<div class='result'>\n";
    echo "<h2>\$_FILES 变量</h2>\n";
    echo "<pre class='files'>";
    if (empty($files)) {
        echo "<span class='warning'>(empty)</span>\n";
    } else {
        var_export($files);
        echo "\n";
    }
    echo "</pre>\n";
    echo "</div>\n";
  
    // 结论
    echo "<div class='conclusion ";
    if (!empty($post)) {
        echo "post\">\n";
        echo "<h2 class='success'>结论:数据被解析为 POST 参数</h2>\n";
        echo "<p>原因:请求中 <strong>没有 filename 属性</strong></p>\n";
        echo "<p>PHP 判断文件上传的唯一标准是:Content-Disposition 中是否包含 <code>filename=</code></p>\n";
    } elseif (!empty($files)) {
        echo "files\">\n";
        echo "<h2 class='warning'>结论:数据被解析为文件上传</h2>\n";
  
        // 检查是否有错误
        foreach ($files as $name => $info) {
            if ($info['error'] === 4) {
                echo "<p>注意:filename 为空字符串,error code = 4 (UPLOAD_ERR_NO_FILE)</p>\n";
            }
        }
  
        echo "<p>原因:请求中 <strong>包含 filename 属性</strong></p>\n";
    } else {
        echo "'>\n";
        echo "<h2>结论:无数据</h2>\n";
    }
    echo "</div>\n";
  
    // 技术说明
    echo "<div class='conclusion' style='background: #252526; border-left: 4px solid #4ec9b0;'>\n";
    echo "<h2>技术说明</h2>\n";
    echo "<pre style='background: transparent;'>PHP 解析 multipart/form-data 的规则:
 
1. <strong>filename 属性是唯一判断标准</strong>
   - 有 filename=xxx   → 进入 \$_FILES
   - 无 filename       → 进入 \$_POST  
   - filename=\"\"     → 进入 \$_FILES,但 error=4
 
2. Content-Type 不影响解析结果
   - 即使设置为 image/png,没有 filename 仍是 POST
 
3. php://input 的限制
   - multipart/form-data 时,php://input 通常为空
 
4. 安全提示
   - WAF 通常对文件上传内容放行
   - 攻击者可以利用无 filename 的字段
   - 将恶意载荷伪装成普通 POST 参数</pre>\n";
    echo "</div>\n";
  
    echo "</body>\n</html>\n";
}
 

我们通过浏览器访问这个php页面,通过BP进行测试

存在fliename时,可以看到$_FILE变量被正常赋值,$_POST为空

clipboard_1773068610879.png

当我们删除filename,再次测试,会发现php把我们的请求解析为post,并非文件上传

clipboard_1773068731608.png

那么这一特性又能怎样呢?

核心绕过思路

在 Nginx + PHP 架构中,Nginx 作为 Web 服务器本身并不负责解析 multipart/form-data 格式的请求体,这部分工作完全交给后端的 PHP 来完成。这意味着一个关键的不一致性:WAF(通常部署在 Nginx 层面)获取并解析到的请求内容,可能与后端 PHP 最终解析出的结果存在差异。这种差异化正是绕过 WAF 的核心基础。

所以我们可以想办法让 WAF 以为我们是在上传文件,而实际上却是在 POST 一个参数,这个参数可以是命令注入、SQL 注入、SSRF 等任意的一种攻击,这样就实现了通用WAF绕过。

思路1:双写上传描述行

来到sqli-labs的11关,为post型注入。如图进行双写,第二行带上filename参数。

clipboard_1773124962551.png

该请求到达雷池waf时,先经过前端Tengine(类似nginx)完成 HTTP 协议初步解析以后, 再转发给 WAF 处理引擎。也就意味着,我们构造的双写数据包,在雷池看来,是一个带有filename参数的文件上传包,但是后端php只看第一行没有filename参数,那么php就认为该数据包是post传参,并非文件上传,于是将“文件内容”当做post传递的参数进行处理了,这里我们post的是union联合查询表名,可以看到成功出数据了

雷池以为我们在上传文件,实则我们在post传参

思路二:双写整个boundary部分

如图双写整个boundary部分,并使用union查询,也可以出数据。雷池waf取的是第二个,视作文件上传,而php只取第一个,视作post提交

clipboard_1773127339104.png

值得一提的是,做到这里,我本能的使用updatexml进行报错注入,因为这一关本身是支持报错注入的。但当我将union查询换为updatexml报错注入时,没有任何回显,雷池waf也没有拦截

clipboard_1773127601193.png

事实上,我们确实绕过了waf,updatexml语句确实被后端数据库执行了,只是报错信息在传回前端页面时,雷池 WAF 对响应体(Response Body)也做了检测和过滤。

  • 为什么union可以出数据?

    联合查询(UNION SELECT)走的是正常查询结果的回显路径。查看这一关的PHP源码,当 $row 有结果时,数据通过 $row['username']$row['password'] 输出。这是正常的业务数据输出流程,WAF 不太容易对这类正常字段内容做严格拦截,否则会产生大量误报。

  • 为什么报错注入无回显?

    报错注入(updatexml)走的是错误信息回显路径。当 SQL 语句执行出错时,mysqli_error($con1) 会把 MySQL 的错误信息原样输出到响应体中。这个错误信息里会包含类似 XPATH syntax error: '~root@localhost~' 这样的内容。

    雷池作为反向代理,不仅检测请求(Request),也会检测响应(Response)。当响应体中出现明显的 SQL 错误信息特征(比如 XPATH syntax errorupdatexml 相关的报错格式、或者数据库敏感信息泄露的模式),雷池很可能会直接拦截该响应,返回一个自定义的错误页面或空响应,或者过滤/替换掉响应体中的敏感错误信息,也就是我们看到的无回显现象。

思路三:破坏filename=结构

这个思路其实延续了上面的双写思路,只不过是让php无法识别第一行带有filename参数的,php自然就识别第二行,让waf成功识别第一行

我们在filename:的冒号前面加上\t制表符,或者空格,都可以成功绕过

clipboard_1773144276405.png

clipboard_1773144349854.png

思路四:破坏Content-Disposition:结构

除了filename可以破坏,我们在Content-Disposition:的冒号前面也可以使用0x00截断,从而让php无法解析

clipboard_1773143865903.png

思路五:破坏参数名

如图参数位置使用00截断,\t不行

clipboard_1773144574483.png

思路六:伪装成urlencoded的form-data

这个思路我认为非常巧妙。如图构造数据包

clipboard_1773147285465.png

Content-Type的值在php看来是application/x-www-form-urlencoded,但在waf看来则是multipart/form-data

  • 以php视角来看,我们手动传递了五个参数,其中就有uname passwd submit这三个后端接收的,以及收尾的垃圾字符,他们五个通过&连接,uname就是我们注入的恶意sqi。
  • 以雷池waf的视角来看,这是一个带有filename的文件上传包,有boundary Content-Disposition Content-Type等等内容,遵循multipart/form-data标准格式,我们传递的恶意sql只是合法上传的“文件内容”而已

总结

其实绕过waf的核心思路就是寻找差异化,waf所在的沙箱环境与真实的生产环境是略有差异的,找到这些差异,并设法利用,就能绕过!

clipboard_1773147849141.png

参考文档:

Microsoft Word - 腾讯 WAF 挑战赛回忆录.docx

标签:#waf绕过
©

版权声明:本文采用 CC BY-NC-SA 4.0 协议授权,转载请注明出处并保留原始链接。

原文链接:https://www.jerrygao.cn//blog/E585ADE7A78DE696B9E6B395E7BB95E8BF87E995BFE4BAADE99BB7E6B1A0E5858DE8B4B9waf

评论 1

F
flc四川-成都WindowsEdge大约 2 个月前

我是首评

JerryGao
JerryGao博主广东-广州市WindowsChrome大约 2 个月前

哈哈你来啦

F
flc四川-成都WindowsEdge大约 2 个月前

必须支持