
Эксплойт для CVE-2022-25173, нацеленный на обход песочницы интерпретатора CPS плагина Jenkins Pipeline Groovy, позволяющий выполнить произвольный код внутри контроллера Jenkins с помощью специально созданных Pipeline-скриптов.
Ключевой компонент набора плагинов Pipeline, предоставляющий стандартный движок выполнения для шагов Pipeline, основанный на пользовательском интерпретаторе Groovy, работающем внутри процесса контроллера Jenkins.
(В принципе можно поддерживать и другие движки выполнения, где FlowDefinition является точкой входа API, но ни один из них не был прототипирован, и создание такого движка, скорее всего, потребует значительных усилий.)
Скрипт Pipeline Groovy, например,
retry(3) {
for (int i = 0; i < 10; i++) {
branches["branch${i}"] = {
node {
retry(3) {
checkout scm
}
sh 'make world'
}
}
}
parallel branches
выполняется как программа на Groovy, при этом некоторые специальные вызовы функций, называемые шагами, выполняют специфические для Jenkins операции.
В этом примере шаг parallel определён в этом плагине, а node, retry, checkout и sh определены в других плагинах из набора Pipeline. Глобальная переменная scm определена в плагине Pipeline Multibranch.
Скрипт Groovy компилируется в класс с именем WorkflowScript, поэтому именно это имя отображается в трассировках стека вместо имени файла скрипта (например, Jenkinsfile).
В отличие от обычной программы Groovy, запускаемой из командной строки, полное состояние программы сборки Pipeline сохраняется на диск каждый раз при выполнении асинхронной операции, что включает большинство шагов Pipeline.
Jenkins может быть перезапущен во время выполнения сборки, и он возобновит выполнение программы с того места, где она остановилась.
Это не предполагает высокой эффективности, поэтому следует ограничивать использование высокоуровневым «связующим» кодом, напрямую связанным с функциями Jenkins;
собственная логика сборки вашего проекта должна выполняться из внешних программ на узле сборки, в шаге sh или bat.
Эпик Pipeline Groovy в JIRA охватывает некоторые известные ограничения интерпретатора Groovy. Эти проблемы возникают из-за того, что Pipeline не может напрямую запускать Groovy, а должен перехватывать каждую операцию для сохранения состояния программы.
Эпик Pipeline Sandbox охватывает проблемы, связанные с песочницей Groovy, используемой для предотвращения захвата Jenkins вредоносными скриптами Pipeline. Скрипты, запускаемые с отключённой песочницей, могут напрямую вызывать внутренние API Jenkins, что может быть полезным обходным путём для отсутствующей функциональности шагов, но по соображениям безопасности только администраторы могут утверждать такие скрипты.
Эпик Pipeline Snippet Generator охватывает проблемы с инструментом, используемым для предоставления примеров синтаксиса шагов на основе живых конфигурационных форм.
Ранее этот плагин назывался «Workflow CPS plugin» или «Workflow Groovy Plugin». Соответственно, он имеет Maven artifactId workflow-cps, а не pipeline-groovy.
Плагин использует библиотеку Groovy CPS для реализации трансформации в стиле передачи продолжений программы во время её компиляции.
Стандартный компилятор Groovy используется для создания AST, но генерация байт-кода перехватывается CompilationCustomizer, который заменяет большинство операций вариантами, выбрасывающими специальную «ошибку» CpsCallableInvocation.
Затем это перехватывается движком, который использует полученную информацию (например, аргументы, которые должны быть переданы вызову метода) для передачи управления следующему продолжению.
Скрипты Pipeline могут помечать определённые методы аннотацией @NonCPS.
Такие методы затем компилируются нормально (за исключением проверок безопасности песочницы) и ведут себя во многом как «двоичные» методы из платформы Java, среды выполнения Groovy, ядра Jenkins или кода плагинов.
Методы @NonCPS могут безопасно использовать не-Serializable объекты в качестве локальных переменных, хотя они не должны принимать несериализуемые параметры или возвращать или сохранять несериализуемые значения.
Нельзя вызывать обычные (трансформированные CPS) методы или шаги Pipeline из метода @NonCPS, поэтому их лучше всего использовать для выполнения некоторых вычислений перед передачей результата обратно в основной скрипт.
Обратите внимание, что в частности, @Override методов, определённых в двоичных классах,
таких как Object.toString(),
в целом следует помечать @NonCPS, поскольку их обычно вызывает двоичный код.
Некоторые виды объектов по своей сути небезопасны для сериализации как таковые, однако мы хотим сохранить ссылку на них в графе программы.
Примером является Executor (~ слот исполнителя на встроенном узле или узле агента), который является частью контекста, передаваемого шагом node любому шагу в его блоке, особенно sh/bat.
Pipeline использует API Pickle для подстановки сериализационно-безопасных версий этих объектов.
Когда WorkflowRun загружается с диска после перезапуска, состояние программы десериализуется, а пиклы (pickles) десериализуются («регидратируются») параллельно.
Если и когда все пиклы успешно десериализованы и полученные объекты помещены обратно в состояние программы, программа возобновляет выполнение, и вызывается StepExecution.onResume для восстановления таймеров и тому подобного.
Вся логика программы выполняется внутри «потока CPS VM», который представляет собой просто пул потоков Java, способный выполнять двоичные методы и определять, какое продолжение выполнять следующим.
Шаг parallel использует «зелёные потоки» (также известные как кооперативная многозадачность): он записывает логические имена потоков (~ ветвей) для различных действий, но не выполняет их буквально одновременно.
Программа может казаться выполняющей задачи одновременно, но только потому, что большинство шагов выполняются асинхронно, пока поток VM простаивает, и они могут перекрываться по времени.
Никакой поток Java не потребляется, за исключением обычно кратких интервалов, когда код Groovy фактически выполняется в потоке VM.
Виджет исполнителя отображает запись для «облегчённого» исполнителя на встроенном узле только тогда, когда поток VM занят; обычно он скрыт.