Code FightersTech

Introdução a secure code review em aplicações Go

Antes de entrarmos, de fato, à nossa introdução a secure code review em aplicações Go, vamos a uma breve contextualização.

Eis que temos uma aplicação ou módulo escrito na linguagem Go, e queremos analisá-la. Como começamos a fazer isso? O objetivo desse blogpost é servir como introdução para pentesters ou pesquisadores de segurança em como analisar um aplicações Go, procurando por vulnerabilidades. Aqui será destacado o caminho que eu fiz em minha própria jornada, durante um projeto piloto de vulnerability research, que foi feito analisando Traefik, um proxy reverso e load balancer.

A parte mais interessante da metodologia está na análise manual de dependências em Go, que foi meu foco principal durante esse processo. Veremos como procurar por low-hanging fruits em módulos Go, e como utilizar as dependências da aplicação para ter um entendimento melhor sobre a aplicação em si. Essa metodologia foi feita por meio do consumo de muitas referências no tópico, e lendo bastante da documentação da linguagem Go.

Todas as referências poderão ser encontradas no fim desse artigo. Temos aqui a palavra “introdução” porque desejamos que essa seja a primeira parte de uma metodologia que será melhorada continuamente, durante futuros projetos de vulnerability research feitos na mesma linguagem. Então mergulharemos mais fundo em blogposts futuros.

Leia também: Dicas e truques para Pentest em API

Visão geral da metodologia

Aqui teremos uma abordagem vendo a aplicação por uma perspectiva de alto nível. Não desceremos em detalhes específicos sobre exploração de vulnerabilidades em Go. Faremos os seguintes passos para a nossa análise:

  1. Entendendo a estrutura do código;
  2. Ferramentas de análise estática para encontrar warnings relevantes e low-hanging fruits;
  3. Mapeamento do uso de módulos;
  4. Descendo no código dos módulos;
  5. Procurando vulnerabilidades dentro dos módulos;
  6. Analisando sources e sinks, através dos módulos da aplicação.

Um pequeno adendo aqui: isso foi feito por um pesquisador que não é um desenvolvedor especialista em Go, e também é feito para ser acessível àqueles que não tem necessariamente um nível avançado na linguagem, mas pelo menos tem um nível intermediário sobre como um código funciona, em qualquer linguagem.

1. Entendendo a estrutura do código

Cada base de código é uma criaturinha especial. Mesmo Go sendo uma linguagem que “força” algumas boas práticas de código por definição, encontramos várias diferenças estruturais em cada base de código a ser analisada.

A melhor forma de entender um código é se colocar no lugar de um possível contribuidor, ou desenvolvedor do projeto. Então comece como se você mesma estivesse buscando implementar uma feature ou corrigir algum bug no projeto, o que pode virar realidade se você encontrar uma vulnerabilidade em um projeto open-source.

Se você quer contribuir para um projeto, a primeira coisa a ser feita é entender o seu contexto. Busque responder para si as seguintes perguntas:

  • Qual é o objetivo desse projeto?
  • Ele está sendo desenvolvido continuamente?
  • Quantas pessoas estão ativamente envolvidas no projeto?
  • O que a aplicação faz na prática?

Essas questões levarão você a um entendimento melhor do objetivo geral da aplicação, e também fazer com que seja mais fácil visualizar que possíveis vulnerabilidades você deve procurar.

Depois de responder essas questões em uma visão de mais alto nível, vá para a parte da documentação do projeto onde se diz “Contributing”. Grande parte dos projetos terão algo parecido com isso em um arquivo README.md :

Abra e consuma toda documentação possível sobre como contribuir com o código da aplicação. Aqui possivelmente encontraremos algumas informações sobre:

  • Boas práticas de código utilizadas no projeto;
  • Como o processo de revisão de pull requests está sendo feito;
  • Como vulnerabilidades e problemas de segurança são tratadas por quem mantém o código;
  • Como rodar os testes, e quais testes estão sendo rodados para evitar que erros sejam cometidos por contribuições novas no código.

Essas são as informações mais úteis que poderemos obter nessa etapa. Se o projeto tiver testes para ser rodados, rode eles!

Em projetos escritos em Go, a maior parte dos testes rodam com um simples comando: go test . Em projetos mais complexos podem haver testes de integração ou outros testes além de apenas testes unitários, que podem ser rodados com algum Makefile. Mas esses detalhes serão encontrados na parte de “Contributing” da documentação.

Mais informações sobre testes em Go podem ser encontradas aqui.

