
Este repositório contém os resultados da minha pesquisa de agosto de 2020 sobre o firmware de IPC/NVR da Tiandy. Encontrei duas vulnerabilidades que poderiam ser usadas para recuperar remotamente a senha do administrador e obter acesso root ao dispositivo.
Este repositório contém os resultados da minha pesquisa de agosto de 2020 sobre o firmware IPC/NVR da Tiandy (esses dispositivos também são vendidos como OMNY). Esta "pesquisa" não foi exaustiva, mas encontrei vários métodos para recuperar remotamente a senha do administrador, ativar o telnet e alterar a senha do root.
É difícil dizer exatamente quais versões são afetadas, já que só podemos baixar as recentes. Todas estas versões para download são afetadas:
DVRS_V9.12.7.20200422
DVRS_V11.7.4.20200721
NVSS_V13.6.1.20200723
NVSS_V22.1.0.20200722
Não só existem diferentes ramos para diferentes dispositivos, mas alguns componentes são versionados e atualizados separadamente, como a API web, onde encontrei um bypass de autenticação que só funciona em versões lançadas desde meados de 2019, independentemente do número da versão do firmware. Se por acaso você souber mais sobre as versões afetadas, agradeço sua ajuda.
Estou fazendo divulgação completa aqui, mas é razoável. Um patch do fornecedor (improvável, já que eles não respondem) não faria o problema desaparecer, especialmente quando nenhum dispositivo online tem o firmware mais recente (nem de longe). A vulnerabilidade real é que esses dispositivos estão expostos à internet. E isso é algo que os usuários finais precisam corrigir, não a Tiandy.
Como bônus, também estou incluindo o descompactador de firmware e algumas informações sobre como acessar os fluxos via RTSP/RTMP (boa sorte tentando encontrar isso no manual).
Primeiramente apresento os scripts:
Em seguida, tento explicar brevemente o que esses scripts fazem e porquê. Não repito o código, mas tento explicar contexto suficiente para você entender o código:
Por fim, a coisa fica relativamente técnica:
Você precisará de Python 3 com PyCrypto.
Primeiro, tente recover.py. Isso requer que a porta 3001 esteja acessível:
python3 recover.py [HOST]
se tudo correr bem, as credenciais do administrador devem ser impressas.
Se essa porta não estiver acessível, a da web pode funcionar. Isso requer a url:
python3 cgi_recover.py http://123.45.67.89
python3 cgi_recover.py https://123.45.67.89
Se nenhum dos métodos acima funcionar, verifique se o telnet está habilitado. Se estiver, você pode obter root no dispositivo diretamente, basta quebrar este hash:
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(abra um PR caso consiga quebrá-lo :D)
Em firmwares NVR V7 mais antigos, você pode executar comandos diretamente:
python3 ftpupdate.py [host] [adminpass] '[cmd]'
mas não dá nenhuma saída. Para facilitar, incluí este atalho:
python3 ftpupdate.py [host] [adminpass] adduser [username] [password]
Isso adicionará outro usuário com uid 0.
Primeiro, habilite o telnet usando:
python3 telnet.py [host] [adminpw]
ou para dispositivos recentes:
python3 cgi_recover.py [host] telnet
Então você pode sobrescrever /etc/passwd (presumo que você saiba como isso funciona). Tente filetransport.py primeiro (para NVRs):
python3 filetransport.py [host] [adminpass] put /etc/passwd <[source_file]
isso não dá nenhum retorno; você precisa testar tentando fazer login...
Para modelos IPC onde filetransport.py não funciona, tente upgrade_rw.py:
python3 upgrade_rw.py [url] [adminpass] /etc/passwd <[source_file]
(isso também não dá nenhum retorno)
Por fim, para dispositivos ainda mais novos, isso também pode ser feito pela API web:
python3 cgi_recover.py [url] write /etc/passwd <[source_file]
Se nenhum dos métodos acima funcionou (verifique se você consegue fazer login), tente novamente todos esses métodos, mas sobrescreva /config/etc/passwd em vez disso. Em algumas versões do firmware, /etc/passwd é um symlink para esse caminho. Por fim, você também pode tentar sobrescrever /tdfs/etc/passwd, mas depois disso pode ser necessário reiniciar o dispositivo; para reiniciar, use:
python3 reboot.py [host] [adminpass]
O firmware antigo V7 (IPC e NVR) não parece ser afetado, mas se você tiver a senha do administrador, para NVRs existe um RCE autenticado (ftpupdate.py), e para IPCs, o script upgrade-rw.py pode ser usado para sobrescrever /etc/passwd.
Versões posteriores do firmware NVR (V9 e V11) têm uma conta padrão que, combinada com escalonamento de privilégios "passivo", torna possível recuperar a senha do administrador. Podemos então sobrescrever /etc/passwd usando filetransport.py.
Embora a conta padrão não esteja presente no firmware IPC, outro método de recuperação aparece: o método PSW. Este é um mecanismo de recuperação de senha sem segurança alguma. Está presente em todas as versões de firmware para download desde o V9. Enquanto filetransport.py só funciona em NVRs, upgrade_rw.py alcança o mesmo propósito em IPCs usando o mecanismo de atualização, para que ainda possamos obter o acesso root.
O firmware de 2019 introduz outro vetor de ataque: um bypass de autenticação usando a API web. Ao exportar o arquivo de configuração sem autenticação, podemos recuperar a senha e preparar um pacote de atualização para sobrescrever arquivos arbitrários.
Falando em vulnerabilidades, existem 4 delas:
Observe que não investiguei os recursos "cloud", ou seja, se é possível enumerar dispositivos e, portanto, conectar-se a dispositivos não expostos à internet (como acontece com dispositivos Xiongmai).
Em versões antigas, o telnet é habilitado por padrão e é isto que podemos encontrar no arquivo /etc/passwd:
support:$1$$AErA9BQgLjrxTJB1748k71:501:501:Linux User,,,:/home/support:/bin/sh
(a senha do root é atualizada dinamicamente; também não quebrei este hash, então pull requests são mais que bem-vindos :D)
O usuário support (na verdade presente em todas as versões do firmware) pode parecer sem privilégios, mas, é claro, esse usuário tem privilégios suficientes para ler a senha do Admin e sobrescrever scripts de inicialização graváveis por todos em /etc/init.d ou até mesmo criar novos :)
Conceitualmente, o método é realmente simples. Apenas enviamos um pacote de login e lemos a resposta. Só isso, porque a resposta de "login bem-sucedido" contém as credenciais de todos os usuários, independentemente dos nossos privilégios. Embora já fosse assim no V7, na prática esse método só se tornou útil quando a conta padrão foi introduzida no firmware NVR. A "Default" irremovível não tem privilégios remotos, então você não pode fazer nada com ela. Bem, talvez exceto ler a senha do administrador...
Embora pareça trivial, não foi tão trivial de implementar. Um protocolo personalizado é usado para comunicação, e as senhas são criptografadas usando DES, mas com os bits invertidos (a parte mais difícil foi descobrir isso), com uma chave transmitida pelo servidor. Como não há derivação de chave, um bisbilhoteiro poderia facilmente descriptografar tudo. Ainda assim, é preciso descobrir que os bits estão invertidos ou reimplementar tudo do zero...
Veja a função recover_with_default no arquivo recover.py para a implementação.
Ao analisar o binário, é difícil não notar esse mecanismo. Seu único propósito é... tornar possível a recuperação de senha, e ele realmente faz isso bem. Bem demais, eu diria...
O que está acontecendo aqui? Acho que isso era para ser um mecanismo de recuperação de senha, presumivelmente criado para que os fornecedores pudessem fornecer uma maneira de os proprietários de dispositivos recuperarem suas senhas.
Posso supor que o fluxo deveria ser assim:
Tudo isso é ótimo, exceto que falta uma coisa... onde está a segurança? Em lugar nenhum, ao que parece. Não há nada que nos impeça de enviar esse pacote, derivar o código de segurança e recuperar a senha de qualquer dispositivo acessível.
Acho isso surpreendente, porque não é que haja alguma falha no mecanismo que anule a segurança. Simplesmente não há segurança alguma. Não há nada para corrigir, mas isso também não me parece uma backdoor óbvia. Isso deixa rastros nos logs e tem 3 esquemas diferentes de derivação, cada um mais sofisticado que o anterior. Isso realmente levou tempo para implementar...
Voltando ao método: para que isso funcione, o dispositivo precisa ter um telefone/e-mail associado. Olhando a versão mais antiga, vi que apenas o administrador pode fazer isso, mas então encontrei um bypass. Surpreendentemente, no firmware mais novo, esse bypass não é mais necessário, pois é explicitamente possível alterar o e-mail do dispositivo sem autenticação. Agora, é aqui que acho que a backdoor deveria estar :)
No lado técnico, esse mecanismo é bastante complicado e foi o mais difícil de fazer engenharia reversa e reimplementar. Existem 3 versões desse mecanismo, cada uma usa um algoritmo diferente para derivar o código de segurança. Além do DES com bits invertidos, uma cifra de substituição personalizada com uma chave fixa está envolvida. Mas tudo isso é em vão, porque a Tiandy não consegue tornar segura uma criptografia simétrica com chave fixa, por mais que tente.
Uma coisa que observei é que, como o código de segurança muda a cada minuto, há a possibilidade de o processo original falhar simplesmente porque o código mudou entre o pacote do passo 3 e o do passo 5, não importa quão pouco tempo tenha passado entre o envio deles. Levei isso em consideração, então meu script tenta o processo novamente se o código se mostrar inválido.
Todo o processo, incluindo definir o e-mail (que não precisamos possuir), está implementado no arquivo recover.py.
Este funciona com versões de firmware mais recentes (2019 e posteriores) que têm a interface web "moderna" (aquela com o "mapa". Gosto desse mapa, embora a Austrália pareça um pouco distorcida).
Este bypass é simples. Embora a maioria dos endpoints da API seja autenticada, há algumas exceções. No entanto, a verificação de se deve pular a autenticação e a correspondência de qual endpoint ativar são implementadas em lugares diferentes. Na maioria dos casos, a autenticação é ignorada quando o caminho da URL é igual a uma determinada string, o que é seguro.
Mas versões recentes introduzem outra exceção, que é ativada quando as strings Record/DownLoad e ID= simplesmente existem em algum lugar no caminho da URL.
Agora, para endpoints onde o caminho completo é comparado, isso ainda é seguro. Como Security/users é um desses endpoints, não podemos recuperar a senha diretamente. Para nossa sorte, o endpoint de exportação de configuração é selecionado verificando se o caminho da URL começa com uma determinada string (usando strncmp), então podemos simplesmente adicionar essas strings ao caminho e exportar o arquivo de configuração.
Recuperar a senha usando essa falha está implementado em cgi_recover.py.
Teoricamente, o administrador pode atualizar o firmware, e o firmware não é assinado nem criptografado. Mas será que realmente precisamos preparar um pacote de firmware personalizado? Às vezes não. Às vezes sim, e eu fui louco o suficiente para realmente implementá-lo...
Se você tiver a senha, há uma vulnerabilidade de injeção de comandos que você pode usar, o script ftpupdate.py. O que acontece é que dizemos ao dispositivo para buscar uma atualização via ftp e ftpget é usado para executar essa tarefa. Como era de se esperar, nossos parâmetros vão diretamente para a função system().
No firmware NVR, o protocolo binário tem o comando FILETRANSPORT, que faz exatamente o que o nome diz. Na verdade, não há mais nada a dizer, porque você poderia simplesmente baixar o SDK e usar o mesmo comando. Claro que eu quis reimplementá-lo, então para ver como isso funciona, olhe filetransport.py.
Modelos IPC não têm esse comando, porém, como eu disse antes, sempre podemos atualizar o firmware. Embora preparar toda a flash seja impraticável, os pacotes de atualização da Tiandy nos permitem substituir arquivos individuais, que é exatamente o que precisamos (veja descompactação do firmware).
Exceto que não é tão simples. O formato de arquivo "box" tem alguns metadados e depois uma matriz de arquivos. O primeiro arquivo deve ser nomeado ProductModule e deve conter parâmetros de dispositivo correspondentes, caso contrário a atualização não prosseguirá. Precisamos não apenas dos valores desses parâmetros, mas também dos próprios parâmetros. Além disso, a parte dos metadados (incluindo a versão do arquivo box) também é verificada.
Montar isso manualmente parece impraticável, mas felizmente há outro caminho. As exportações de arquivo de configuração usam o mesmo formato de arquivo "box", com todos os metadados correspondentes e o arquivo ProductModule incluído, exceto o campo de tipo de atualização.
Consegui descobrir como preencher esse campo e, portanto, implementar esse processo. Embora o mecanismo também esteja presente no firmware NVR, ele não funciona da mesma forma; no entanto, não faz sentido analisar mais, já que os NVRs têm o comando FILETRANSPORT descrito anteriormente.
O script upgrade_rw.py faz uso do processo de atualização. O nome sugere algo mais, porém... Isso porque, ao exportar o arquivo de configuração, especificamos quais arquivos exportar pelo nome e, como era de se esperar, qualquer arquivo funciona, então podemos baixar o box e então ler esse arquivo usando o mesmo código usado para extrair o firmware.
Tanto a exportação quanto a atualização também podem ser feitas via API web. Nesse caso, não é possível ler arquivos arbitrários. Ainda assim, quis implementar isso porque a API parece mais estável; veja cgi_recover.py.
Em modelos que suportam FTP, pode ser possível injetar comandos shell na senha do usuário (quando um comando é executado e adiciona esse usuário para que ele possa fazer login via FTP).
Dispositivos Tiandy têm a porta 3001 aberta. Essa é a porta necessária para executar os métodos não-web. Modelos mais novos têm RTSP na porta 9100 além da porta 554, e também têm RTMP na porta 1935. Modelos IPC usam a porta 8082 para ONVIF. HTTP e HTTPS rodam nas portas padrão.
Dispositivos com a interface web antiga (usando nossa amada tecnologia ActiveX) contêm um dos seguintes em sua resposta HTTP:
<title>Net Video Browser</title>
tdvideo.css
Dispositivos com a interface web mais nova (desta vez usando... Flash) contêm isto na resposta:
res/app-0.1.0.css
(o cabeçalho Last-Modified fica feliz em nos revelar a data exata do lançamento)
Também podemos identificar esses dispositivos pelo certificado, embora HTTPS nem sempre esteja habilitado. Existem apenas dois certificados usados em todos os dispositivos e eles podem ser encontrados no firmware baixado, o que torna inútil o pinning deles.
O mais antigo:
C=CN, ST=Tianjin, O=Tiandy Tech, CN=dvr_ui
O mais novo:
C=CN, ST=Tianjin, L=Tianjin, O=Tiandy Tech Ltd, CN=NetDevice
A propósito... o suporte a HTTPS é implementado via um processo stunnel separado. Isso funciona, mas como era de se esperar, o endereço IP se perde no processo, então os logs sempre dizem 127.0.0.1.
A Tiandy afirma que seus dispositivos suportam RTSP e RTMP. Isso é legal, mas o que não encontramos no manual é como realmente usar esses protocolos, porque não encontramos as URLs RTSP e RTMP necessárias. Felizmente, tenho essa informação como subproduto da análise, então posso compartilhá-la.
Para NVR:
Para ver o stream ao vivo do canal C (começando em 1) com o tipo de stream S (1, 2, 3):
rtsp://username:password@host/C/S
Para IPC:
Para ver o stream ao vivo com o tipo de stream S:
rtsp://username:password@host/S
As URLs RTMP não são tão simples porque exigem um hash personalizado para que a solicitação possa ser autenticada. No entanto, o RTMP também nos permite reproduzir o conteúdo gravado.
A url para o stream ao vivo é:
rtmp://host/live/C/S/authstring
onde C é o canal, S é o tipo de stream.
A url para reprodução é:
rtmp://host/vod/START-STOP/C/S/authstring
onde START e STOP são timestamps unix.
authstring é calculada da seguinte forma:
base64("username:"+md5("username:password")+":unix_timestamp")
A ferramenta rtmpauth.py pode gerá-la:
python3 rtmpauth.py username password
Como esse timestamp é verificado e a diferença não pode ser maior que 2 dias, há limitações:
Atualizações de firmware são empacotadas em um formato de arquivo "box" proprietário que não é assinado nem criptografado. Esse formato é na verdade muito simples do ponto de vista do descompactador. Há um cabeçalho que ignoramos e uma matriz de arquivos para descompactar, onde cada arquivo tem um cabeçalho de tamanho fixo contendo nome do arquivo e tamanho (duas vezes), e então os dados seguem.
A ferramenta unbox.py descompacta o arquivo em um diretório com o nome do arquivo box ou no diretório especificado:
python3 unbox.py [box_file]
python3 unbox.py [box_file] [target_dir]
Esta ferramenta deve ser segura de usar (escrevi esta linha, depois verifiquei a ferramenta novamente e encontrei uma vulnerabilidade... ops) porque caminhos absolutos são transformados em relativos, .. é substituído por __ e não há symlinks.
Às vezes você precisará executar esta ferramenta duas vezes, pois notará que o arquivo dentro de um .box é outro .box.