安全矩阵

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

jsp 文件上传流量层面 waf 绕过新姿势

[复制链接]

112

主题

113

帖子

452

积分

中级会员

Rank: 3Rank: 3

积分
452
发表于 2022-6-25 15:17:08 | 显示全部楼层 |阅读模式
本帖最后由 wangqiang 于 2022-6-25 16:31 编辑

jsp 文件上传流量层面 waf 绕过新姿势
利刃信安
2022-06-25 00:08 发表于台湾
转载地址:jsp 文件上传流量层面 waf 绕过新姿势


文章转载
编辑

tomcat
灵活的parseQuotedToken
看看这个解析value的函数,它有两个终止条件,一个是走到最后一个字符,另一个是遇到;
如果我们能灵活控制终止条件,那么waf引擎在此基础上还能不能继续准确识别呢?
  1. private String parseQuotedToken(final char[] terminators) {
  2.   char ch;
  3.   i1 = pos;
  4.   i2 = pos;
  5.   boolean quoted = false;
  6.   boolean charEscaped = false;
  7.   while (hasChar()) {
  8.     ch = chars[pos];
  9.     if (!quoted && isOneOf(ch, terminators)) {
  10.       break;
  11.     }
  12.     if (!charEscaped && ch == '"') {
  13.       quoted = !quoted;
  14.     }
  15.     charEscaped = (!charEscaped && ch == '\\');
  16.     i2++;
  17.     pos++;

  18.   }
  19.   return getToken(true);
  20. }
复制代码

如果你理解了上面的代码你就能构造出下面的例子

同时我们知道jsp如果带"符号也是可以访问到的,因此我们还可以构造出这样的例子

还能更复杂点么,当然可以的结合这里的\,以及org.apache.tomcat.util.http.parser.HttpParser#unquote中对出现\后参数的转化操作,这时候
如果waf检测引擎当中是以最近""作为一对闭合的匹配,那么waf检测引擎可能会认为这里上传的文件名是y4tacker.txt\,从而放行

变形之双写filename*与filename
首先tomcat的org.apache.catalina.core.ApplicationPart#getSubmittedFileName的场景下,文件上传解析header的过程当中,存在while循环会不断往后读取,
最终会将key/value以Haspmap的形式保存,那么如果我们写多个那么就会对其覆盖,在这个场景下绕过waf引擎没有设计完善在同时出现两个filename的时候
到底取第一个还是第二个还是都处理,这些差异性也可能导致出现一些新的场景

同时这里下面一方面会删除最后一个*

另一方面如果lowerCaseNames为true,那么参数名还会转为小写,恰好这里确实设置了这一点

因此综合起来可以写出这样的payload,当然结合上篇还可以变得更多变这里不再讨论

变形之编码误用

假设这样一个场景,waf同时支持多个语言,也升级到了新版本会解析filename*,假设go当中有个编码叫y4,而java当中没有,waf为了效率将两个混合处理,这样会导致什么问题呢?

如果没有,这里报错后会保持原来的值,因此我认为这也可以作为一种绕过思路?
  1. try {
  2.   paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue)
  3.     : MimeUtility.decodeText(paramValue);
  4. } catch (final UnsupportedEncodingException e) {
  5.   // let's keep the original value in this case
  6. }
复制代码

Spring4猜猜我在第几层
说个前提这里只针对单文件上传的情况,虽然这里的代码逻辑一眼看出不能有上面那种存在双写的问题,但是这里又有个更有趣的现象

我们来看看这个extractFilename函数里面到底有啥骚操作吧,这里靠函数indexOf去定位key(filename=/filename*=)再做截取操作
  1. private String extractFilename(String contentDisposition, String key) {
  2.     if (contentDisposition == null) {
  3.         return null;
  4.     } else {
  5.         int startIndex = contentDisposition.indexOf(key);
  6.         if (startIndex == -1) {
  7.             return null;
  8.         } else {
  9.             String filename = contentDisposition.substring(startIndex + key.length());
  10.             int endIndex;
  11.             if (filename.startsWith(""")) {
  12.                 endIndex = filename.indexOf(""", 1);
  13.                 if (endIndex != -1) {
  14.                     return filename.substring(1, endIndex);
  15.                 }
  16.             } else {
  17.                 endIndex = filename.indexOf(";");
  18.                 if (endIndex != -1) {
  19.                     return filename.substring(0, endIndex);
  20.                 }
  21.             }

  22.             return filename;
  23.         }
  24.     }
  25. }
复制代码

这时候你的反应应该会和我一样,套中套之waf你猜猜我是谁

当然我们也可以不要双引号,让waf哭去吧

