在 5.6.15 之前的 Kibana 版本以及 6.6.1 版本中,Timelion 可视化工具存在任意代码执行缺陷。拥有 Timelion 应用访问权限的攻击者可以发送一个请求,尝试执行 JavaScript 代码。这可能导致攻击者以主机系统上 Kibana 进程的权限执行任意命令。
CVSS:

博客: https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/
Payload: https://github.com/mpgn/CVE-2019-7609
设置:
使用:
解析 sheet 后:

预先处理 sheet 后,我们得到一个 queries 对象:

然后,我们遍历 queries 数组(通过使用 values 方法将其转换为数组),并将 query.function 和 query.arguments 传入 invoke 以调用函数。
在 invoke 函数中,首先根据名称(query.function)获取函数的定义:

这个函数定义包含三种类型的函数:fn、originalFn 和 timelionFn。然后,它遍历 args(即 query.arguments)并解析参数:

在这种情况下,args 中的参数是一个类型为 'literal' 的对象,因此它仅返回字面值:

实际上,该值被包裹在一个 promise 中。

索引参数是一个包含 byName 对象的参数,该对象内部有 q 数组:

之后,invoke 函数将调用 functionDef.fn,该函数是 Timelion 类的一个函数,并传入所有索引参数:

fn 函数将调用 originalFn,该函数来自 config.fn,其中 config 是 tlConfig。而该函数是继承自 Timelion 的 Datasource 类的 fn:

正如我们所看到的,它调用另一个名为 esFn 的 originalFn,该函数返回一个 seriesList 对象:

该函数通过调用 callWithRequest 函数用于“从 elasticsearch 实例拉取数据”。
seriesList 在 Datasource.fn 中看起来像这样:

而在 Timelion.fn 中它看起来像这样:

预先处理 sheet 后,链列表为:

代码将调用 resolveChainList 来解析链列表:

每个链都会被映射到一个 Promise,该 Promise 由以 'first' 作为 fnName、以链本身作为 args 调用 invoke 的函数返回。
函数定义再次根据名称获取:

这次,resolveArguments 接收 args 作为数组,并将递归调用 resolveArguments 来解析每个参数。但是,我们只有一个索引为 0 的参数。该参数是一个类型为 chain 的对象。
因此,在 switch 分支中,它将调用 invokeChain 函数并传入链对象:

在 invokeChain 内部,它将对 chain 数组执行 shift 操作。shift 函数返回被移除的元素,因此 link 将是 es 对象:

因为 es 的类型不是 'chain'(而是 'function'),并且 !result 为真,所以代码调用 invoke 函数,以 'first' 作为 fnName、es 对象作为 args:
这次的过程与上面相同,因此跳过,直接调用 invokeChain,其中 link 是 props 对象:

再次,代码调用 invoke 函数,以 'first' 作为 fnName、props 对象作为 args:

props 的 originalFn 现在是 firstFn,而不是像 es 那样的 fn,并且只接受一个参数:

该原始函数最终会调用存在漏洞的 unflatten 函数。实际上,它会首先调用 Timelion 的 fn。然后 fn 会调用 props 的 originalFn:

props 的 firstFn 会省略 args 的 byName 对象中的 inputSeries 和 global 属性:

因此传入 unflatten 的 data 将是:
{
label.__proto__.env.AAA: "a"
}
证明:

使用 props(label.__proto__.env.AAA="a")。data 将看起来像这样:

在第一次迭代中,prop = '' 不存在,它将赋值 cur[''] = {}。这意味着 result 也会有一个名为 '' 的属性,其值为空对象。之后,cur 被赋值为 {},即前一次赋值的返回值(cur 和 cur[prop] 都指向同一个对象):

现在,prop 将变为 'label':

和之前一样,result 会有一个 'label' 属性,然后 prop 变为 __proto__:

在第三次迭代中,因为 __proto__ 总是存在于每个对象中,cur 将变为其原型:

之后,prop 变为 env。
然后,由于 cur 指向 Object.prototype,它会向 Object.prototype 添加一个 env 属性,其值为空对象,并且 cur 也引用了 Object.prototype.env 内部的 env。

之后,循环将退出,cur[prop] = data[p] 将等价于 env[AAA] = 'a'。

Object.prototype 被污染了 env:

当调用 fork 函数时将端口改为 9669(另一个端口),然后我们可以正常调试主进程:

模拟被污染的 Object.prototype 并跳转到循环之前:

正如我们所看到的,由于原型污染,env 等价于 { AAA: 'a' }。更具体地说,env 被赋值为 options.env。而 options 没有定义该属性,因此会使用其原型的值。
循环之后:

不知何故,由于无效的环境变量(AAA=a、env=[Object object]),canvas 无法创建新的 socket:

但是子进程仍然可以创建:

在正常流程下,它会创建一个新的 WebSocket 连接:

现在尝试使用 payload 并跳转到 normalizeSpawnArguments 中:

正如我们所看到的,堆栈跟踪是相同的。
但是,env 被污染了:

瞧!
循环之后的 envPairs:

我们得到了反向 shell:

在 spawn 函数中调用 normalizeSpawnArguments 之后:

正如我们所看到的,envPairs 包含我们的 payload。此外,之后 spawn 会调用 internal/child_process.js 中的 spawn 函数:

options 变量将被传入 this._handle.spawn:

其中 this._handle 是进程的句柄:

From 3377f813a5d96ff466bdf7343ce161de24830ed4 Mon Sep 17 00:00:00 2001
From: ppisljar <[email protected]>
Date: Wed, 16 Jan 2019 06:01:20 -0800
Subject: [PATCH] fixes
---
.../core_plugins/timelion/server/series_functions/props.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/legacy/core_plugins/timelion/server/series_functions/props.js b/src/legacy/core_plugins/timelion/server/series_functions/props.js
index 81b74901d4db..80e9cafd6712 100644
--- a/src/legacy/core_plugins/timelion/server/series_functions/props.js
+++ b/src/legacy/core_plugins/timelion/server/series_functions/props.js
@@ -32,7 +32,7 @@ function unflatten(data) {
let prop = '';
let m;
while (m = regex.exec(p)) {
- cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
+ cur = (cur.hasOwnProperty(prop) && cur[prop]) || (cur[prop] = (m[2] ? [] : {}));
prop = m[2] || m[1];
}
cur[prop] = data[p];