Não precisamos saber de todos os detalhes sórdidos da implementação dos testes, nem ao menos da aplicação em si, pelo menos nessa etapa. Aqui queremos apenas um entendimento de alto nível sobre a estrutura do código e o contexto da aplicação.

2. Low-hanging fruits e análise estática

Agora que conhecemos o terreno onde estamos entrando, utilizaremos algumas ferramentas para procurar por low-hanging fruits na aplicação. Low-hanging fruit é o nome que damos para vulnerabilidades que podem ser encontradas “sem muito esforço”, ou seja, sem analisarmos profundamente o código.

Essa etapa é importante e possivelmente é a que tomará mais tempo de quem estiver fazendo um projeto de pentest ou auditoria de código para encontrar riscos e vulnerabilidades em largura, e não profundidade.

No caso de você que está trabalhando com pesquisa, e tem como foco encontrar 0-days em um código em Go, essas ferramentas (provavelmente) não jogarão 0-days para a sua tela, mas poderão ajudar a ter uma melhor visibilidade sobre o esforço que está sendo colocado na segurança da aplicação. Algumas ferramentas também podem ajudar a encontrar caminhos e padrões para exploração de vulnerabilidades mais críticas.

Há uma vastidão de analisadores estáticos open-source com foco em segurança para aplicações em Go. Aqui destacamos Gosec e Gokart. Essas duas ferramentas focam especificamente em análise estática para segurança.

staticcheck e go vet são analisadores estáticos que não tem foco específico em segurança, mas podem nos dar algumas warnings que podem ajudar a encontrar outras potenciais vulnerabilidades, ou ao menos dar um entendimento melhor sobre a aplicação.

Agora, Semgrep, um analisador estático open-source focado em segurança e escalabilidade. É muito rápido e tem um conjunto de regras imenso mantido pela comunidade. Não é um analisado específico para Go, mas há alguns pacotes de regras especificas para a linguagem.

Destaco aqui dois conjuntos de regras para Go: p/golang e p/trailofbits . Exemplo: para rodar o semgrep com p/golang , navegue até a pasta onde está o código a ser analisado, e rode:

docker run --rm -v "${PWD}:/src" returntocorp/semgrep --config 'p/golang'

E teremos como output da ferramenta alguns erros ou warnings, como:

integration/access_log_test.go
severity:warning rule:go.lang.security.audit.crypto.use_of_weak_crypto.use-of-md5: Detected MD5 hash algorithm which is considered insecure. MD5 is not collision resistant and is therefore not suitable as a cryptographic signature. Use SHA256 or SHA3 instead.
272:	digest := md5.New()

CodeQL é outra ótima ferramenta que pode ser usada para essa análise, e merece um blog post inteiro sobre como analisar e criar queries personalizadas. Você pode encontrar mais sobre como rodar ela para um projeto em Go no link abaixo:

https://github.com/github/codeql-go

Tome um tempo para rodar essas ferramentas e revisar o output delas. Vale ressaltar que o esforço nessa etapa deve ser proporcional ao quanto ela encaixa no seu objetivo. Poderemos usar essa etapa novamente para analisar os módulos utilizados pela aplicação separadamente.

3. Mapeando o uso dos módulos

Reuso de código e dependência de bibliotecas em uma aplicação em Go é feito utilizando o que chamamos de “Go Modules”, que no caso chamaremos apenas de módulos. Os módulos em o mesmo objetivo que um pacote python, ruby gem, módulo em node, etc. Desde a versão 1.11 do Go, lançada em agosto de 2018, podemos encontrar os módulos Go utilizados no arquivo go.mod . Tomemos o seguinte arquivo como exemplo:

Na linha 1, module diz o nome do módulo que é nossa aplicação, nesse caso estamos utilizando Traefik como exemplo. Na linha 3 temos a especificação de versão do Go. E finalmente a palavra require na linha 6, seguida por uma lista de módulos dos quais a aplicação depende.

Go utiliza versionamento semântico de módulos, nós temos o nome do repositório, e a versão que está sendo utilizada como dependência. Voltaremos nesse arquivo depois.

Nosso objetivo aqui é identificar como e quais módulos estão sendo utilizados pela aplicação. Quando abrimos um arquivo de código em Go (que levam a extensão .go ) encontramos algo como o seguinte no início do arquivo:

A linha 1 define o nome do pacote, que é algo como um namespace, e a linha 3 contém a palavra import , que é seguida pelos módulos que estão sendo utilizados nesse arquivo.