Spring5
同样是springboot2.6.4+springframework5.3,这里不去研究小版本间是否有差异只看看大版本了
“双写”绕过
来看看核心部分
  1. public static ContentDisposition parse(String contentDisposition) {
  2.     List<String> parts = tokenize(contentDisposition);
  3.     String type = (String)parts.get(0);
  4.     String name = null;
  5.     String filename = null;
  6.     Charset charset = null;
  7.     Long size = null;
  8.     ZonedDateTime creationDate = null;
  9.     ZonedDateTime modificationDate = null;
  10.     ZonedDateTime readDate = null;

  11.     for(int i = 1; i < parts.size(); ++i) {
  12.         String part = (String)parts.get(i);
  13.         int eqIndex = part.indexOf(61);
  14.         if (eqIndex == -1) {
  15.             throw new IllegalArgumentException("Invalid content disposition format");
  16.         }

  17.         String attribute = part.substring(0, eqIndex);
  18.         String value = part.startsWith(""", eqIndex + 1) && part.endsWith(""") ? part.substring(eqIndex + 2, part.length() - 1) : part.substring(eqIndex + 1);
  19.         if (attribute.equals("name")) {
  20.             name = value;
  21.         } else if (!attribute.equals("filename*")) {
  22.             //限制了如果为null才能赋值
  23.             if (attribute.equals("filename") && filename == null) {
  24.                 if (value.startsWith("=?")) {
  25.                     Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value);
  26.                     if (matcher.find()) {
  27.                         String match1 = matcher.group(1);
  28.                         String match2 = matcher.group(2);
  29.                         filename = new String(Base64.getDecoder().decode(match2), Charset.forName(match1));
  30.                     } else {
  31.                         filename = value;
  32.                     }
  33.                 } else {
  34.                     filename = value;
  35.                 }
  36.             } else if (attribute.equals("size")) {
  37.                 size = Long.parseLong(value);
  38.             } else if (attribute.equals("creation-date")) {
  39.                 try {
  40.                     creationDate = ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);
  41.                 } catch (DateTimeParseException var20) {
  42.                 }
  43.             } else if (attribute.equals("modification-date")) {
  44.                 try {
  45.                     modificationDate = ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);
  46.                 } catch (DateTimeParseException var19) {
  47.                 }
  48.             } else if (attribute.equals("read-date")) {
  49.                 try {
  50.                     readDate = ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);
  51.                 } catch (DateTimeParseException var18) {
  52.                 }
  53.             }
  54.         } else {
  55.             int idx1 = value.indexOf(39);
  56.             int idx2 = value.indexOf(39, idx1 + 1);
  57.             if (idx1 != -1 && idx2 != -1) {
  58.                 charset = Charset.forName(value.substring(0, idx1).trim());
  59.                 Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset), "Charset should be UTF-8 or ISO-8859-1");
  60.                 filename = decodeFilename(value.substring(idx2 + 1), charset);
  61.             } else {
  62.                 filename = decodeFilename(value, StandardCharsets.US_ASCII);
  63.             }
  64.         }
  65.     }

  66.     return new ContentDisposition(type, name, filename, charset, size, creationDate, modificationDate, readDate);
  67. }
复制代码

spring5当中又和spring4逻辑有区别,导致我们又可以”双写”绕过(至于为什么我要打引号可以看看我代码中的注释),因此如果我们先传filename=xxx再
传filename*=xxx,由于没有前面提到的filename == null的判断,造成可以覆盖filename的值

同样我们全用filename*也可以实现双写绕过,和上面一个道理

但由于这里indexof的条件变成了”=”号,而不像spring4那样的filename=/filename=*,毕竟indexof默认取第一个,造成不能像spring4那样做嵌套操作
文章作者: Y4tacker[1]
文章链接:https://y4tacker.github.io/2022/06/21/year/2022/6/%E6%8E%A2%E5%AF%BBJava%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%B5%81%E9%87%8F%E5%B1%82%E9%9D%A2waf%E7%BB%95%E8%BF%87%E5%A7%BF%E5%8A%BF%E7%B3%BB%E5%88%97%E4%BA%8C/[2]

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0[3] 许可协议。转载请注明来自 Y4tacker's Blog[4]!
引用链接
[1] Y4tacker: mailto:undefined
[2] https://y4tacker.github.io/2022/ ... E5%88%97%E4%BA%8C/:
https://y4tacker.github.io/2022/06/21/year/2022/6/探寻Java文件上传流量层面waf绕过姿势系列二/
[3] CC BY-NC-SA 4.0: https://creativecommons.org/licenses/by-nc-sa/4.0/
[4] Y4tacker's Blog: https://y4tacker.github.io/


回复

使用道具 举报

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

本版积分规则

小黑屋|安全矩阵

GMT+8, 2022-10-1 07:10 , Processed in 0.010711 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

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