fbpx

Pesquisando sistematicamente o PHP disable_functions ignora

Pesquisando sistematicamente o PHP disable_functions ignora

 

# Pesquisando sistematicamente o PHP disable_functions ignora

Nos últimos dias, uma vulnerabilidade foi descoberta na função ** imap_open ** ([CVE-2018-19518] (https://www.cvedetails.com/cve/cve-2018-19518)). O principal problema com esse problema é que um usuário local pode abusar dessa vulnerabilidade para ignorar algumas restrições em servidores protegidos e executar comandos do SO, porque essa função geralmente é permitida. Esse tipo de desvio é semelhante ao baseado no ShellShock: alguém pode injetar comandos do SO em funções não banidas por disable_functions. Eu queria encontrar uma maneira de descobrir desvios semelhantes de maneira automatizada.

A idéia principal é dividir o problema em poucas tarefas:

1. Extraia os parâmetros esperados por cada função PHP
2. Execute e rastreie chamadas para todas as funções com os parâmetros corretos
3. Procure no rastreamento por possíveis chamadas perigosas (por exemplo, um execve)

Claro que essa é uma abordagem ingênua, mas esse mesmo trabalho pode ser reutilizado para me ajudar enquanto fuzzing … então não é um esforço inútil, eu acho ** 🙂 **.

## 0x01 Extraindo os parâmetros

A primeira coisa que precisamos lidar é uma maneira real de adivinhar ou identificar corretamente os parâmetros usados ​​para cada função. É claro que podemos analisar as informações da documentação pública (como o PHP.net por exemplo), mas algumas funções podem ter parâmetros ocultos não documentados ou a documentação apenas os nomeia como “misturados”. Esse ponto é importante porque, se a função não for chamada corretamente, nosso rastreamento perderá chamadas em potencial para funções / syscalls perigosas. Existem diferentes maneiras de realizar essa identificação, cada uma com suas vantagens e desvantagens, portanto, precisamos combinar todas elas para descobrir o número máximo de parâmetros (e ** que tipo ** são eles).

Uma abordagem extremamente preguiçosa pode ser usar a classe [ReflectionFunction] (http://php.net/manual/es/class.reflectionfunction.php). Através desta classe simples, podemos obter (a partir do próprio PHP) o nome e os parâmetros usados ​​por todas as funções disponíveis, mas tem a desvantagem de não saber que tipo é realmente. Podemos discriminar apenas “strings” e “matrizes”. Um exemplo:


<? php
// Obtém todas as funções definidas de forma lenta
$ all = get_defined_functions ();
// Descarte funções irritantes
// De https://github.com/nikic/Phuzzy
$ bad = array (‘sleep’, ‘usleep’, ‘time_nanosleep’, ‘time_sleep_until’, ‘pcntl_sigwaitinfo’, ‘pcntl_sigtimedwait’,
‘readline’, ‘readline_read_history’, ‘dns_get_record’, ‘posix_kill’, ‘pcntl_alarm’, ‘set_magic_quotes_runtime’, ‘readline_callback_handler_install’,);
$ all = array_diff ($ all [“interno”], $ incorreto);

foreach ($ all as $ function) {
$ parameters = “$ function”;
$ f = nova ReflectionFunction (função $);
foreach ($ f-> getParameters () como $ param) {
if ($ param-> isArray ()) {
$ parameters. = “ARRAY”;
} mais {
$ parameters. = “STRING”;
}
}
eco substr ($ parâmetros, 0, -1);
eco “\ n”;
}
?>

Este código gera uma lista de funções que podemos analisar posteriormente para gerar os testes nos quais vamos rastrear as chamadas:


json_last_error_msg
spl_classes
spl_autoload STRING STRING
spl_autoload_extensions STRING
spl_autoload_register STRING STRING STRING
spl_autoload_unregister STRING
spl_autoload_functions
spl_autoload_call STRING
class_parents STRING STRING
class_implements STRING STRING
class_uses STRING STRING
spl_object_hash STRING
spl_object_id STRING
iterator_to_array STRING STRING
iterator_count STRING
iterator_apply STRING STRING ARRAY

Uma abordagem muito melhor pode ser conectar a função usada internamente pelo PHP para analisar os parâmetros, como é usado no artigo “[Procurando por parâmetros ocultos nas funções internas do PHP (usando frida)] (http: // www. libnex.org/blog/huntingforhidden Parameterswithinphpbuilt-infunctionsusingfrida) ”. Os autores usam o FRIDA para ligar a função ** zend_parse_parameters ** e analisar o padrão usado para validar os parâmetros passados ​​(se você quiser saber um pouco mais sobre o FRIDA, sinta-se à vontade para conferir meu artigo [Hackeando um jogo para aprender o básico do FRIDA (Pwn Aventura 3)] (https://x-c3ll.github.io/posts/Frida-Pwn-Adventure-3/)). Essa abordagem é uma das melhores porque, com os padrões, podemos saber que tipo está esperando, mas tem uma pequena desvantagem: essa função foi descontinuada e, portanto, no futuro não será usada.

Os internos do PHP 7 são um pouco diferentes do PHP 5 e algumas APIs, como a usada para análise de parâmetros, são afetadas por essas alterações. A API antiga é baseada em strings, enquanto a nova API (usada pelo PHP 7) é baseada em macros. Onde tínhamos a função zend_parse_parameters, agora temos a macro ** ZEND_PARSE_PARAMETERS_START ** e sua família. Para saber mais sobre como o PHP analisa os argumentos, sinta-se à vontade para conferir este artigo incrível do phpinternals.net: [Análise de Parâmetros Zend (API ZPP)] (https://phpinternals.net/categories/zend_parameter_parsing). Basicamente agora não podemos apenas dizer “Hey FRIDA! faça sua mágica! ”, conectando uma função de chave mestra.

Se lembrarmos, em nosso artigo [Melhorando as extensões PHP como método de persistência] (https://x-c3ll.github.io/posts/PHP-extension-backdoor/), vimos que a função md5 estava usando a nova API ZPP para analisar os parâmetros:


ZEND_PARSE_PARAMETERS_START (1, 2)
Z_PARAM_STR (arg)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL (raw_output)
ZEND_PARSE_PARAMETERS_END ();

Para extrair os parâmetros, uma abordagem surrada (e eficaz) é compilar o PHP com símbolos e usar um script no GDB para analisar as informações. É claro que existem maneiras melhores do que usar o GDB, mas recentemente tive que criar scripts para alguns auxiliares para depurar o PHP no GDB que se encaixam muito bem nessa outra tarefa, por isso não tenho vergonha de reutilizá-los. Vamos compilar a última versão do PHP:


cd / tmp
wget http://am1.php.net/distributions/php-$(wget -qO- http://php.net/downloads.php | grep -m 1 h3 | cut -d ‘”‘ -f 2 | cut -d “v” -f 2) .tar.gz
tar xvf php * .tar.gz
rm php * .tar.gz
cd php *
./configure CFLAGS = “- g -O0”
make -j10
sudo make install

Nosso script para o GDB funcionará da seguinte maneira:

1. Execute `list functionName`
2. Se ** ZEND_PARSE_PARAMETERS_END ** não estiver presente, aumente o número de linhas a serem exibidas na lista e tente novamente
3. Se estiver presente, interrompa o loop e extraia as linhas entre as macros **… _START ** e ** … _END **
4. Analise os parâmetros entre eles

Esse fluxo de ações pode ser resumido neste snippet simples:

# Quando faço coisas assim, me sinto muito mal
# Satanismo cortesia de @ TheXC3LL

classe zifArgs (gdb.Command):
“Mostra os parâmetros PHP usados ​​por uma função quando ela usa a API PHP 7 ZPP. Símbolos necessários.”

def __init __ (próprio):
super (zifArgs, auto) .__ init __ (“zifargs”, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, True)

def invoke (self, arg, from_tty):
size = 10
enquanto True:
tente:
sourceLines = gdb.execute (“lista zif_” + arg, to_string = True)
exceto:
tente:
sourceLines = gdb.execute (“lista php_” + arg, to_string = True)
exceto:
tente:
sourceLines = gdb.execute (“lista php_if_” + arg, to_string = True)
exceto:
print (“\ 033 [31m \ 033 [1mFunction” + arg + “não definido! \ 033 [0m”)
retornar
se “ZEND_PARSE_PARAMETERS_END” não estiver no sourceLines:
tamanho + = 10
gdb.execute (“definir tamanho da lista” + str (tamanho))
mais:
gdb.execute (“definir tamanho da lista 10”)
quebrar
tente:
chunk = sourceLines [sourceLines.index (“_ START”): sourceLines.rindex (“_ END”)]. split (“\ n”)
exceto:
print (“\ 033 [31m \ 033 [1mParâmetros não encontrados. Tente zifargs_old <função> \ 033 [0m”)
retornar
params = []
para x no pedaço:
se “Z_PARAM_ARRAY” em x:
params.append (“\ 033 [31mARRAY”)
se “Z_PARAM_BOOL” em x:
params.append (“\ 033 [32mBOOL”)
se “Z_PARAM_FUNC” em x:
params.append (“\ 033 [33mCALLABLE”)
se “Z_PARAM_DOUBLE” em x:
params.append (“\ 033 [34mDOUBLE”)
se “Z_PARAM_LONG” em x ou “Z_PARAM_STRICT_LONG” em x:
params.append (“\ 033 [36mLONG”)
se “Z_PARAM_ZVAL” em x:
params.append (“\ 033 [37mMIXED”)
se “Z_PARAM_OBJECT” em x:
params.append (“\ 033 [38mOBJECT”)
se “Z_PARAM_RESOURCE” em x:
params.append (“\ 033 [39mRESOURCE”)
se “Z_PARAM_STR” em x:
params.append (“\ 033 [35mSTRING”)
se “Z_PARAM_CLASS” em x:
params.append (“\ 033 [37mCLASS”)
se “Z_PARAM_PATH” em x:
params.append (“\ 033 [31mPATH”)
se “Z_PARAM_OPTIONAL” em x:
params.append (“\ 033 [37mOPTIONAL”)
se len (params) == 0:
print (“\ 033 [31m \ 033 [1mParâmetros não encontrados. Tente zifargs_old <função> ou zifargs_error <função> \ 033 [0m”)
retornar
print (“\ 033 [1m” + ” .join (parâmetros) + “\ 033 [0m”)

zifArgs ()

Os resultados são muito bons (tudo depois de “OPCIONAL” é opcional):

pwndbg: carregado 171 comandos. Digite pwndbg [filter] para uma lista.
pwndbg: criou as funções $ rebase, $ ida gdb (podem ser usadas com print / break)
[+] Estúpido GDB Helper para PHP carregado! (por @ TheXC3LL)
Lendo símbolos do php … pronto.
pwndbg> zifargs md5
STRING BOOL OPCIONAL
pwndbg> hora dos zifargs
LONG BOOL OPCIONAL

Como dissemos antes, toda técnica tem sua própria desvantagem, e abordagens tão ingênuas podem falhar:


pwndbg> zifargs array_map
CALLABLE

A função [array_map] (http://php.net/manual/es/function.array-map.php) possui uma matriz como segundo parâmetro e nosso snippet não conseguiu detectá-la. Outra técnica para extrair os parâmetros é analisar os erros descritivos gerados por algumas funções no PHP. Por exemplo, array_map, informará quantos parâmetros precisam:


psyconauta @ insulatergum: ~ / research / php / |
⇒ php -r ‘array_map ();’

Aviso: array_map () espera pelo menos 2 parâmetros, 0 dados no código da linha de comandos na linha 1

E se definirmos os dois parâmetros como strings, ele alertará sobre que tipo de parâmetro está esperando:


psyconauta @ insulatergum: ~ / pesquisa / php /
⇒ php -r ‘array_map (“aaa”, “bbb”);’

Aviso: array_map () espera que o parâmetro 1 seja um retorno de chamada válido, função ‘aaa’ não encontrada ou nome de função inválido no código de linha de comando na linha 1

Portanto, podemos usar esses erros para inferir os parâmetros:

1. Chame a função vazia
2. Verifique os erros para ver quantos parâmetros precisam
3. Preencha-os com cordas
4. Analise o tipo esperado do aviso
5. Altere o parâmetro para um do tipo certo
6. Repita de 4

Eu implementei outro comando extremamente gasto para o GDB executar esta tarefa:

 

# Não me deixe usar gdb quando estiver bêbado
# Desculpe por este trecho de código 🙁

classe zifArgsError (gdb.Command):
“Tenta inferir parâmetros de erros de PHP”

def __init __ (próprio):
super (zifArgsError, self) .__ init __ (“zifargs_error”, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, True)

def invoke (self, arg, from_tty):
carga = “<? php” + arg + “();?>”
arquivo = aberto (“/ tmp / .zifargs”, “w”)
file.write (carga útil)
file.close ()
tente:
output = str (subprocess.check_output (“php /tmp/.zifargs 2> & 1”, shell = True))
exceto:
print (“\ 033 [31m \ 033 [1mFunction” + arg + “não definido! \ 033 [0m”)
retornar
tente:
número = saída [output.index (“pelo menos”) +9: output.index (“pelo menos”) +10]
exceto:
número = saída [output.index (“exatamente”) +8: output.index (“exatamente”) + 9]
print (“\ 033 [33m \ 033 [1m” + arg + “(\ 033 [31m” + número + “\ 033 [33m): \ 033 [0m”)
params = []
inferido = []
i = 0
enquanto True:
carga = “<? php” + arg + “(”
para x no intervalo (0, int (número) -len (params)):
params.append (“‘aaa'”)
carga útil = = ‘,’. junção (parâmetros) + “);?>”
arquivo = aberto (“/ tmp / .zifargs”, “w”)
file.write (carga útil)
file.close ()
output = str (subprocess.check_output (“php /tmp/.zifargs 2> & 1”, shell = True))
#print (saída)
se “,” na saída:
separador = “,”
elif “file” na saída:
params [i] = “/ etc / passwd” # Não execute isso como root, pelo amor de Deus.
infered.append (“\ 033 [31mPATH”)
i + = 1
elif “in” na saída:
separador = “in”

tente:
dataType = output [: output.rindex (separador)]
dataType = dataType [dataType.rindex (“”) +1:]. lower ()
se dataType == “array”:
params [i] = “matriz (‘a’)”
infered.append (“\ 033 [31mARRAY”)
se dataType == “retorno de chamada”:
params [i] = “‘var_dump'”
infered.append (“\ 033 [33mCALLABLE”)
se dataType == “int”:
parâmetros [i] = “1337”
infered.append (“\ 033 [36mINTEGER”)
i + = 1
#print (parâmetros)
exceto:
se len (inferido)> 0:
print (“\ 033 [1m” + ” .join (inferido) + “\ 033 [0m”)
retornar
mais:
print (“\ 033 [31m \ 033 [1mNão foi possível recuperar parâmetros de” + arg + “\ 033 [0m”)
retornar

Experimente com array_map:


pwndbg> zifargs_error array_map
array_map (2):
ARRAY CALLABLE

Até agora, explicamos diferentes técnicas que podem ser combinadas para obter automaticamente os parâmetros necessários para executar corretamente (ou quase) todas as funções do PHP. Como eu disse antes, essas técnicas também podem ser usadas para difusão, a fim de alcançar caminhos de código adicionais ou para executar instâncias de difusão ignoradas. Vamos ver agora como podemos usar as informações coletadas.

0x02 Obtendo e analisando rastreamentos

A maneira mais simples de obter um rastreamento é usar ferramentas conhecidas como strace e ltrace. Com poucas linhas de bash, podemos analisar os logs gerados na etapa anterior com o nome da função e parâmetros, executar o rastreador e salvar os logs em um arquivo. Vamos analisar o log gerado pela função mail (), por exemplo:


⇒ strace -f / usr / bin / php -r ‘mail (“aaa”, “aaa”, “aaa”, “aaa”);’ 2> & 1 | grep exe
execve (“/ usr / bin / php”, [“/ usr / bin / php”, “-r”, “mail (\” aaa \ “, \” aaa \ “, \” aaa \ “, \” aaa \ “);”], [/ * 28 vars * /]) = 0
[pid 471] execve (“/ bin / sh”, [“sh”, “-c”, “/ usr / sbin / sendmail -t -i”], [/ * 28 vars * /] <inacabado … >
[pid 471] <… resumo executivo>) = 0
[pid 472] execve (“/ usr / sbin / sendmail”, [“/ usr / sbin / sendmail”, “-t”, “-i”], [/ * 28 vars * /]) = -1 ENOENT ( Esse arquivo ou diretório não existe)

Você viu isso ** execve () ** com o sendmail? Isso significa que esta função pode ser abusada a fim de ignorar o disable_functions (na medida em que permitimos ao putenv manipular o LD_PRELOAD). De fato, é assim que [CHANKRO] (https://github.com/TarlogicSecurity/Chankro) funciona: se podemos definir variáveis ​​de ambiente, podemos definir LD_PRELOAD para carregar um arquivo malicioso quando um binário externo (como vemos no rastreamento de log) ) é chamado. Basta executar o script, aguardar e executar alguns greps para verificar se há chamadas interessantes. ** Fácil peasy 🙂 **

## 0x03 Palavras finais

Automatizar a extração de parâmetros pode ser um pouco complicado, então eu decido escrever este artigo para contribuir com meu grão de areia. Meses atrás, li [este artigo] (http://www.libnex.org/blog/huntingforhiddenparameterswithinphpbuilt-infunctionsusingfrida) em que o FRIDA é usado para conectar zend_parse_parameters e eu queria completar um pouco mais essas informações para iniciantes em PHP internos. A vulnerabilidade imap_open () foi uma desculpa perfeita para escrever sobre o tópico ** 🙂 **.

Se você achar útil este artigo, ou quiser me indicar um erro ou erro de digitação, entre em contato comigo no twitter [@ TheXC3LL] (https://twitter.com/TheXC3LL).

28 de outubro de 2019

Sobre nós

A Linux Force Brasil é uma empresa que ama a arte de ensinar. Nossa missão é criar talentos para a área de tecnologia e atender com excelência nossos clientes.

CNPJ: 13.299.207/0001-50
SAC:         0800 721 7901

[email protected]

Comercial  Comercial: (11) 3796-5900

Suporte:    (11) 3796-5900
[email protected]

Copyright © Linux Force Security  - Desde 2011.