CVE-2024-3400: PAN-OS Command Injection Vulnerability in GlobalProtect Gateway. Learn More

CVE-2024-3400: PAN-OS Command Injection Vulnerability in GlobalProtect Gateway. Learn More

Services
Capture
Managed Detection & Response

Eliminate active threats with 24/7 threat detection, investigation, and response.

twi-managed-portal-color
Co-Managed SOC (SIEM)

Maximize your SIEM investment, stop alert fatigue, and enhance your team with hybrid security operations support.

twi-briefcase-color-svg
Advisory & Diagnostics

Advance your cybersecurity program and get expert guidance where you need it most.

tw-laptop-data
Penetration Testing

Test your physical locations and IT infrastructure to shore up weaknesses before exploitation.

twi-database-color-svg
Database Security

Prevent unauthorized access and exceed compliance requirements.

twi-email-color-svg
Email Security

Stop email threats others miss and secure your organization against the #1 ransomware attack vector.

tw-officer
Digital Forensics & Incident Response

Prepare for the inevitable with 24/7 global breach response in-region and available on-site.

tw-network
Firewall & Technology Management

Mitigate risk of a cyberattack with 24/7 incident and health monitoring and the latest threat intelligence.

Solutions
BY TOPIC
Offensive Security
Solutions to maximize your security ROI
Microsoft Exchange Server Attacks
Stay protected against emerging threats
Rapidly Secure New Environments
Security for rapid response situations
Securing the Cloud
Safely navigate and stay protected
Securing the IoT Landscape
Test, monitor and secure network objects
Why Trustwave
About Us
Awards and Accolades
Trustwave SpiderLabs Team
Trustwave Fusion Security Operations Platform
Trustwave Security Colony
Partners
Technology Alliance Partners
Key alliances who align and support our ecosystem of security offerings
Trustwave PartnerOne Program
Join forces with Trustwave to protect against the most advance cybersecurity threats
SpiderLabs Blog

The Case of an Obscure Injection

During a recent application penetration test, I came across what proved to be an interesting SQL Injection (SQLi) vulnerability. This case of SQLi was interesting for a couple reasons:

  1. The challenges that it presented during exploitation
  2. The Database Management System (DBMS) that was in use.

Below, I'll walk you through the hurdles that I encountered and how I overcame them to lead to full data extraction.

The application was using a backend Java remoting framework called DWR, and this is where the SQLi appeared. The system calls looked like this:

8765_38bae954-1962-4530-a815-051295666773

The injection was located in the c0-param1 parameter. On the surface, it looked like it was going to be quite easy, as it gave verbose error messages. When a single quote was added to the end of the parameter, I got the following error stating, "java.sql.SQLException: A syntax error has occurred."

12387_e82079ae-5c28-4e26-b7ea-472325bb1af1

The c0-param1 parameter controlled how many results were retrieved from the database. Based on this, the injection appeared to be in something like the TOP keyword for Microsoft SQL Server or LIMIT for MySQL. I decided the application was more likely running MS SQL Server than MySQL. So I tried adding a column to the results as illustrated here:

10509_8d0a754c-b3c0-4bf7-9a31-a449445433a0

When I executed this query, I got an error message stating, "java.sql.SQLException: The column (card_no) must be in the GROUP BY list." With a little Googling, I quickly figured out that the DBMS was IBM Informix. I knew very little to nothing about Informix, so some research was in order. It didn't help that there was nearly nothing by the way of SQLi information out there for Informix, so I had to turn to the IBM documentation looking for a function I could use to leverage the error-based nature of this vulnerability. However, that appeared to be fruitless.

Since I now knew it was Informix, I had a better understanding of where the injection was occurring. It was in the FIRST clause, similar to TOP and LIMIT. The query looked something like this:

SELECT FIRST 10 col1, col2, ... FROM table...

My first thought was that I could basically control the entire query with comments, but…not so fast, my friend. The application did not like the comment string of "--", and it would throw an error whenever it saw this. Without comments or a useful error-based function, I turned to boolean-based blind injection.

Informix supports the CASE statement that is in most SQL languages, so I decided to use that. The injection looked like this:

