CVE-2022-26352
“Exploring a CVE…”
Introdução
Essa CVE representa uma falha de upload de arquivos em um CMS chamado DotCMS. A falha foi identificada e reportada incialmente pelo pesquisador shubs.
Objetivo
A ideia deste artigo é replicar essa CVE tendo o mínimo de informação possível.
O intuito é chegar o mais próximo possível do processo de identificar um Zero Day
.
Informações
As principais informações que obtive foram:
- A vulnerabilidade era um file upload + path traversal;
- Versão do software vulnerável;
- Programa open source escrito em Java.
Metodologia
Neste caso em específico como eu já tinha um conhecimento prévio da vulnerabilidade que eu deveria explorar, decidi então focar em sinks que pudessem estar relacionados a upload de arquivos em Java.
Mapeamento de rotas
Antes de mais nada é necessário entender a estrutura de uma aplicação, um bom começo
para esse entendimento é um arquivo chamado web.xml
que contém as definições
entre as classes e as suas respectivas rotas na url. Outro ponto importante é
de entender a estrutura da linguagem em questão, para identificar onde está a
definição de cada rota.
O código seguinte representa o arquivo web.xml que contém as definições de rotas de uma aplicação Java.
Ou seja todas as rotas de API começam com /api
.
<servlet-mapping>
<servlet-name>RESTAPI</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
Estrutura de rotas
Como essa aplicação utiliza o pacote javax.ws.rs
, podemos assumir que
o padrão de rotas pode ser identificado como: @Path
. Mas isso só foi
possível de entender depois de ler e ver alguns exemplos da especificação JAX-RS.
No java as rotas de API também podem estar uma dentro da outra, como neste exemplo.
A rota /all/params
só é acessada se eu passar primeiro o /license
na url obedecendo a hierarquia.
A rota final fica assim: http://127.0.0.1:8080/api/license/all/{params}
@Path("/license")
public class LicenseResource {
private final WebResource webResource = new WebResource();
private static final String SERVER_ID = "serverid";
@NoCache
@GET
@Path("/all/{params:.*}")
...
Autenticação
Do ponto de vista de segurança, é interessante saber identificar as rotas que possuem ou não autenticação. O código seguinte representa quando uma rota é autenticada.
final InitDataObject initData = new WebResource.InitBuilder(webResource)
.requiredBackendUser(true)
.requiredFrontendUser(false)
.params(params)
.requestAndResponse(request, response)
.rejectWhenNoUser(true).init();
Procurando por upload de arquivos
O primeiro passo foi utilizar uma tool chamada ripgrep para realizar uma busca recursiva nos diretórios da aplicação. Buscando por uma lista de palavras que estão relacionadas a manipulação de arquivos em java, faz com que nossa busca seja mais acertiva.
Lista comum:
- “multipart”,
- “InputStream”,
- “getPart”,
- “formData”,
- “FileUtils”
- “getAbsolutePath”
Depois de percorrer a lista utilizando grep, notei que mesmo com o número de arquivos reduzidos ainda eram muitos para serem analisados um a um. =/
Ai que vem o pulo do gato… Sabendo que a aplicação utiliza javax para a criação das rotas de API. Resolvi então buscar exemplos de como é feito upload na especificação do JAX-RS.
Depois de algum tempo pesquisando, eu decidi que a palavra “MULTIPART_FORM_DATA” pode ser uma boa, pois é utilizada em vários exemplos de upload de arquivo. Agora a minha pesquisa retornou apenas 10 arquivos distintos. O que é plausível de analisar um a um do ponto de vista de code review.
O comando utilizado foi esse: rg -B 3 -A 3 -i 'MULTIPART_FORM_DATA' -g '*.java'
O grep
na imagem abaixo foi somente para questões de print e ter uma melhor visualização`.
Ponto de entrada
Analisando arquivo por arquivo, encotrei um trecho de código MUITO interessante!
Esse trecho de código abaixo foi identificado no arquivo: dotCMS/src/main/java/com/dotcms/rest/ContentResource.java.
@Deprecated
@POST
@Path("/{params:.*}")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response multipartPOST(@Context HttpServletRequest request,
@Context HttpServletResponse response,
FormDataMultiPart multipart, @PathParam("params") String params)
throws URISyntaxException, DotDataException {
return multipartPUTandPOST(request, response, multipart, params, "POST");
}
... trecho omitido ....
final InitDataObject init = new WebResource.InitBuilder(request, response)
.requiredAnonAccess(AnonymousAccess.WRITE)
.params(params)
.init();
Basicamente, esse trecho de código diz que é possível o poder de escrita como “Anonymous” ou seja. Não é necessário estar logado para acessar essa rota.
Encontrando o sinkhole
Agora que existe um potencial caminho para realizar um upload de arquivo sem estar autenticado. Continuei analisando o código para entender o que era possível ser feito nessa rota.
Observando o código, existem algumas condicionais “IF/ELSE”, para verificar o tipo de arquivo que estava
sendo enviado e para cada tipo uma ação diferente é realizada. Mas ao passar pelo trecho de código abaixo
consegui notar que quando o upload é feito em “PLAIN TEXT”, a função processFile
é chamada.
} else if(mediaType.equals(MediaType.TEXT_PLAIN_TYPE)) {
try {
map.put(name, part.getEntityAs(String.class));
processMap( contentlet, map );
if(null != contentDisposition && UtilMethods.isSet(contentDisposition.getFileName())){
processFile(contentlet, usedBinaryFields, binaryFields, part);
Ao procurar pela definição dessa função, é possível notar que o filename
é concatenado
diretamente com o caminho da aplicação onde o arquivo será salvo. Então se utilizar um path traversal
no nome do arquivo é possível controlar o local onde o será salvo.
private void processFile(final Contentlet contentlet,
final List<String> usedBinaryFields,
final List<String> binaryFields,
final BodyPart part) throws IOException, DotSecurityException, DotDataException {
final InputStream input = part.getEntityAs(InputStream.class);
final String filename = part.getContentDisposition().getFileName();
final File tmpFolder = new File(APILocator.getFileAssetAPI().getRealAssetPathTmpBinary() + UUIDUtil.uuid());
if(!tmpFolder.mkdirs()) {
throw new IOException("Unable to create temp folder to save binaries");
}
final File tempFile = new File(
tmpFolder.getAbsolutePath() + File.separator + filename);
Files.deleteIfExists(tempFile.toPath());
Explorando a vulnerabilidade
Como não existe nenhuma validação com o nome do arquivo a primeira ideia é tentar enviar um arquivo jsp
para conseguir execução remota de código na máquina alvo.
Agora que a parte principal foi identificada, basta apenas montar a request e enviar para o servidor. O que eu fiz para validar se realmente a exploração iria funcionar foi subir a aplicação local em um container docker para validar.
A parte que literalmente fiquei travado foi ter recebido um erro 500
.
E por muito tempo eu fiquei achando que não estava funcionando o meu payload
ou que a aplicação estava quebrada.
Mesmo com o erro 500 na aplicação o payload funcionou. Na imagem abaixo um poc da webshell executando na aplicação.
Conclusão
O processo de pegar uma CVE e tentar replicar com o mínimo de informações possíveis, acaba se transformando em uma forma de estudos. Isso não se limita ao contexto web, podendo ser utilizado para divesas outras áreas do hacking.
Referências
Posts:
- Post Board parte 2
- Post Board parte 1
- Guess Me Parte 2
- Guess Me Parte 1
- Food Store
- IOT Connect
- UnCrackable L1 Parte 2
- UnCrackable L1 Parte 1
- CVE-2022-26352
- DCA php
- Docker Code Analyzer
- Vulnado Parte 3
- Vulnado Parte 2
- Vulnado Parte 1
- Damm Vulnerable WebSocket
- OWASP ZAP Zed Attack Proxy
- Estratégias de um code review
- O que é code review?