
Exploit para CVE-2022-25173 visando o bypass da sandbox do interpretador CPS do Jenkins Pipeline Groovy Plugin, permitindo execução arbitrária de código no controlador Jenkins através de scripts Pipeline elaborados.
Um componente chave do conjunto de plugins Pipeline, este fornece o motor de execução padrão para os passos do Pipeline, baseado num interpretador Groovy personalizado que é executado dentro do processo do controlador Jenkins.
(Em princípio, outros motores de execução poderiam ser suportados, sendo FlowDefinition o ponto de entrada da API, mas nenhum foi prototipado e provavelmente seria um esforço muito substancial escrever um.)
Código de script Groovy do Pipeline como
retry(3) {
for (int i = 0; i < 10; i++) {
branches["branch${i}"] = {
node {
retry(3) {
checkout scm
}
sh 'make world'
}
}
}
parallel branches
é executado como um programa Groovy, com certas chamadas de função especiais chamadas steps a realizar operações específicas do Jenkins.
Neste exemplo, o step parallel está definido neste plugin, enquanto node, retry, checkout e sh estão definidos noutros plugins do conjunto Pipeline. A variável global scm está definida no plugin Pipeline Multibranch.
O script Groovy é compilado para uma classe chamada WorkflowScript, por isso é esse o nome mostrado nos stack traces em vez do nome do ficheiro do script (por exemplo, Jenkinsfile).
Ao contrário de um programa Groovy normal executado a partir da linha de comandos, o estado completo do programa de uma build do Pipeline é guardado em disco sempre que é realizada uma operação assíncrona, o que inclui a maioria dos steps do Pipeline.
O Jenkins pode ser reiniciado enquanto uma build está a decorrer e retomará a execução do programa de onde parou.
Isto não pretende ser eficiente e, portanto, deve ser limitado a código de “cola” de alto nível diretamente relacionado com funcionalidades do Jenkins;
a lógica de build do seu próprio projeto deve ser executada a partir de programas externos num nó de build, num step sh ou bat.
O épico Pipeline Groovy no JIRA cobre algumas limitações conhecidas no interpretador Groovy. Estes problemas decorrem do facto de o Pipeline não conseguir executar Groovy diretamente, mas ter de intercetar cada operação para guardar o estado do programa.
O épico Pipeline Sandbox cobre problemas com a sandbox Groovy usada para impedir que scripts Pipeline maliciosos assumam o controlo do Jenkins. Scripts executados com a sandbox desativada podem fazer chamadas diretas às APIs internas do Jenkins, o que pode ser uma solução alternativa útil para funcionalidades de step em falta, mas por razões de segurança apenas os administradores podem aprovar tais scripts.
O épico Pipeline Snippet Generator cobre problemas com a ferramenta usada para fornecer exemplos de sintaxe de step com base em formulários de configuração ao vivo.
Este plugin era anteriormente conhecido como "Workflow CPS plugin" ou "Workflow Groovy Plugin". Por conseguinte, tem o Maven artifactId workflow-cps, e não pipeline-groovy.
O plugin utiliza a biblioteca Groovy CPS para implementar uma transformação de estilo de passagem de continuação no programa à medida que é compilado.
O compilador Groovy padrão é usado para criar a AST, mas a geração de bytecode é intercetada por um CompilationCustomizer que substitui a maioria das operações por variantes que lançam um “erro” especial, CpsCallableInvocation.
Isto é então capturado pelo motor, que utiliza a informação dele (como argumentos prestes a ser passados a uma chamada de método) para passar o controlo para a próxima continuação.
Os scripts Pipeline podem marcar métodos designados com a anotação @NonCPS.
Estes são então compilados normalmente (exceto pelas verificações de segurança da sandbox) e, portanto, comportam-se muito como métodos “binários” da Plataforma Java, do runtime Groovy, ou do código do Jenkins core ou de plugin.
Os métodos @NonCPS podem usar com segurança objetos não Serializable como variáveis locais, embora não devam aceitar parâmetros não serializáveis nem devolver ou armazenar valores não serializáveis.
Não pode chamar métodos regulares (transformados por CPS), ou steps do Pipeline, a partir de um método @NonCPS, pelo que são mais usados para realizar alguns cálculos antes de passar um resumo de volta para o script principal.
Note em particular que @Overrides de métodos definidos em classes binárias,
como Object.toString(),
devem em geral ser marcados como @NonCPS porque será normalmente código binário a chamá-los.
Alguns tipos de objetos não são intrinsecamente seguros para serializar como tal, mas queremos manter uma referência a eles no grafo do programa.
Um exemplo é o Executor (~ slot de executor num nó built-in ou agente) que faz parte do contexto passado por um step node para qualquer step no seu bloco, especialmente sh/bat.
O Pipeline utiliza a API Pickle para substituir estes objetos por versões seguras para serialização.
Quando um WorkflowRun é carregado do disco após um reinício, o estado do programa é desserializado e os pickles são desserializados (“reidratados”) em paralelo.
Se e quando todos os pickles forem desserializados com sucesso e os objetos resultantes forem colocados de volta no estado do programa, o programa começa a executar novamente e StepExecution.onResume é chamado para restaurar timers e afins.
Toda a lógica do programa é executada dentro de uma “thread CPS VM”, que é apenas um pool de threads Java que pode executar métodos binários e determinar qual a continuação a seguir.
O step parallel usa “green threads” (também conhecidas como multitarefa cooperativa): regista nomes de threads lógicas (~ branch) para várias ações, mas não as executa literalmente em simultâneo.
O programa pode parecer realizar tarefas em simultâneo, mas apenas porque a maioria dos steps é executada de forma assíncrona, enquanto a thread VM está inativa, e podem sobrepor-se no tempo.
Nenhuma thread Java é consumida exceto durante os intervalos tipicamente breves em que o código Groovy está efetivamente a ser executado na thread VM.
O widget do executor apenas exibe uma entrada para o executor “flyweight” no nó built-in quando a thread VM está ocupada; normalmente está oculto.