探寻Java文件上传流量层面waf绕过姿势系列二

admin 2024年10月29日00:07:17评论8 views字数 4930阅读16分26秒阅读模式

探寻Java文件上传流量层面waf绕过姿势系列二

写在前面

这篇和上篇不同的是上篇更多关注于RFC文档规范的部分,而这篇更关注于如何从代码层面上的利用来绕过,具体内容请接着往下看

正文

tomcat

灵活的parseQuotedToken

继续看看这个解析value的函数,它有两个终止条件,一个是走到最后一个字符,另一个是遇到;

如果我们能灵活控制终止条件,那么waf引擎在此基础上还能不能继续准确识别呢?

123456789101112131415161718192021
private String parseQuotedToken(final char[] terminators) {  char ch;  i1 = pos;  i2 = pos;  boolean quoted = false;  boolean charEscaped = false;  while (hasChar()) {    ch = chars[pos];    if (!quoted && isOneOf(ch, terminators)) {      break;    }    if (!charEscaped && ch == '"') {      quoted = !quoted;    }    charEscaped = (!charEscaped && ch == '\\');    i2++;    pos++;  }  return getToken(true);}

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

变形之双写filename*与filename

这个场景相对简单

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

变形之编码误用

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

探寻Java文件上传流量层面waf绕过姿势系列二

如果没有,这里报错后会保持原来的值,因此我认为这也可以作为一种绕过思路?

123456
try {  paramValue = RFC2231Utility.hasEncodedValue(paramName) ? RFC2231Utility.decodeText(paramValue)    : MimeUtility.decodeText(paramValue);} catch (final UnsupportedEncodingException e) {  // let's keep the original value in this case}

Spring4

这里我用了springboot1.5.20RELEASE+springframework4.3.23,这里不去研究小版本间是否有差异只看看大版本了

猜猜我在第几层

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

探寻Java文件上传流量层面waf绕过姿势系列二

我们来看看这个extractFilename函数里面到底有啥骚操作吧,这里靠函数indexOf去定位key(filename=/filename*=)再做截取操作

1234567891011121314151617181920212223242526
private String extractFilename(String contentDisposition, String key) {    if (contentDisposition == null) {        return null;    } else {        int startIndex = contentDisposition.indexOf(key);        if (startIndex == -1) {            return null;        } else {            String filename = contentDisposition.substring(startIndex + key.length());            int endIndex;            if (filename.startsWith("\"")) {                endIndex = filename.indexOf("\"", 1);                if (endIndex != -1) {                    return filename.substring(1, endIndex);                }            } else {                endIndex = filename.indexOf(";");                if (endIndex != -1) {                    return filename.substring(0, endIndex);                }            }            return filename;        }    }}

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

Spring5

同样是springboot2.6.4+springframework5.3,这里不去研究小版本间是否有差异只看看大版本了

“双写”绕过

来看看核心部分

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
public static ContentDisposition parse(String contentDisposition) {    List<String> parts = tokenize(contentDisposition);    String type = (String)parts.get(0);    String name = null;    String filename = null;    Charset charset = null;    Long size = null;    ZonedDateTime creationDate = null;    ZonedDateTime modificationDate = null;    ZonedDateTime readDate = null;    for(int i = 1; i < parts.size(); ++i) {        String part = (String)parts.get(i);        int eqIndex = part.indexOf(61);        if (eqIndex == -1) {            throw new IllegalArgumentException("Invalid content disposition format");        }        String attribute = part.substring(0, eqIndex);        String value = part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ? part.substring(eqIndex + 2, part.length() - 1) : part.substring(eqIndex + 1);        if (attribute.equals("name")) {            name = value;        } else if (!attribute.equals("filename*")) {            //限制了如果为null才能赋值            if (attribute.equals("filename") && filename == null) {                if (value.startsWith("=?")) {                    Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value);                    if (matcher.find()) {                        String match1 = matcher.group(1);                        String match2 = matcher.group(2);                        filename = new String(Base64.getDecoder().decode(match2), Charset.forName(match1));                    } else {                        filename = value;                    }                } else {                    filename = value;                }            } else if (attribute.equals("size")) {                size = Long.parseLong(value);            } else if (attribute.equals("creation-date")) {                try {                    creationDate = ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);                } catch (DateTimeParseException var20) {                }            } else if (attribute.equals("modification-date")) {                try {                    modificationDate = ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);                } catch (DateTimeParseException var19) {                }            } else if (attribute.equals("read-date")) {                try {                    readDate = ZonedDateTime.parse(value, DateTimeFormatter.RFC_1123_DATE_TIME);                } catch (DateTimeParseException var18) {                }            }        } else {            int idx1 = value.indexOf(39);            int idx2 = value.indexOf(39, idx1 + 1);            if (idx1 != -1 && idx2 != -1) {                charset = Charset.forName(value.substring(0, idx1).trim());                Assert.isTrue(StandardCharsets.UTF_8.equals(charset) || StandardCharsets.ISO_8859_1.equals(charset), "Charset should be UTF-8 or ISO-8859-1");                filename = decodeFilename(value.substring(idx2 + 1), charset);            } else {                filename = decodeFilename(value, StandardCharsets.US_ASCII);            }        }    }    return new ContentDisposition(type, name, filename, charset, size, creationDate, modificationDate, readDate);}

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

探寻Java文件上传流量层面waf绕过姿势系列二

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

探寻Java文件上传流量层面waf绕过姿势系列二

但由于这里indexof的条件变成了”=”号,而不像spring4那样的filename=/filename=*,毕竟indexof默认取第一个,造成不能像spring4那样做嵌套操作

- source:y4tacker

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月29日00:07:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   探寻Java文件上传流量层面waf绕过姿势系列二https://cn-sec.com/archives/3314743.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息