c0-param1=string:10 (CASE WHEN SUBSTR((SELECT USER FROM SYSTABLES WHERE TABID = 1), 1, 1) = 'a' THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Basically, if the condition were true (i.e., the first letter of the current username is 'a') the query would return results, and if it were false, it would return an error since the subquery (SELECT 1 FROM SYSTABLES) returns more than one result. However, this led me to my second hurdle; I couldn't use the equal sign either. The application would throw an error with that too. By using alternate operators, I arrived at the following injection:

c0-param1=string:10 (CASE WHEN SUBSTR((SELECT USER FROM SYSTABLES WHERE TABID > 0 AND TABID < 2), 1, 1) LIKE 'a' THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Armed with the knowledge of this injection and a working proof-of-concept, I turned to my tool of choice for doing the heavy lifting of dumping the database, sqlmap (for some sqlmap tips from my colleague Christophe De La Fuente, read his post "Sqlmap Tricks for Advanced SQL Injection"). SQLmap is an awesome tool, and the author, Bernardo, is a nice guy whom I met at Defcon this year…but I digress. Alas, sqlmap does not support Informix. So, I had to write my own tool for dumping the database.

I started with dumping the schema. My script walked through the database in this fashion as follows:

Get the length of the table name (incrementing the highlighted portions):

c0-param1=string:10 (CASE WHEN CHAR_LENGTH((SELECT TABNAME FROM SYSTABLES WHERE TABID > 0 AND TABID < 2)) > 1 THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Get the table name:

c0-param1=string:10 (CASE WHEN SUBSTR((SELECT TABNAME FROM SYSTABLES WHERE TABID > 0 AND TABID < 2), 1, 1) LIKE 'a' THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Get the number of columns in the table:

c0-param1=string:10 (CASE WHEN (SELECT NCOLS FROM SYSTABLES WHERE TABID > 0 AND TABID < 2) > 1 THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Get the length of the column name:

c0-param1=string:10 (CASE WHEN CHAR_LENGTH((SELECT COLNAME FROM SYSCOLUMNS WHERE (TABID > 0 AND TABID < 2) AND (COLNO > 0 AND COLNO < 2))) > 1 THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Get the column name:

c0-param1=string:10 (CASE WHEN SUBSTR((SELECT COLNAME FROM SYSCOLUMNS WHERE (TABID > 0 AND TABID < 2) AND (COLNO > 0 AND COLNO < 2)), 1, 1) LIKE 'a' THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Once I had the schema, I could begin dumping the data. This led to the final two hurdles I needed to overcome. The subquery that is passed to the SUBSTR function can only return one result. Therefore, I had to find a way to only return one column at a time.

My first thought was to use the FIRST statement that I talked about in the beginning. Unfortunately, it results in this error, "Cannot use 'FIRST' in this context.", as the FIRST clause cannot be used in a subquery. So, I decided to look for some sort of ID field in the user-defined tables, but there did not appear to be anything useful.

Through a little research I discovered that Informix tables actually have a hidden column named ROWID, which is a unique sequential number for each row. However, these ROWIDs can become fragmented with DELETEs and INSERTs into the table. In order to find the ROWIDs that contained data, I turned to a function called NVL. This function returns different results depending on whether the first argument is NULL. By selecting a column that was most likely to always contain data, I could find the ROWIDs in use like the following:

c0-param1=string:10 (NVL((SELECT username FROM users WHERE ROWID > 0 AND ROWID < 2), (SELECT 1 FROM SYSTABLES))),

With the active ROWIDs, I could begin reaping my rewards like so:

c0-param1=string:10 (CASE WHEN SUBSTR((SELECT username FROM users WHERE ROWID > 0 AND ROWID < 1), 1, 1) LIKE 'a' THEN 1 ELSE (SELECT 1 FROM SYSTABLES) END),

Through this method, I was able to discover plaintext application credentials, plaintext bank SFTP credentials and unencrypted payment card numbers.

Along with this success, I now know a ton more about IBM Informix than I ever thought I would need to know.

Latest SpiderLabs Blogs

Guardians of the Gateway: Identity and Access Management Best Practices

This is Part 10 in my ongoing project to cover 30 cybersecurity topics in 30 weekly blog posts. The full series can be found here.

Read More

Protecting Zion: InfoSec Encryption Concepts and Tips

This is Part 9 in my ongoing project to cover 30 cybersecurity topics in 30 weekly blog posts. The full series can be found here.

Read More

EDR – The Multi-Tool of Security Defenses

This is Part 8 in my ongoing project to cover 30 cybersecurity topics in 30 weekly blog posts. The full series can be found here.

Read More