安全矩阵

 找回密码
 立即注册
搜索
查看: 621|回复: 0

实战 | Post文件上传WAF Bypass总结

[复制链接]

180

主题

231

帖子

1180

积分

金牌会员

Rank: 6Rank: 6

积分
1180
发表于 2022-12-19 15:50:46 | 显示全部楼层 |阅读模式

实战 | Post文件上传WAF Bypass总结

本文介绍的思路主要围绕针对于 POST 参数的 multipart/form-data 进行讨论。
multipart/form-data 是为了解决上传文件场景下文件内容较大且内置字符不可控的问题。在最初的 http 协议中,并没有上传文件方面的功能。RFC1867 为 HTTP 协议添加了这个能力。常见的浏览器都已经支持。按照此规范将用户指定的文件发送到服务器,可以按照此规范解析出用户发送来的文件。
HTTP 传输的内容通过 boundary 进行了分割,以 --boundary 开始,并以 --boundary-- 结尾。
multipart/form-data 格式也是可以传递 POST 参数的。对于 Nginx + PHP 的架构,Nginx 实际上是不负责解析 multipart/form-data 的 body 部分的,而是交由 PHP 来解析,因此 WAF 所获取的内容就很有可能与后端的 PHP 发生不一致。
通过一个简单的脚本来验证上面的说法。
  1. <?php
  2. echo file_get_contents("php://input");
  3. echo '$_POST Content\n';
  4. echo '';
  5. var_dump($_POST);
  6. echo '$_FILES Content\n';
  7. echo '';
  8. var_dump($_FILES);
  9. ?>
复制代码

正常情况下使用 multipart/form-data POST 传输一个参数 f,其值为 1:
上面说到,multipart/form-data 用来解决传输文件的问题,那什么情况是上传文件?什么情况是 POST 参数呢?关键点在于有没有一个完整的 filename=,这 9 个字符缺一不可。加上了 filename= 以后的回显:
由于一些 WAF 产品对用户上传文件的内容不做匹配,直接放行。因此,关键问题在于,WAF 能否准确有效识别出哪些内容是传给 POST 数组的,哪些传给 FILES 数组?如果不能,那就可以想办法让 WAF 以为我们是在上传文件,而实际上却是在 POST 一个参数,这个参数可以是命令注入、SQL 注入、SSRF 等任意的一种攻击,这样就实现了通用型的Waf Bypass。
Bypass 思路 - 初级0x00 截断
在 filename 之前加入了 0x00 (%00 url decode),有些 WAF 在检测前会对 HTTP 协议中的 0x00 进行过滤, 这样就导致了 WAF 认为是含有 filename 的普通上传,而后端 PHP 则认为是 POST 参数。
文件描述双写混淆
双写 Content-Disposition,一些 WAF 会取第二行,而实际 PHP 会获取第一行。
另外针对 Content-Disposition 的双写混淆还有可以包括 Content-Type:
但是这种方式会将 f 变量中加入一些垃圾数据,在进行注入时需要进行闭合处理。
multipart 混淆
通过构建一个新的 multipart 部分,是两个部分传递的参数名相同,达到混淆的目的。
带有垃圾数据的情况
不带垃圾数据的情况
Boundary 混淆构造双重 boundary
在 PHP 中,只识别到 boundary=a,即真正的 boundary 为 a。若 WAF 中识别到的 boundary 为 b,就会将第 13 行到第 18 行做为文件 pic.png 的内容进行传输,达到混淆的目的。
构造双重 Content-Type
这种混淆方式与上一种情况类似,只是将 Content-Type 进行混淆,指定不同的 boundary。
空白 boundary
在 PHP 中,只识别到 boundary=空 。若 WAF 中错将 ; 识别到为 boundary,就会将第 13 行到第 18 行做为文件 pic.png 的内容进行传输,达到混淆的目的。
空格 boundary
同样的 boundary 也可以是空格
boundary 中的逗号
事实上,在 PHP 中会将 Boundary 中的逗号作为分隔符,即 boundary 遇到逗号就结束。
只标识一个逗号也可以:
Bypass 思路 - 进阶0x00 截断进阶


这三个位置都可以。将其替换为 0x00 和 0x20 与之同理。
此外,将 0x00 放到参数名中也可以绕过:
Boundary 混淆进阶
boundary 的名称是可以前后加入任意内容的,WAF 如果严格按 boundary 去取,就会出现混淆。
在双写 Content-Type 的混淆中,将第一个 Content-Type 和冒号部分填入了空格,实现绕过。
Boundary 的取值混淆:
单双引号混合
Content-Disposition 中的字段使用单引号、双引号进行混淆
urlencoded 与 multipart 混淆
在 Content-Type 头中,分别指定为:urlencoded 与 multipart。实际上 PHP 识别到的为 urlencoded,若 WAF 识别到的为 multipart,就可以绕过检测。通过 & 来作为参数分隔符,截取参数 sqlInjectionParam 的前后部分,完整保留该参数。
由于 multipart/form-data 下的内容不进行 urldecoded, 一些 WAF 也正是这样设计的,这样做本没有问题,但是如果是 urlencoded 格式的内容,不进行 url 解码就会引入 %0a 这样字符,而这样的字符不解码是可以直接绕过防护规则的,从而导致了绕过。
Bypass 思路 - 高级
此章节通过结合 PHP 源码来讨论 WAF Bypass 的可能性。
skip_upload - 1
在 PHP 源码中,处理 multipart 时存在这样一段代码:
其中的 param 就是 name="f",当程序进入 c < 0 这个分支时,就会跳过当前 part 的上传流程。由于初始化时 c = 0,遇到 [ 时,c += 1,遇到 ] 时,c -= 1。因此,可以构造 name="f]",即可让 c = -1。
skip_upload - 2
在 PHP 源码中,有这样一段代码:
当文件上传数量超出最大值后,会跳过当前 part 文件的处理。在 php 5.2.12 和以上的版本,有一个隐藏的文件上传限制是在 php.ini 里没有的,就是这个 max_file_uploads 的设定,该默认值是 20, 在 php 5.2.17 的版本中该值已不再隐藏。文 件上传限制最大默认设为 20,所以一次上传最大就是 20 个文档,所以超出 20 个就会报错。
  1. POST /waf.php HTTP/1.1
  2. Accept: */*
  3. Host: localhost:8081
  4. Accept-Encoding: gzip, deflate
  5. Connection: close
  6. Content-Type: multipart/form-data; boundary=a
  7. Content-Length: 2030

  8. --a
  9. Content-Disposition: form-data; name="1"; filename="pic.png"
  10. Content-Type: image/png

  11. 1
  12. --a
  13. Content-Disposition: form-data; name="2"; filename="pic.png"
  14. Content-Type: image/png

  15. 2
  16. --a
  17. Content-Disposition: form-data; name="3"; filename="pic.png"
  18. Content-Type: image/png

  19. 3
  20. --a
  21. Content-Disposition: form-data; name="4"; filename="pic.png"
  22. Content-Type: image/png

  23. 3
  24. --a
  25. Content-Disposition: form-data; name="5"; filename="pic.png"
  26. Content-Type: image/png

  27. 3
  28. --a
  29. Content-Disposition: form-data; name="6"; filename="pic.png"
  30. Content-Type: image/png

  31. 3
  32. --a
  33. Content-Disposition: form-data; name="7"; filename="pic.png"
  34. Content-Type: image/png

  35. 3
  36. --a
  37. Content-Disposition: form-data; name="8"; filename="pic.png"
  38. Content-Type: image/png

  39. 3
  40. --a
  41. Content-Disposition: form-data; name="9"; filename="pic.png"
  42. Content-Type: image/png

  43. 3
  44. --a
  45. Content-Disposition: form-data; name="10"; filename="pic.png"
  46. Content-Type: image/png

  47. 3
  48. --a
  49. Content-Disposition: form-data; name="11"; filename="pic.png"
  50. Content-Type: image/png

  51. 3
  52. --a
  53. Content-Disposition: form-data; name="12"; filename="pic.png"
  54. Content-Type: image/png

  55. 3
  56. --a
  57. Content-Disposition: form-data; name="13"; filename="pic.png"
  58. Content-Type: image/png

  59. 3
  60. --a
  61. Content-Disposition: form-data; name="14"; filename="pic.png"
  62. Content-Type: image/png

  63. 3
  64. --a
  65. Content-Disposition: form-data; name="15"; filename="pic.png"
  66. Content-Type: image/png

  67. 3
  68. --a
  69. Content-Disposition: form-data; name="16"; filename="pic.png"
  70. Content-Type: image/png

  71. 3
  72. --a
  73. Content-Disposition: form-data; name="17"; filename="pic.png"
  74. Content-Type: image/png

  75. 3
  76. --a
  77. Content-Disposition: form-data; name="18"; filename="pic.png"
  78. Content-Type: image/png

  79. 3
  80. --a
  81. Content-Disposition: form-data; name="19"; filename="pic.png"
  82. Content-Type: image/png

  83. 3
  84. --a
  85. Content-Disposition: form-data; name="id"; filename="pic.png"
  86. Content-Type: image/png

  87. 3
  88. --a
  89. Content-Disposition: form-data; name="id";

  90. sql injection!!!!
  91. --a--
复制代码


关于文件扩展名的绕过
前文主要是提出一些关于源站与 WAF 在解析 multipart 之间存在的差异导致的绕过。在实际渗透测试中,如何绕过文件扩展名是很重要一个点,所以本节内容主要介绍,在 WAF 解析到 filename 参数的情况下,从协议和后端解析的层面如何绕过文件扩展名。
整体上的思路为:filename="file_name.php",对于 WAF 层面来说,发现扩展名为 php,接着进行拦截,绕过的目标为,使 WAF 解析出的 filename 不出现 php 关键字,并且后端程序在验证扩展名的时候会认为这是一个 php 文件。
从各种程序解析的代码来看,为了让 waf 解析出现问题,干扰的字符除了上文说的引号,空格,转义符,还有 :;,这里还是要分为两种形式的测试。
无引号包裹的形式
  1. Content-Disposition: form-data; name=key3; filename=file_name:.php

  2. Content-Disposition: form-data; name=key3; filename=file_name'.php

  3. Content-Disposition: form-data; name=key3; filename=file_name".php

  4. Content-Disposition: form-data; name=key3; filename=file_name".php

  5. Content-Disposition: form-data; name=key3; filename=file_name .php

  6. Content-Disposition: form-data; name=key3; filename=file_name;.php
复制代码

前五种情况 flask/Java 解析结果都是一致的,会取整体作为 filename 的值,都是含有 php 关键字的,这也说明如果 waf 解析存在差异,将特殊字符直接截断取值,会导致 waf 被绕过。
最后一种情况,flask/Java/php 解析都会直接截断,filename=file_name,这样后端获取不了,无论 waf 解析方式如何,无法绕过。
对于 php 而言,前三种会如 flask 以一样,将整体作为 filename 的值,第五种空格类型,php 会截断,最终取 filename=file_name,这种容易理解,当没出现引号时,出现空格,即认为参数值结束。
然后再测试转义符号的时候,出现了从 \ 开始截断,并去 \ 后面的值最为 filename 的值,这种解析方式和 boundary 解析也不相同,且双引号和单引号相同效果。实际上 php 并没有把 \ 当作转义符号,而是将 filename 参数看当做文件路径,并取出 path 里面文件名的部分。所以这个解析方式和引号跟本没关系,只是 php 在解析 filename 时,会取最后的 \ 或者 / 后面的值作为文件名。
有引号包裹的形式
  1. Content-Disposition: form-data; name=f; filename="file_name:.php"

  2. Content-Disposition: form-data; name=f; filename="file_name'.php"

  3. Content-Disposition: form-data; name=f; filename="file_name".php"

  4. Content-Disposition: form-data; name=f; filename="file_name".php"

  5. Content-Disposition: form-data; name=f; filename="file_name .php"

  6. Content-Disposition: form-data; name=f; filename="file_name;.php"
复制代码

flask 解析结果除第三种 filename 取 file_name 之外,其它都会取双引号内整体的值作为 filename,转义符具有转义作用。php 第三种也会解析出 file_name,但是在第四种转义符是具有转义作用的。使用单引号的情况和上文引号部分分析一致。
对于 Java 来说,除第三种情况外,都是会取引号内整体作为 filename 值,但是第三种情况 Java 会继续取值,那么最后 filename 为 file_name".php。所以对于 Java 这个异常的特性来说,通常 WAF 会像 php/flask 那样在第一次出现闭合双引号时,直接取双引号内内容作为 filename 的取值,这样就可以绕过文件扩展名的检测。
参考

do9gy - 腾讯 WAF 挑战回忆录https://t.zsxq.com/UfAEeY3
donky16 - 从 RFC 看如何通过 multipart 文件上传绕过 WAFhttps://t.zsxq.com/fMBM7qz
来源

作者:geekby原文地址:https://www.geekby.site/2022/03/waf-bypass/如有侵权,请联系删除
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-5-9 03:56 , Processed in 0.014463 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表