Regex dbfilter của Odoo từng bị chèn qua header Host để vô hiệu hóa việc lọc database như thế nào, một biến thể ReDoS chúng tôi phát hiện trên chính runbot của Odoo, và bản vá Odoo phát hành sau khi chúng tôi báo cáo.

Trong bài viết trước, chúng ta đã thấy cơ chế lọc database của Odoo dựa trên một biểu thức chính quy (regex). Bài này sẽ xem xét việc, cho tới khi được vá, regex đó có thể bị lợi dụng trong một số hệ thống Odoo như thế nào, kể cả trên chính runbot của Odoo.

Vấn đề mô tả dưới đây đã được báo cáo cho Odoo và đã được vá. Chúng tôi trình bày ở đây như một bài học về việc một mẩu cấu hình nhỏ có thể trở thành lỗ hổng bảo mật ra sao, chứ không phải như một cách tấn công còn dùng được: các phiên bản liên quan đều đã cũ và được thay thế từ lâu.

Lỗ hổng: chèn regex

Hai biến %d và %h được thay trực tiếp bằng giá trị lấy từ header Host của HTTP request.

Kẻ tấn công có thể tạo một header Host để chèn một chuỗi tùy ý vào regex, kể cả các ký tự đặc biệt. Chẳng hạn, khi chèn |^.*$|, họ có thể thay đổi hoàn toàn hành vi của regex: mọi tên database bỗng nhiên đều khớp, và bộ lọc bị vô hiệu hóa.

Khai thác trên runbot

Wildcard DNS

Odoo có một bản ghi wildcard DNS cho *.runbotXX.odoo.com. Lấy một runbot build làm ví dụ:

http://315285-10-0-opw-1820081-refix-sig-fc659d.runbot11.odoo.com

Hostname đó phân giải thành một alias của runbot11.odoo.com.

dbfilter

Instance Odoo của build này chạy với --db-filter='%d.*$'. Truy cập URL của build sẽ liệt kê mọi database khớp với 315285-10-0-opw-1820081-refix-sig-fc659d.*$.

Khớp hostname theo wildcard

Ở phía Nginx, instance này truy cập được qua mọi hostname khớp với ^315285-10-0-opw-1820081-refix-sig-fc659d[-.].*$.

Vì vậy một hostname như 315285-10-0-opw-1820081-refix-sig-fc659d-hello-my-name-is-brian.runbot11.odoo.com cũng khớp. Nhưng không có database nào khớp với tên đó, nên truy cập vào chỉ mở ra form tạo database của Odoo.

Ký tự đặc biệt

Vì phần .* trong regex của Nginx chấp nhận mọi ký tự, một hostname chứa |^.*$| cũng sẽ khớp. Các ký tự này không thể nằm trong một bản ghi DNS, nên request phải kết nối tới runbot11.odoo.com rồi tự đặt header Host, việc này khá đơn giản với một HTTP client thông thường.

Liệt kê toàn bộ database

Ghép các mảnh này lại là vượt qua được bộ lọc: với header Host được dàn dựng, trình quản lý database trả về toàn bộ danh sách database trên host, khoảng 148 database trong ví dụ.

Bản thân thông tin lộ ra ở đây (một danh sách tên database) không quá đáng chú ý. Nhưng trong bối cảnh khác, kẻ tấn công có thể đi xa hơn: database thử nghiệm thường có mật khẩu yếu hơn database production, và một chỗ đứng chân trong đó có thể được dùng để lần tới phần còn lại của host.

Đi xa hơn: ReDoS

Có một lớp tấn công gọi là Regular Expression Denial of Service (ReDoS), trong đó một chương trình bị làm chậm gần như tê liệt chỉ bằng một regex. Một số pattern tạo ra số lượng đường đi cần duyệt tăng theo hàm mũ với một số input nhất định.

Trường hợp thường gặp là một regex yếu bị đưa vào một input độc hại. Ở đây thì ngược lại: chúng tôi không kiểm soát input (tên các database mà Odoo lọc), nhưng lại kiểm soát được regex, thông qua header Host được chèn vào. Vì vậy, thay vì tìm input làm treo một regex, chúng tôi tìm một regex làm treo trên các tên database đang có. Regex đó cần hai đặc điểm:

  • một pattern yếu như (a+)+; tên database cho phép [0-9a-z\-], nên chúng tôi dùng ([0-9a-z\-]+)+;
  • không khớp với bất kỳ tên database thật nào, để buộc bộ máy regex phải duyệt hết mọi đường đi. Mỗi build tạo ra một database -base và một database -all, nên tên luôn kết thúc bằng base hoặc all, rất dễ loại trừ.

Gửi một regex như vậy qua header Host khiến server dồn toàn bộ thời gian vào việc so khớp. Trên runbot, request bị hủy sau 60 giây, vì các build chạy ở chế độ multi-worker với mặc định limit_time_cpu = 60. Trên một hệ thống không có giới hạn đó, tác động sẽ kéo dài hơn.

Kết luận

Chúng tôi đã báo cáo vấn đề này cho đội bảo mật của Odoo. Dù tác động hạn chế, họ vẫn quyết định phát hành một security advisory đầy đủ. Bản vá rất đơn giản: escape hai biến %d và %h bằng re.escape trước khi dựng regex (odoo/odoo#32511).

Bài học rộng hơn là: một giá trị lấy từ HTTP request, ở đây là header Host, không bao giờ nên được đưa thẳng vào một biểu thức chính quy. Odoo đã tuân thủ quy trình responsible disclosure trong suốt quá trình, và bản vá đã có trong mọi phiên bản hiện tại.

Ghi chú về module dbfilter_from_header của OCA

Module dbfilter_from_header của OCA có một thư mục static/, và điều này vô tình khiến module được nạp vô điều kiện, ngay cả khi nó không được khai báo trong server_wide_modules lẫn không được cài đặt. Nghĩa là chỉ cần có nó trong addons path, một instance đã trực tiếp đối mặt với cùng ReDoS đó qua header X-Odoo-dbfilter (hoặc X-Openerp-dbfilter). Vấn đề này sau đó cũng đã được xử lý.