{
    "version": "https:\/\/jsonfeed.org\/version\/1.1",
    "title": "Заметки Андрея Гейна: posts tagged PHDays",
    "_rss_description": "Конференция PHDays и соревнования PHDays CTF",
    "_rss_language": "en",
    "_itunes_email": "",
    "_itunes_categories_xml": "",
    "_itunes_image": "",
    "_itunes_explicit": "",
    "home_page_url": "https:\/\/andgein.ru\/blog\/tags\/phdays\/",
    "feed_url": "https:\/\/andgein.ru\/blog\/tags\/phdays\/json\/",
    "icon": "https:\/\/andgein.ru\/blog\/pictures\/userpic\/userpic@2x.jpg?1631100411",
    "authors": [
        {
            "name": "Андрей Гейн",
            "url": "https:\/\/andgein.ru\/blog\/",
            "avatar": "https:\/\/andgein.ru\/blog\/pictures\/userpic\/userpic@2x.jpg?1631100411"
        }
    ],
    "items": [
        {
            "id": "2",
            "url": "https:\/\/andgein.ru\/blog\/all\/2-phdays-ctf-2017-service\/",
            "title": "Заметка вторая. О сервисе на PHDays CTF",
            "content_html": "<p><i>Продолжение истории о <a href=\"\/blog\/all\/1-unicode-and-csv-in-python\/\">необычном поведении<\/a> юникода и CSV в питоне<\/i><\/p>\n<h2>Для тех, кому лень читать предыдущую заметку или вспоминать, о чём там было<\/h2>\n<p>Если использовать стандартные питоновские <a href=\"https:\/\/docs.python.org\/3\/library\/codecs.html#codecs.open\">codecs.open<\/a>, <a href=\"https:\/\/docs.python.org\/3\/library\/csv.html#csv.DictWriter\">csv.DictWriter<\/a> и <a href=\"https:\/\/docs.python.org\/3\/library\/csv.html#csv.DictReader\">csv.DictReader<\/a>, то можно столкнуться с интересным поведением. Создаём в программе файл в кодировке UTF-8, пишем туда с помощью DictWriter, а затем читаем через DictReader. Если в данных встречались юникодные символы Pararaph separator или Line separator, то мы считаем больше записей, чем было записано, а их CSV-структура будет поломана.<\/p>\n<p>Читайте <a href=\"\/blog\/all\/1-unicode-and-csv-in-python\/\">первую заметку<\/a> для подробностей.<\/p>\n<h2>Как из этого получился сервис для классического CTF<\/h2>\n<p>В апреле мы с ребятами из Хакердома как раз готовили онлайновый CTF для форума <a href=\"https:\/\/phdays.com\">PHDays<\/a>. Темой был выбран интернет вещей, а сервисами были умный чайник, термометр, дверной замок, телевизор и даже холодильник. Мне достался последний, потому что я слишком люблю еду.<\/p>\n<p>Чтобы органично встроить уязвимость в сервис, мне нужно было сделать так, чтобы кто-то писал в CSV-файл, а кто-то другой — читал. В итоге сервис состоял из двух частей — веб-приложения для удобного управления человеком и API для других умных кухонных гаджетов, которым нужна информация от холодильника.<\/p>\n<p>Веб-интерфейс позволял пользователям регистрироваться и создавать холодильные камеры или просто холодильники. У каждого холодильника был владелец, и увидеть чужие просто так нельзя. В холодильники можно добавлять еду. Для этого сначала было необходимо зарегистрировать новый продукт (например, картошку или молоко) и указать, в чём он измеряется — в граммах, пачках или литрах. Выглядело это так:<\/p>\n<div class=\"e2-text-picture\">\n<div class=\"fotorama\" data-width=\"1122\" data-ratio=\"2.5384615384615\">\n<img src=\"https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_17-12-56-(2).png\" width=\"1122\" height=\"442\" alt=\"\" \/>\n<img src=\"https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_17-14-21.png\" width=\"1125\" height=\"676\" alt=\"\" \/>\n<img src=\"https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_17-23-56.png\" width=\"1124\" height=\"597\" alt=\"\" \/>\n<img src=\"https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_20-56-37.png\" width=\"1127\" height=\"398\" alt=\"\" \/>\n<img src=\"https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_20-58-25-(2).png\" width=\"1159\" height=\"584\" alt=\"\" \/>\n<\/div>\n<\/div>\n<p>Кроме холодильников можно было создавать рецепты. Про каждый ингредиент рецепта известно, сколько и какого продукты нужно положить, а также что с ним надо сделать и сколько после этого подождать.<\/p>\n<div class=\"e2-text-picture\">\n<img src=\"https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_21-07-14.png\" width=\"1122\" height=\"669\" alt=\"\" \/>\n<\/div>\n<p>Рецепты были очень важны для функционирования сервиса, ведь в том самом API было всего две команды — получить список твоих холодильников и получить список рецептов, которые можно приготовить из еды, лежащей в одном из твоих холодильников. Звучит сложно, но идея очень простая — если у вас есть умная мультиварка, то она хочет узнать, какие блюда можно сегодня приготовить из продуктов, лежащих у вас в холодильнике.<\/p>\n<p>Принадлежность выдаваемых рецептов владельцу холодильника в API не проверялась, но должно было работать само: в рецепте должен присутствовать хотя бы один продукт из холодильника, а в холодильник мы можем добавлять только принадлежащие нам продукты. Моё молоко и молоко Васи — два разных продукта, я могу добавить в холодильники и рецепты только первое, а про второе даже не смогу узнать.<\/p>\n<h2>Уязвимость<\/h2>\n<p>Итак, где же тут наши подозреваемые — юникод и CSV?<\/p>\n<p>Сервис API не имеет доступа к базе данных, поэтому информация для него складывалась веб-приложением в специальные CSV-файлы, откуда считывались API-приложением раз в секунду. Вот пример такого файла:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">id,recipe_id,food_type_id,what_to_do,count,pause_after\n1,10,15,Yhws aqx vpjhtlyhw ruv tbcyis,11,7\n2,11,16,Ok jbso gtpzs ndgoz udeksmvk,10,16\n3,12,17,Y ap mculltvedfwwabbnnnco um,6,19\n4,13,18,cniyvdjsyuctaamupp zm  qj nwvm,10,9\n5,14,19,Kfdvm ref wtb pdtitb,14,15<\/code><\/pre><p>В нём хранятся ингредиенты рецептов. Столбцы, соответственно — id ингредиента, id рецепта (сами рецепты лежат в другом файле), id продукта, что нужно сделать, сколько взять продукта и какую выдержать после этого паузу.<\/p>\n<p>Давайте посмотрим, что будет, если в поле what_to_do добавить тот самый Paragraph separator:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">6,15,20,blablabla\u202913503,14,20<\/code><\/pre><p>При чтении этот файл будет выглядеть для DictReader’а как<\/p>\n<pre class=\"e2-text-code\"><code class=\"\">6,15,20,blablabla\u2029\n13503,14,20<\/code><\/pre><p>То есть так, будто мы добавили продукт №20 в рецепт №14. Продукт №20 принадлежит нам, значит, мы смогли добавить нашу еду в чужой рецепт №14! Теперь API сможет вывести этот рецепт, если в каком-нибудь из наших холодильников будет лежать продукт №20.<\/p>\n<p>Для полноты картины покажу, как выглядели сохранение и загрузка данных в CSV.<\/p>\n<h3>Сохранение:<\/h3>\n<pre class=\"e2-text-code\"><code class=\"\">def dump_model_to_file(model, filename):\n    # Use Django API to get all model fields\n    columns = [field.attname for field in model._meta.fields]\n\n    with codecs.open(filename, &#039;w&#039;, encoding=&#039;utf-8&#039;) as opened_file:\n        # Open csv writer and dump header: special row with columns names\n        writer = csv.DictWriter(opened_file, fieldnames=columns)\n        writer.writeheader()\n\n        # Iterate over all objects of the model\n        for obj in model.objects.all():\n            object_dict = {}\n            for column in columns:\n                # Don&#039;t worry about newlines (\\n and \\r): csv.DictWriter will enclose such strings in quotes (&quot;)\n                # So I think there is no vulnerability here\n                object_dict[column] = str(getattr(obj, column, &#039;&#039;))\n\n            # Dump dictionary for current object\n            writer.writerow(object_dict)<\/code><\/pre><h3>Загрузка:<\/h3>\n<pre class=\"e2-text-code\"><code class=\"\">class Model:\n    def __init__(self, dictionary):\n        for key, value in dictionary.items():\n            if value is None:\n                value = &#039;0&#039;\n            if value.isnumeric():\n                value = int(value)\n            setattr(self, key, value)\n\n\ndef load_models_from_file(filename):\n    objects = {}\n    with codecs.open(filename, &#039;r&#039;, encoding=&#039;utf-8&#039;) as opened_file:\n        reader = csv.DictReader(opened_file)\n        for row in reader:\n            objects[model.id] = Model(row)\n    return objects<\/code><\/pre><h2>Ещё две уязвимости<\/h2>\n<p>В Холодильнике мной была заложена ещё одна уязвимость. Но так получилось, что в итоге уязвимостей оказалось не две, а три. Так бывает на CTF, и в этом нет ничего страшного. Иногда бывает обидно, что ты заложил сложную уязвимость, а случайно оставил простую. Но в данном случае всё случилось удачно: незапланированная уязвимость была проще первой, а запланированная — совсем элементарной (правда, позволяла украсть только 20% флагов). Так как в целом соревнование получилось очень сложным, то появление одной незапланированной уязвимости средней сложности сыграло нам на руку.<\/p>\n<p>Итак, сначала о запланированной уязвимости. Каждый сервис находился в своём докер-контейнере. Докер (docker) — это удобный способ изолировать своё приложение от других, в линуксе работает за счёт его фирменных LXC-контейнеров и ограничений в cgroup. Подробнее о докере можно почитать на <a href=\"https:\/\/docker.com\/\">официальном сайте<\/a>.<\/p>\n<p>Мой сервис состоял из четырёх докер-контейнеров: для веб-приложения, для API-приложения, для базы данных и для веб-сервера nginx. Во время старта первого накатывались миграции, собиралась статика и выполнялись другие служебные команды. В том числе такая:<\/p>\n<pre class=\"e2-text-code\"><code class=\"\"># DON&#039;T RUN IT IN PRODUCTION. SOME EVIL GUYS CAN BRUTEFORCE PASSWORD AND WHO KNOW WHAT HAPPENS...\necho &quot;[+] [DEBUG] Django setup, executing: add superuser&quot;\nPGPASSWORD=${POSTGRES_PASSWORD} psql -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -c &quot;INSERT INTO auth_user (password, last_login, is_superuser, username, first_name, last_name, email, is_staff, is_active, date_joined) VALUES (&#039;pbkdf2_sha256\\$36000\\$k36V24q60mNo\\$v5og9qcgc2sqkVwGjZDKNK+wcJy60ix8DIt9E8Yg48c=&#039;, &#039;1970-01-01 00:00:00.000000&#039;, true, &#039;admin&#039;, &#039;admin&#039;, &#039;admin&#039;, &#039;admin@admin&#039;, true, true, &#039;1970-01-01 00:00:00.000000&#039;) ON CONFLICT (username) DO NOTHING&quot;<\/code><\/pre><p>Эта команда добавляет напрямую в базу данных супер-пользователя с логином admin. Его пароль мы не знаем, так что сразу зайти под ним не можем, но доступен хеш от пароля: ‘pbkdf2_sha256$36000$k36V24q60mNo$v5og9qcgc2sqkVwGjZDKNK+wcJy60ix8DIt9E8Yg48c=’. Подбор пароля не занимает много времени, так как он словарный и встречается во всех списках самых частых паролей.<\/p>\n<p>Попрактикуйтесь — сможете ли вы подобрать пароль? pbkdf2_sha256 — это тип хеша и подписи, а 36000 — количество итераций. Справиться с задачей поможет hashcat, john the ripper или любой другой подборщик прообразов хешей.<\/p>\n<h2>Незапланированная уязвимость<\/h2>\n<p>Последняя уязвимость нашлась в функции добавлении продукта в холодильник. Здесь не проверялось, что вы являетесь владельцем добавляемого продукта. Можно было создать супер-холодильник, содержащий все продукты с номерами от 1 до 1000, а затем попросить API выдать рецепты, содержащие хотя бы какой-нибудь продукт из этого холодильника. Конечно, он находил и чужие рецепты, а вместе с ними выдавал и флаги, хранящиеся в описаниях этих рецептов.<\/p>\n<h2>Выводы<\/h2>\n<p>Никогда не используйте csv.DictReader\/csv.DictWriter вместе с файлами, открытыми модулем codecs. В новых питонах открывайте CSV-файлы с помощью стандартной функции open, передавая ей аргументы encoding и newline.<\/p>\n<h2>Бонус для дочитавших до конца<\/h2>\n<p>Все исходники сервиса, докер-файлы, чекер для проверяющей системы и мой авторский эксплойт для первой уязвимости можно найти в <a href=\"https:\/\/github.com\/HackerDom\/phdctf-2017\">репозитории разработки<\/a>, который мы открыли сразу после окончания соревнования.<\/p>\n",
            "date_published": "2017-05-17T15:23:38+01:00",
            "date_modified": "2017-08-17T19:24:59+01:00",
            "tags": [
                "csv",
                "CTF",
                "PHDays",
                "python",
                "unicode",
                "программирование"
            ],
            "image": "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_20-58-25.png",
            "_date_published_rfc2822": "Wed, 17 May 2017 15:23:38 +0100",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "2",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "jquery\/jquery.js",
                    "fotorama\/fotorama.css",
                    "fotorama\/fotorama.js",
                    "highlight\/highlight.js",
                    "highlight\/highlight.css"
                ],
                "og_images": [
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_20-58-25.png",
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_17-12-56-(2).png",
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_17-14-21.png",
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_17-23-56.png",
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_20-56-37.png",
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_20-58-25-(2).png",
                    "https:\/\/andgein.ru\/blog\/pictures\/2017-05-16_21-07-14.png"
                ]
            }
        }
    ],
    "_e2_version": 4134,
    "_e2_ua_string": "Aegea 11.3 (v4134)"
}