前言
24年9月份的时候打攻防遇到一个帆软报表版本为v11,从/* by 01130.hk - online tools website : 01130.hk/zh/formatxml.html */
/webroot/decision/system/info
可以看到细的版本号为模版注入修复前的版本。于是直接使用/* by 01130.hk - online tools website : 01130.hk/zh/formatxml.html */
/webroot/decision/view/ReportServer?test=exp
进行利用发现被WAF拦截,经过测试这个地方很难用常规的方法绕过WAF
。
分析源码
因为比较难从传统的办法绕过WAF
,于是转而分析源码看是否存在一些其他的绕过方式。
其实之前就对这个漏洞进行过分析,从公开的POC路由可以直接搜索/view/ReportServer
然后再搜索com.fr.web.controller.ViewRequestConstants#REPORT_VIEW_PATH_COMPATIBLE
哪里被调用
找到对应实现类和方法,正常来说的话这里可以直接在idea
里两下shift
直接查找路由但是不知道为啥idea
没识别到,猜测可能是帆软改了Controller
注解的包名导致的。com.fr.web.controller.ReportRequestCompatibleService#preview
这里直接使用的getQueryString
获取我们输入的查询参数且不会URL解码所以有很多特殊字符也不能输入,所以想从这个地方找到一些绕过WAF
的办法比较难,除非去看还有哪些比较的特殊的模板方法能用来编码解码。但是经过测试发现WAF
对于${..}
特别敏感就算找到一些特殊的模板方法估计也没用。因为帆软报表以前也出过这个类型的模版注入,想着应该还有其他地方可以前台触发这个漏洞。于是通过jadx
直接搜索com.fr.base.TemplateUtils#render(java.lang.String)、com.fr.base.TemplateUtils#renderParameter4Tpl
这类sink
发现搜索结果有点多找起来的话比较麻烦。在阅读源码的过程中发现大多数进入这两个sink
的字符串都符合如下正则特征"\$\{.*\}.*\+
于是使用jadx
直接搜索。
排除参数不可控、以及也是使用getQueryString
的很快定位到com.fr.nx.app.web.v9.handler.handler.PDFPrintPrintForIEHandler#handleRequest
protected void handleRequest(HttpServletRequest var1, HttpServletResponse var2) throws Exception {
String var3 = SessionPoolManager.getOrGenerateSessionIDWithCheckRegister(var1, var2);
if (var3 != null) {
VersionTransition.saveCalculatorContext(var1, "/view/report");
String var4 = "${servletURL}?op=export&sessionID=" + var3 + "&format=pdf&frandom=" + Math.random() + System.currentTimeMillis() + "&isPDFPrint=true&extype=ori";
String var6 = WebUtils.getHTTPRequestParameter(var1, "codebase");
String var5;
if ("true".equals(var6)) {
var5 = "<OBJECT ID='PDFReader' WIDTH='100%' HEIGHT='100%' CLASSID='CLSID:CA8A9780-280D-11CF-A24D-444553540000'";
var5 = var5 + " codebase=\"${servletURL}?op=resource&resource=/AdobeReader.exe\">";
var5 = var5 + "<param name='src' value='" + var4 + "'></OBJECT>";
} else {
var5 = "<OBJECT ID='PDFReader' WIDTH='0' HEIGHT='0' CLASSID='CLSID:CA8A9780-280D-11CF-A24D-444553540000'><param name='src' value='" + var4 + "'></OBJECT>";
}
PrintWriter var7 = WebUtils.createPrintWriter(var2);
var7.print(TemplateUtils.render(var5));
var7.flush();
var7.close();
}
}
先从请求中获取sessionID
然后拼接进var4
再拼接进var5
最后进入render
触发模版注入。然后我们查找在哪里使用了PDFPrintPrintForIEHandler
找到入口方法com.fr.nx.app.web.controller.NXController#pdfPrintForIEV9
其路由为/webroot/decision/nx/report/v9/print/ie/pdf
设置了请求方式仅为GET
其实我本来是想找POST
的这类路由的因为POST
肯定比较好绕一点。但是后面查看获取sessionID
的方式时发现这里存在多种编码方式可以绕过WAF
。我们跟入com.fr.web.core.SessionPoolManager#getOrGenerateSessionIDWithCheckRegister
查看如何获取sessionID
public static String getOrGenerateSessionIDWithCheckRegister(HttpServletRequest var0, HttpServletResponse var1) throws Exception {
String var2 = NetworkHelper.getHTTPRequestSessionIDParameter(var0);
if (var2 == null) {
var2 = generateSessionIDWithCheckRegister(var0, var1);
}
return var2;
}
一直跟下去最后会到getHTTPRequestEncodeParameter
public static String getHTTPRequestEncodeParameter(HttpServletRequest var0, String var1, boolean var2) {
ExtraClassManagerProvider var3 = (ExtraClassManagerProvider)PluginModule.getAgent(PluginModule.ExtraCore);
Object var4;
if (var3 == null) {
var4 = DefaultRequestParameterHandler.getInstance();
} else {
var4 = (RequestParameterHandler)var3.getSingle("RequestParameterHandler");
if (var4 == null) {
var4 = DefaultRequestParameterHandler.getInstance();
}
}
Object var5 = ((RequestParameterHandler)var4).getParameterFromHeader(var0, var1);
if (var5 == null) {
var5 = ((RequestParameterHandler)var4).getParameterFromRequest(var0, var1);
}
if (var5 == null) {
var5 = ((RequestParameterHandler)var4).getParameterFromAttribute(var0, var1);
}
if (var5 == null) {
var5 = ((RequestParameterHandler)var4).getParameterFromJSONParameters(var0, var1);
}
if (var5 == null) {
var5 = ((RequestParameterHandler)var4).getParameterFromSession(var0, var1);
}
if (var5 == null) {
var1 = CodeUtils.cjkEncode(var1);
var5 = ((RequestParameterHandler)var4).getParameterFromRequest(var0, var1);
if (var5 == null) {
var5 = ((RequestParameterHandler)var4).getParameterFromAttribute(var0, var1);
if (var5 == null) {
var5 = ((RequestParameterHandler)var4).getParameterFromSession(var0, var1);
}
}
}
return var2 ? checkURLDecode(var5) : GeneralUtils.objectToString(var5);
}
这里var1=sessionID,var2=true
这个方法里通过多种方式获取参数值。
- Request.getHeader里获取
- Request.getParameter获取
- Session.getAttribute获取
- getParameterFromJSONParameters
- Request.getAttribute获取
所以我们这里可以用来获取的途径有三种getHeader|getParameter|getParameterFromJSONParameters
注意到最后return
的时候var2=true
就会进入checkURLDecode
private static String checkURLDecode(Object var0) {
if (var0 == null) {
return null;
} else {
String var1 = CommonCodeUtils.decodeText(String.valueOf(var0));
try {
return URLDecoder.decode(var1, "UTF-8");
} catch (UnsupportedEncodingException var3) {
return null;
} catch (IllegalArgumentException var4) {
return var1;
}
}
}
这里会先调用decodeText
进行解码再调用URLDecoder
解码。跟入decodeText
最后会调用com.fr.stable.CommonCodeUtils#cjkDecode
进行解码
public static @NotNull String cjkDecode(@Nullable String text) {
if (text == null) {
return "";
} else if (!isCJKEncoded(text)) {
return text;
} else {
StringBuilder newTextBuf = new StringBuilder();
for(int i = 0; i < text.length(); ++i) {
char ch = text.charAt(i);
if (ch == '[') {
int rightIdx = text.indexOf(93, i + 1);
if (rightIdx > i + 1) {
String subText = text.substring(i + 1, rightIdx);
if (subText.length() > 0) {
ch = (char)Integer.parseInt(subText, 16);
}
i = rightIdx;
}
}
newTextBuf.append(ch);
}
return newTextBuf.toString();
}
}
对应的编码方法为com.fr.stable.CommonCodeUtils#cjkEncode
public static @NotNull String cjkEncode(@Nullable String text) {
if (text == null) {
return "";
} else {
StringBuilder newTextBuf = new StringBuilder();
int i = 0;
for(int len = text.length(); i < len; ++i) {
char ch = text.charAt(i);
if (needToEncode(ch)) {
newTextBuf.append('[');
newTextBuf.append(Integer.toString(ch, 16));
newTextBuf.append(']');
} else {
newTextBuf.append(ch);
}
}
return newTextBuf.toString();
}
}
所以我们可以将我们的payload
先URL编码再使用cjkEncode
编码进行利用从而绕过WAF
进行模版注入。按照上述思路进行测试后发现生成的payload
长度太长了,我们这个新找的接口是GET型的所以payload
长度太长的话会直接导致tomcat
报错。于是换了一个简短一些的写文件马,以及在cjkEncode
编码的时候只编码非字母非数字字符,然后将payload
放入header
中,成功绕过WAF
写入文件。
但是发现webshell
没有正常解析,应该是windows
然后使用Anchor
师傅的解决办法,使用/webroot/decision/file
接口初始化JasperInitializer
再次访问webshell
成功解析
上面的编码方式实际上在帆软的大多数获取参数值的场景都可以使用。
总结
在WAF
越来越强且使用越来越广的高对抗情况下,我们除了使用传统的绕过方法还可以从漏洞代码出发寻找其他漏洞利用路径以及可能存在的某种编码方式或解析差异绕过WAF
进行攻击。对于0day
挖掘人员在进行漏洞挖掘利用的过程中也应深入源码查看是否有某种特定的方式可以使得我们的payload
不具备明显特征,这样在利用的过程中也能减少被发现的可能,提高0day
的存活时间。
如需编码脚本进行研究可关注公众号漫漫安全路
,回复fr
得到下载地址。
本文仅供安全研究和学习使用,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。