前言
24年9月份的时候打攻防遇到一个帆软报表版本为v11,从/webroot/decision/system/info可以看到细的版本号为模版注入修复前的版本。于是直接使用/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得到下载地址。
本文仅供安全研究和学习使用,由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |