Server-side template injection vulnerabilities (SSTI) - Các lỗ hổng SSTI (phần 1)
I. Đặt vấn đề
1. Bối cảnh
Theo thời gian và nhu cầu, các dữ liệu hiển thị trên trang web không ngừng thay đổi. Ba yếu tố cơ bản nhất tạo nên một trang web là HTML, CSS, Javascript. Để thêm, sửa, xóa chức năng, dữ liệu, thay đổi bố cục giao diện dẫn đến lập trình viên cần chỉnh sửa toàn bộ source code - tiêu tốn tài nguyên, thời gian. Bởi vậy kỹ thuật template ra đời. Cách thức hoạt động cơ bản của ngôn ngữ template bao gồm back-end rendering và front-end rendering:
- Render trên back-end bao gồm việc dịch các ngôn ngữ template theo một tiêu chuẩn và chuyển chúng thành HTML, JavaScript hoặc CSS, từ đó trả về cho phía front-end.
- Sau đó, quá trình front-end rendering tiếp nhận, thực thi và gửi toàn bộ mã nguồn trên đến client, cho phép client tạo ra giao diện người dùng.
Xét một ví dụ:
Front-end:
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<form method="{{method}}" action="{{action}}">
<input type="text" name="user" value="{{username}}">
<input type="password" name="pass" value="">
<button type="submit">submit</button>
</form>
<p>Used {{mikrotime(true) - time}}</p>
</body>
</html>
back-end:
$template Engine=new TempLate Engine();
$template=$template Engine-load File('login.tpl');
$template->assign('title', 'login');
$template->assign('method', 'post');
$template->assign('action', 'login.php');
$template->assign('username', get Username From Cookie() );
$template->assign('time', microtime(true) );
$template->show();
Đoạn mã trên tạo một đối tưởng template, load file template login.tpl, gán giá trị cho các biến bằng hàm assign()
, cuối cùng gọi hàm show()
hiển thị nội dung đã được thay thế bằng HTML trong front-end.
2. Giới thiệu lỗ hổng Server-side template injection (SSTI)
Server-side template injection (SSTI) là dạng lỗ hổng cho phép kẻ tấn công inject các payload (tạo bởi chính ngôn ngữ template đó) vào các template, và chúng được thực thi tại phía server. Trong đa số trường hợp xảy ra lỗ hổng SSTI đều mang lại các hậu quả to lớn cho server, bởi các payload SSTI được thực thi trực tiếp tại server và thường dẫn tới tấn công thực thi mã nguồn tùy ý từ xa (RCE - Remote Code Execution).
Xét ví dụ quá trình gửi thư điện tử tới người nhận theo tên. Sử dụng template Twig render nội dung theo dữ liệu tĩnh (static) sẽ không tạo ra lỗ hổng SSTI do giá trị first_name
là giá trị tĩnh.
$output = $twig->render("Dear {first_name},", array("first_name" => $user.first_name) );
Tuy nhiên, khi input từ người dùng được trực tiếp liên kết truyền vào template sẽ có thể dẫn tới tấn công SSTI.
$output = $twig->render("Dear " . $_GET['name']);
Tình huống trên sử dụng giá trị tham số name
lấy từ phương thức GET trực tiếp tạo thành một phần của template. Do người dùng có thể thay đổi giá trị $_GET['name']
nên kẻ tấn công có thể inject các payload tùy ý, chẳng hạn:
http://vulnerable-website.com/?name={{payload}}
SSTI là một trong những lỗ hổng ở mức nâng cao, có thể xảy ra trong nhiều loại template khác nhau. Chúng ta sẽ cùng đến với một số demo trong mục tiếp theo.
II. SSTI trong các template và cách ngăn chặn
Do có nhiều loại template khác nhau, trong bài viết này tôi sẽ thực hiện demo với các loại template thông dụng trong Python, PHP, JAVA.
1. Framework Flask, Template Jinja2 trong Python
Flask là một framework trong python cung cấp một bộ công cụ cùng các thư viện để xây dựng các ứng dụng. Flask sử dụng Jinja2 làm hệ thống template mặc định. Theo định nghĩa từ https://jinja.palletsprojects.com/en/3.1.x/intro/
Jinja is a fast, expressive, extensible templating engine. Special placeholders in the template allow writing code similar to Python syntax. Then the template is passed data to render the final document.
Có thể hiểu Jinja2 là một hệ thống template engine cho phép lập trình viên tách biệt logic xử lý dữ liệu và hiển thị giao diện trong các ứng dụng web bằng các biến template.
Cùng xem xét một demo đầu tiên sử dụng Flask xây dựng một ứng dụng web:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World'
if __name__ == '__main__':
app.run(host = '0.0.0.0', port = 1337)
Đoạn code sử dụng phương thức route()
liên kết hàm hello_world()
với thư mục gốc (/
) của ứng dụng. Nói cách khác, khi người dùng truy cập trang chủ của ứng dụng, Flask sẽ gọi hàm hello_world()
trả về response hiển thị "Hello World".
Tiếp theo, chúng ta có thể sử dụng template render giao diện với code html bằng hàm render_template_string()
@app.route('/')
def hello_world():
template = '''
<p>Hello World</p>
'''
return render_template_string(template)
Cùng đến với ưu điểm trong Jinja2: truyền giá trị các biến template nhằm tăng tính linh hoạt cho giao diện. Ví dụ:
@app.route('/')
def hello_world():
name = request.args.get('name') # template variable example
template = '''
<p>Hello %s</p>
''' % (name)
return render_template_string(template)
Tại đây, lỗ hổng SSTI xảy ra do ứng dụng đưa giá trị input name
trực tiếp vào template và cùng template render ra giao diện, không có bất kỳ bước filter nào. Thật vậy, sử dụng cú pháp Jinja2, chúng ta có thể kiểm tra phát hiện lỗ hổng bằng payload {{7*7}}
(hoặc bất kỳ phép tính nào):
Tham số name
được thực thi trong template Jinja2 và trả về giá trị 49, chứng tỏ ứng dụng có thể bị khai thác bởi lỗ hổng SSTI.
Để ngăn chặn, lập trình viên không được cho phép các tham số input render trong template: render template xong mới ghép giá trị tham số vào. Một ví dụ sau khi chỉnh sửa đoạn code:
@app.route('/')
def hello_world():
name = request.args.get('name') # template variable example
template = Template('''
<h3>Hello {{name}}</h3>
''')
return template.render(name = name)
2. Template Tornado trong Python
Tornado là một framework web được sử dụng để xây dựng các ứng dụng web và các dịch vụ web có khả năng mở rộng cao. Tornado cũng là một loại template trong Python. Xem xét đoạn script sử dụng template Tornado sau:
import tornado.template
import tornado.ioloop
import tornado.web
TEMPLATE = '''
<html>
<head><title>Welcome</title></head>
<body> Hello {{foo}} </body>
</html>
'''
class MainHandler(tornado.web.RequestHandler):
def get(self):
name = self.get_argument('name', '')
template_data = TEMPLATE.replace("foo", name)
t = tornado.template.Template(template_data)
self.write(t.generate(name = name))
application = tornado.web.Application([
(r"/", MainHandler),
], debug = True, static_path = None, template_path = None)
if __name__ == '__main__':
application.listen(1337)
tornado.ioloop.IOLoop.instance().start()
Đoạn code trên định nghĩa một biến TEMPLATE
trong dạng văn bản template, sau đó thay thế biến foo
trong đó thành giá trị biến name
truyền qua phương thức GET, cuối cùng render toàn bộ template ra giao diện. Ứng dụng web có thể bị khai thác bằng lỗ hổng SSTI. Với input 7*7
, giao diện trả về 49, là dấu hiệu xảy ra lỗ hổng SSTI:
Để ngăn chặn lỗ hổng, lập trình viên nên render template sau đó mới thay thế các tham số. Chẳng hạn đoạn code sau đã sửa lại và phát huy tác dụng:
TEMPLATE = '''
<html>
<head><title>Welcome</title></head>
<body> Hello {{ foo }} </body>
</html>
'''
class MainHandler(tornado.web.RequestHandler):
def get(self):
name = self.get_argument('name', '')
t = tornado.template.Template(TEMPLATE)
self.write(t.generate(foo = name))
Các tài liệu tham khảo
- https://portswigger.net/web-security/server-side-template-injection
- https://flask.palletsprojects.com/en/2.2.x/
- https://jinja.palletsprojects.com/en/3.1.x/
- https://www.tornadoweb.org/en/stable/
©️ Tác giả: Lê Ngọc Hoa từ Viblo
All rights reserved