# Khai thác dbfilter để mua vui và chút ít lợi lộc

> 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.

**Date:** 2019-04-09
**Source:** <https://trobz.com/vi/insights/odoo-dbfilter-regex-injection/>

---


Trong [bài viết trước](/vi/insights/odoo-dbfilter/), 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ụ:

```text
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](https://owasp.org/www-community/attacks/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](https://github.com/odoo/odoo/issues/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](https://www.odoo.com/page/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`](https://github.com/OCA/server-tools/tree/9.0/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ý.