É claro e explícito qual módulo está sendo utilizado, pois o identificador é o nome do repositório do módulo em si, a mesma coisa que podemos encontrar no arquivo go.mod .

Agora, nos arquivos que estamos analisando, veremos onde esse módulo está sendo chamado. No nosso exemplo temos algo assim: dns.AlgumaCoisa

Anote que tipos e funções estão sendo referenciados de cada módulo Go. Não é necessário anotar absolutamente todo uso de todo módulo, mas principalmente dos que parecem relevantes para os seus objetivos nessa análise.

Depois das anotações, procure na documentação do módulo o que as funções e tipos encontradas fazem. Isso gerará um entendimento mais profundo sobre a implementação da aplicação, e será de grande ajuda quando formos analisar os módulos em si.

Grande parte dos módulos em Go tem a documentação publicada no link abaixo:

https://pkg.go.dev/

Você pode procurar ali, ou em alguns casos podemos apenas inserir o nome do repositório ao fim da URL: https://pkg.go.dev/<repo_name> . No nosso exemplo seria isso: https://pkg.go.dev/github.com/miekg/dns

Para mais informação sobre módulos em Go:

https://go.dev/blog/using-go-modules

4. Descendo ao código dos módulos

Há várias formas de obter-se o código fonte dos módulos que a aplicação está utilizando.

Uma é ir manualmente ao repositório do módulo, mas nesse caso teríamos que procurar pela versão específica que está sendo utilizada no seu arquivo `go.mod`. Isso porque se utilizarmos diretamente o código que está na branch master, é possível que nosso código seja diferente do que está sendo utilizado pela aplicação.

Outras formas incluem utilizar o próprio utilitário da linguagem Go. Quando rodamos o comando go build ou go mod download , ele baixará os módulos necessários dentro do nosso caminho $GOPATH , que está setado nas variáveis de ambiente.

O código fonte dos módulos estará em $GOPATH/pkg/mod , ou em $GOPATH/pkg/mod/cache , mas guardado em arquivos .zip .

Seguindo o nosso exemplo anterior do módulo miekg/dns , ele se encontraria no seguinte diretório: $GOPATH/pkg/mod/github.com/miekg/dns\\@v1.1.40/ .

Isso significa que nesse caminho de diretórios encontraremos a versão 1.1.40 do módulo, que está sendo utilizado na nossa aplicação. Caso você não queira encher sua máquina com o código da aplicação que você está analisando, você pode utilizar um container docker para copiá-los da seguinte forma:

# run this docker command inside the source code root directory
docker run -v "${PWD}:/src" -w /src -ti --entrypoint /bin/bash golang:1.17
# now inside the container, download the modules specifying the GOPATH
GOPATH=/src/my_modules go mod download
rm -rf my_modules/pkg/mod/cache
mv my_modules/pkg/mod my_modules 
rm -rf my_modules/pkg/

Depois de rodar esses comandos dentro da pasta onde se encontra o código da sua aplicação, você terá uma versão mais limpa dos módulos dentro da pasta my_modules .

Agora você deve ter as anotações sobre os módulos que são interessantes para o seu projeto, que foram coletadas no passo 3. , e a versão específica de cada módulo para nossa diversão.

Documentação sobre outros comandos que manipulam e leem o arquivo go.mod pode ser encontrada aqui:

https://golang.org/ref/mod

Copie as pastas que contém o código dos módulos que são interessantes para os seus objetivos, e que são relevantes para ter o código analisado um pouco mais profundamente. Não é viável analisar o suficiente todos os módulos baixados, principalmente porque alguns deles são módulos padrão da própria linguagem.

Depois de ter os módulos devidamente separados, procure por mais vulnerabilidades.

5. Procurando por vulnerabilidades dentro dos módulos

A primeira coisa que devemos fazer agora é buscar por vulnerabilidades já reportadas e corrigidas para cada módulo que está sendo analisado. Temos dois objetivos fazendo isso aqui:

  • Verificar se a aplicação está utilizando módulos em versões vulneráveis;
  • Usar essas vulnerabilidades para estudar padrões vulneráveis no módulo;

Snyk tem uma boa base de dados de vulnerabilidades, separada por módulos, onde podemos encontrar o exato pull requests, issue criada e correção para cada vulnerabilidade.

Anote os padrões encontrados, e se possível tente verificar se há algum jeito de burlar a correção feita no código em vulnerabilidades já corrigidas.

