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


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àmall_products()
thuộc classshop
./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àmproduct_details(product_id)
của classshop
và truyền kết quả trả về vào biến product, sau đó sẽ render với templateitem.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ếnf
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ụngpickle
để 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ảngshop
chứa các chuỗi base64Dòng 45: Đọc nội dung của file
f
bằngf.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 dungCuố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 testCuố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
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 🐧