Se a aplicação utiliza alguma versão vulnerável de algum módulo, anote isso também.

Utilizar um módulo vulnerável não significa necessariamente que a aplicação em si está vulnerável. Para que isso seja verdade, é necessário que a vulnerabilidade no módulo possa ser alcançada através do seu uso pela aplicação. Para verificar isso, as anotações do passo 3. são muito úteis.

Repita o passo 2. para cada módulo. Utilizando análise estática é possível encontrar erros e warnings que podem potencialmente ser vulnerabilidades, se a aplicação estiver alcançando aquele ponto vulnerável do módulo, e pode também dar um melhor entendimento sobre o código do módulo.

Se o objetivo aqui é encontrar zero-days na aplicação, use a base de conhecimento criada no estudo de vulnerabilidades já corrigidas, para encontrar outros padrões, ou mesmo tentar bypassar as correções dessas vulnerabilidades.

Nós não entraremos em detalhes em “como encontrar zero-days em Go”, porque não é o objetivo desse blog post, mas próximos blogposts complementarão isso.

Se algo foi encontrado nessa etapa em algum módulo utilizado por essa aplicação, vá para o próximo.

6. Analisar sources e sinks, da aplicação para o módulo

Mesmo que tenhamos encontrar vulnerabilidades em algum módulo, ou que a aplicação esteja utilizando algum módulo em versão vulnerável, isso não significa necessariamente que a aplicação em si esteja vulnerável.

O primeiro passo para conseguir uma prova de conceito da vulnerabilidade, é mapear o que chamaremos aqui de “sources” e “sinks”. Falando brevemente, essa é a definição desses dois termos:

  • Source é por onde dados não confiáveis estão entrando, geralmente input de usuário.
  • Sink é a parte vulnerável do código onde esses dados podem alcançar.

Se temos um source através do qual o fluxo de dados pode alcançar um sink, temos aí uma potencial exploração da vulnerabilidade.

No nosso caso, podemos utilizar a documentação feita no passo 3. para nos ajudar a encontrar os sources, ou ao menos um caminho do meio para algum source, sendo que temos a nossa lista de funções que a aplicação está utilizando dos módulos analisados.

Se há um fluxo de dados que podemos controlar, e esses dados alcançam alguma função vulnerável de algum dos módulos Go, em conclusão temos uma vulnerabilidade e podemos trabalhar em desenvolver uma prova de conceito para ela.

Estudos em secure code review em aplicações Go: próximos passos

Espero que esse material sirva como base inicial para pessoas que não tem um conhecimento profundo em Go possam iniciar e acelerar seus projetos de Pentest ou pesquisa em novas bases de código feitas nessa linguagem.

Go hoje é uma linguagem na qual existem aplicações largamente utilizadas em projetos imensos, e iniciar uma pesquisa de vulnerabilidades em uma base de código dessa pode ser intimidador, e por isso uma metodologia é essencial.

Utilizem essa metodologia como base, pegando o que é útil para o seu contexto, e descartando o que não é, e desenvolva a sua própria em cima dela. No futuro, teremos mais blogposts que servirão de insumo para melhorar as suas metodologias de pesquisa.

Referências

https://traefik.io/

https://github.com/OWASP/Go-SCP

https://snyk.io/blog/golang-security-access-restriction-bypass-vulnerability-jwt/

https://semgrep.dev/docs/cheat-sheets/go-command-injection/

https://www.trailofbits.com/post/discovering-goroutine-leaks-with-semgrep

https://rules.sonarsource.com/go/

https://blog.shiftleft.io/how-to-review-code-for-vulnerabilities-1d017c21a695

https://www.roguesecurity.in/2021/02/20/methodology-for-secure-code-review/

Nova call to action
About author

Articles

Analista de Segurança de Informação que adora explorar códigos e segurança de baixo nível. Trabalha principalmente com pesquisa em vulnerabilidades na Conviso.
Related posts
Code FightersSem categoria

JSON WEB Tokens: Dicas e ataques para uma implementação segura

O JWT (JSON WEB Tokens) é um padrão aberto, documentado pela RFC-7519 que define como transmitir e…
Read more
PodcastTech

AppSec to Go: Confira entrevista com Felipe Salum, SRE da Apple

Você já se perguntou como funciona o time de SRE em uma Big Tech? Este é o tema do último…
Read more
Code Fighters

Dicas e truques para Pentest em API

Com o objetivo de possibilitar a comunicação entre diferentes plataformas, a utilização de APIs…
Read more

Deixe um comentário