def processStructured(report: ReportWrapper, token: str='') -> VulnTestInfo: """ Process the given report into a AutoTriageUtils.VulnTestInfo named tuple given that it contains structured data """ info = extractJson(report.getLatestActivity()) if info is None: return VulnTestInfo(reproduced=False, message=('Failed to parse JSON! Please try again.'), type='XSS', info={'report': report.getLatestActivity()}) # Pass it off to a helper that can try to handle any inconsistencies url, cookies, type, data = extractDataFromJson(info) if not AutoTriageUtils.isProgramURL(url): return VulnTestInfo(reproduced=False, message=('The url provided (`%s`) is not a program URL!') % url, type='XSS', info={'src': url, 'method': 'structured'}) if type.lower() == 'post': results = testPOSTXSS(url, cookies, data) elif type.lower() == 'get': results = testGETXSS(url, cookies) else: return VulnTestInfo(reproduced=False, message='Found an invalid value "type"=%s in the JSON blob!' % type, type='XSS', info={'src': url, 'method': 'structured'}) reproduced, alertBox, message, confirmedBrowsers, alertBrowsers = makeMarkdownTable(results, token) if reproduced: return VulnTestInfo(reproduced=True, message='Successfully found and confirmed an XSS at `%s`!\n' '\n\n%s\n\n' 'Metadata: {"vulnDomain": "%s"}' % (url, message, urlparse(url).hostname), type='XSS', info={'src': url, 'method': 'unstructured', 'confirmedBrowsers': confirmedBrowsers, 'alertBrowsers': alertBrowsers, 'httpType': type, 'cookies': cookies}) # noqa elif alertBox: return VulnTestInfo(reproduced=False, message=('Failed to confirm the vulnerability! Detected an alert box ' 'but the token: `"%s"` was not found!' '\n\n%s\n\n') % (token, message), type='XSS', info={'src': url, 'method': 'unstructured'}) else: return VulnTestInfo(reproduced=False, message=("Failed to validate XSS at `%s` via structured data. Either try " "again or wait for manual review of your bug.") % url, type='XSS', info={'method': 'structured'})
def isDuplicate(r1: ReportWrapper, r2: ReportWrapper) -> DuplicateResult: """ Returns a confidence rating on whether the two given reports are duplicates of each other """ for module in modules: if (module.match(r1.getReportBody(), r1.getReportWeakness()) and # type: ignore module.match(r2.getReportBody(), r2.getReportWeakness())): # type: ignore return sameCategoryIsDuplicate( r1, r2, module.containsExploit) # type: ignore return DuplicateResult((None, ID('A')))
def process(report: ReportWrapper) -> Optional[VulnTestInfo]: """ Process the given report into a VulnTestInfo named tuple """ # If the user has not yet been prompted for automatic triaging if not report.botHasCommented(): token = AutoTriageUtils.generateToken() return VulnTestInfo(reproduced=False, message=constants.initialMessage(token, 'redirect to a domain', 'Open Redirect'), type='Open Redirect', info={}) elif report.shouldBackoff(): if not report.hasPostedBackoffComment(): addFailureToDB(report.getReporterUsername(), report.getReportID()) return VulnTestInfo(reproduced=False, message=('Automatic verification of vulnerability has failed, Backing off! Falling ' 'back to human verification. '), type='Open Redirect', info={}) else: return None elif report.isVerified(): return None try: if isStructured(report.getLatestActivity()): return processStructured(report, token=report.getToken()) else: return processUnstructured(report, token=report.getToken()) except Exception as e: print("Caught exception: %s" % str(e)) traceback.print_exc() print("+" * 80) return VulnTestInfo(reproduced=False, message=('Internal error detected! Backing off...'), type='Open Redirect', info={})
def getMetadata(id: str): ser = requests.post('http://api:8080/v1/getReport', json={ 'id': id }, auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text metadataComment = ReportWrapper().deserialize( ser).extractMetadata() # type: ignore firstLine = metadataComment.splitlines()[0].replace('# ', '*') + '*' return '\n'.join([firstLine] + metadataComment.splitlines()[1:])
def test_isVerified(monkeypatch): r = ReportWrapper() monkeypatch.setattr(r, '_ReportWrapper__getBody', lambda a: a) monkeypatch.setattr(r, '_getPublicCommentsByUsername', lambda u: ['Comment 1', 'Comment 2']) monkeypatch.setattr(r, 'getState', lambda: "new") assert r.isVerified() is False monkeypatch.setattr(r, 'getState', lambda: "triaged") assert r.isVerified() is True monkeypatch.setattr(r, 'getState', lambda: "new") monkeypatch.setattr(r, '_getPublicCommentsByUsername', lambda u: ['Comment 1', 'Comment 2', "Message\nMetadata: {\"vulnDomain\": etc..."]) assert r.isVerified() is True
def processStructured(report: ReportWrapper, token: str='') -> VulnTestInfo: """ Process the given report into a VulnTestInfo named tuple given that it contains structured data """ info = extractJson(report.getLatestActivity()) if info is None: return VulnTestInfo(reproduced=False, message=('Failed to parse JSON! Please try again.'), type='Open Redirect', info={'report': report.getLatestActivity()}) # Pass it off to a helper that can try to handle any inconsistencies url, cookies, type, data = extractDataFromJson(info) if not isProgramURL(url): return VulnTestInfo(reproduced=False, message=('The url provided (`%s`) is not a program URL!') % url, type='Open Redirect', info={'src': url, 'method': 'structured'}) if type.lower() == 'post': res = testPOSTOpenRedirect(url, cookies, data) elif type.lower() == 'get': res = testGETOpenRedirect(url, cookies) else: return VulnTestInfo(reproduced=False, message='Found an invalid value "type"=%s in the JSON blob!' % type, type='Open Redirect', info={'src': url, 'method': 'structured'}) if res and token.lower() in urlparse(res).hostname.lower(): return VulnTestInfo(reproduced=True, message=('Successfully found and confirmed an open redirect from `%s` to `%s`!\n' 'Metadata: {"vulnDomain": "%s"}') % (url, res, urlparse(url).hostname), type='Open Redirect', info={'src': url, 'redirect': res, 'method': 'structured', 'httpType': type, 'cookies': cookies}) elif res: return VulnTestInfo(reproduced=False, message=tokenNotFoundMessage % (url, res, token, token), type='Open Redirect', info={'src': url, 'redirect': res, 'method': 'structured'}) else: return VulnTestInfo(reproduced=False, message=("Failed to validate open redirect at `%s` via structured data. Either try again " "or wait for manual review of your bug.") % url, type='Open Redirect', info={'method': 'structured'})
def test_api(): ids = json.loads( requests.post('http://api:8080/v1/getReportIDs', json={ 'time': '1970-01-01T00:00:00Z', 'openOnly': False }, auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text) openIDs = json.loads( requests.post('http://api:8080/v1/getReportIDs', json={ 'time': '1970-01-01T00:00:00Z', 'openOnly': True }, auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text) assert isinstance(ids, list) assert isinstance(openIDs, list) assert len(openIDs) <= len( ids ) # There should be an equal or lesser number of open bugs than all bugs assert all([(id in ids) for id in openIDs]) # All open ids should be in ids for id in ids: # They should be strings but they should be parseable into integers assert isinstance(id, str) and isinstance(int(id), int) # There should be no duplicate IDs assert len(set(ids)) == len(ids) for id in ids[:10]: ser = requests.post('http://api:8080/v1/getReport', json={ 'id': id }, auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text try: r = ReportWrapper().deserialize(ser) except: assert False for serRep in json.loads( requests.post('http://api:8080/v1/getReports', auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text)[:10]: try: r = ReportWrapper().deserialize(serRep) except: assert False assert r.getReportID() in ids
def suggestPayout(report: ReportWrapper) -> Optional[BountyInfo]: """ Returns a BountyInfo containing a suggested payout and the standard deviation for the given report """ if xss.match(report.getReportBody(), report.getReportWeakness()): return suggestPayoutGivenType(config.payoutDB['xss'], report.getVulnDomains()) if openRedirect.match(report.getReportBody(), report.getReportWeakness()): return suggestPayoutGivenType(config.payoutDB['open redirect'], report.getVulnDomains()) if sqli.match(report.getReportBody(), report.getReportWeakness()): return suggestPayoutGivenType(config.payoutDB['sqli'], report.getVulnDomains()) return None
def shouldProcessReport(report: ReportWrapper) -> bool: """ Whether the bot should process the given ReportWrapper """ username = report.getReporterUsername() return (shouldProcess_blacklist(username) and shouldProcess_whitelist(username) and shouldProcess_failures(username) and shouldProcess_match(report))
def getReport(id: str) -> ReportWrapper: """ Get the ReportWrapper describing the report with the given ID number """ resp = requests.post('http://api:8080/v1/getReport', json={'id': id}, auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)) return ReportWrapper().deserialize(Serialized(resp.text))
def test_match(): assert xss.match(ReportWrapper(domXSSInitReport).getReportBody(), ReportWrapper(domXSSInitReport).getReportWeakness()) assert xss.match(ReportWrapper(genericXSSInitReport).getReportBody(), ReportWrapper(genericXSSInitReport).getReportWeakness()) assert xss.match(ReportWrapper(reflectedXSSInitReport).getReportBody(), ReportWrapper(reflectedXSSInitReport).getReportWeakness()) assert xss.match(ReportWrapper(storedXSSInitReport).getReportBody(), ReportWrapper(storedXSSInitReport).getReportWeakness())
def getBody(id: str): ser = requests.post('http://api:8080/v1/getReport', json={ 'id': id }, auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text return ReportWrapper().deserialize(ser).getReportBody() # type: ignore
def processUnstructured(report: ReportWrapper, token: str = '') -> VulnTestInfo: """ Process the given report into a VulnTestInfo named tuple given that it doesn't contain structured data """ urls = extractURLs(report.getLatestActivity()) if config.DEBUG: print("URLs=%s" % str(urls)) if len(urls) > 5: if config.DEBUG: print("User submitted %s URLs. Skipping...") return VulnTestInfo( reproduced=False, message='Found %s URLs. Please resubmit with a single URL to test.', type='SQLi', info={ 'URLs': str(urls), 'method': 'structured' }) testedURLs = [] for url in urls: if isProgramURL(url): # Unstructured reports are treated as a GET delay = testGETSQLDelay(url, {}) if delay and abs(delay - int(token)) < maxTimeDiff: return VulnTestInfo( reproduced=True, message=('Successfully found and confirmed SQLi at `%s`!\n' 'Metadata: {"vulnDomain": "%s"}') % (url, urlparse(url).hostname), type='SQLi', info={ 'src': url, 'method': 'unstructured', 'delay': int(delay), 'httpType': 'GET', 'cookies': {} }) elif delay: return VulnTestInfo(reproduced=False, message=wrongDelayMessage % (str(int(delay)), token, token), type='SQLi', info={ 'src': url, 'method': 'unstructured' }) else: testedURLs.append(url) if len(testedURLs) > 0: return VulnTestInfo(reproduced=False, message=constants.structuredDataMessage % ('SQLi'), type='SQLi', info={'method': 'unstructured'}) else: return VulnTestInfo(reproduced=False, message=constants.failedToFindURLsMessage, type='SQLi', info={'method': 'unstructured'})
def getReport() -> str: """ Get the serialized version of the report at the given ID """ data = request.get_json(force=True) id = data['id'] if config.DEBUGVERBOSE: print("/v1/getReport: id=%s" % id) j = getEndpoint("https://api.hackerone.com/v1/reports/%s" % id) return ReportWrapper(j['data']).serialize()
def rdToRW(r: ReportData) -> ReportWrapper: """ Convert a ReportData to a ReportWrapper """ rw = ReportWrapper() monkeypatch.setattr(rw, 'getReportTitle', lambda: r.title) monkeypatch.setattr(rw, 'getReportBody', lambda: r.body) monkeypatch.setattr(rw, 'getReportedTime', lambda: r.time) monkeypatch.setattr(rw, 'getState', lambda: r.state) monkeypatch.setattr(rw, 'getReportID', lambda: r.id) monkeypatch.setattr(rw, 'getReportWeakness', lambda: r.weakness) return rw
def getAllOpenReports(time: datetime) -> List[ReportWrapper]: """ Get a list of all the open reports """ reports = [ ReportWrapper().deserialize(ser) for ser in json.loads( requests.post('http://api:8080/v1/getReports', auth=HTTPBasicAuth('AutoTriageBot', secrets.apiBoxToken)).text) ] return list( filter(lambda r: r.getReportedTime() < time, (filter( lambda r: r.getState() in ['new', 'triaged', 'needs-more-info'], reports))))
def processUnstructured(report: ReportWrapper, token: str='') -> AutoTriageUtils.VulnTestInfo: """ Process the given report into a AutoTriageUtils.VulnTestInfo named tuple given that it doesn't contain structured data """ urls = extractURLs(report.getLatestActivity()) if config.DEBUG: print("URLs=%s" % str(urls)) if len(urls) > 5: if config.DEBUG: print("User submitted %s URLs. Skipping...") return VulnTestInfo(reproduced=False, message='Found %s URLs. Please resubmit with a single URL to test.', type='XSS', info={'URLs': str(urls), 'method': 'structured'}) testedURLs = [] for url in urls: if AutoTriageUtils.isProgramURL(url): testedURLs.append(url) results = testGETXSS(url, {}) reproduced, alertBox, message, confirmedBrowsers, alertBrowsers = makeMarkdownTable(results, token) if reproduced: return VulnTestInfo(reproduced=True, message=('Successfully found and confirmed an XSS at `%s`!\n' '\n\n%s\n\n' 'Metadata: {"vulnDomain": "%s"}') % (url, message, urlparse(url).hostname), type='XSS', info={'src': url, 'method': 'unstructured', 'confirmedBrowsers': confirmedBrowsers, 'alertBrowsers': alertBrowsers, 'httpType': 'GET', 'cookies': {}}) elif alertBox: return VulnTestInfo(reproduced=False, message=('Failed to confirm the vulnerability! Detected an alert ' 'box but the token: `"%s"` was not found!' '\n\n%s\n\n') % (token, message), type='XSS', info={'src': url, 'method': 'unstructured'}) if len(testedURLs) > 0: return VulnTestInfo(reproduced=False, message=constants.structuredDataMessage % 'XSS', type='XSS', info={'method': 'unstructured'}) else: return VulnTestInfo(reproduced=False, message=constants.failedToFindURLsMessage, type='XSS', info={'method': 'unstructured'})
def getReports() -> str: """ Get all of the reports on the program - For H1, getReports *is* different from [getReport(id) for id in getReportIDs(0)] because the /v1/getReports API endpoint returns all the reports at once, but the comments are not included. So if you need access to the comments, use getReport(id). But if you only need the report body then getReports is faster since it does not make as many requests. Returns string encoded JSON that is a list of serialized ReportWrappers """ if config.DEBUGVERBOSE: print("/v1/getReports") url = "https://api.hackerone.com/v1/reports?filter[program][]=%s&page[size]=100" % config.programName return json.dumps( [ReportWrapper(j).serialize() for j in getEndpointPaginated(url)])
def test_verifyProcess(monkeypatch): from AutoTriageBot import verify time = datetime.datetime.now() monkeypatch.setattr(verify, 'postComment', Counter()) report = ReportWrapper() monkeypatch.setattr(report, 'needsBotReply', lambda: False) assert verify.postComment.count == 0 assert verify.processReport(report, time) is None assert verify.postComment.count == 0 monkeypatch.setattr(report, 'needsBotReply', lambda: True) monkeypatch.setattr(report, 'getReportedTime', lambda: datetime.datetime.now()) monkeypatch.setattr( verify.config, 'genesis', datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)) monkeypatch.setattr(verify.config, 'DEBUG', False) monkeypatch.setattr(report, 'getReportBody', lambda: "XSS report") monkeypatch.setattr(report, 'getReportTitle', lambda: "XSS report") monkeypatch.setattr(report, 'getReportWeakness', lambda: "XSS") monkeypatch.setattr(report, 'getReportID', lambda: '-1') vti = VulnTestInfo(reproduced=False, message="VTI", info={}, type='type') for module in verify.modules: monkeypatch.setattr(module, 'process', lambda r: vti) monkeypatch.setattr(module, 'match', lambda u, v: True) monkeypatch.setattr(report, 'needsBotReply', lambda: True) assert report.needsBotReply() assert verify.postComment.count == 0 assert verify.processReport(report, time) == vti assert verify.postComment.count == 1 assert verify.postComment.lastCall == (('-1', vti), { 'addStopMessage': True }) for module in verify.modules: monkeypatch.setattr(module, 'match', lambda b, w: False) assert verify.postComment.count == 1 assert verify.processReport(report, time) is None assert verify.postComment.count == 1
def generateMetadataVTI(report: ReportWrapper, vti: VulnTestInfo) -> VulnTestInfo: """ Given the results of a vulnerability test thar reproduced a vulnerability and a report, generate an internal VTI used to hold metadata about the vulnerability """ internalMetadata = {'id': report.getReportID(), 'title': report.getReportTitle(), 'reportedTime': str(report.getReportedTime()), 'verifiedTime': str(datetime.now()), 'type': vti.type, 'exploitURL': vti.info['src'], 'method': vti.info['method']} if vti.type == 'XSS': internalMetadata['confirmedBrowsers'] = vti.info['confirmedBrowsers'] internalMetadata['alertBrowsers'] = vti.info['alertBrowsers'] internalMetadata['httpType'] = vti.info['httpType'] internalMetadata['cookies'] = vti.info['cookies'] elif vti.type == 'SQLi': internalMetadata['delay'] = vti.info['delay'] internalMetadata['httpType'] = vti.info['httpType'] internalMetadata['cookies'] = vti.info['cookies'] elif vti.type == 'Open Redirect': internalMetadata['redirect'] = vti.info['redirect'] internalMetadata['httpType'] = vti.info['httpType'] internalMetadata['cookies'] = vti.info['cookies'] message = '# Internal Metadata: \n\n```\n%s\n```\n' % json.dumps(internalMetadata, sort_keys=True, indent=4, separators=(',', ': ')) if config.DEBUGVERBOSE: print(internalMetadata) internalVTI = VulnTestInfo(reproduced=False, message=message, info={}, type='') return internalVTI
def processReport(report: ReportWrapper) -> None: """ Process the given report and post a private comment with a suggested bounty """ if config.payoutDB: bountyInfo = suggestPayout(report) if bountyInfo: postComment(report.getReportID(), VulnTestInfo( reproduced=False, info={}, message='Suggested bounty: %.2f with a σ of %.2f' % (bountyInfo.average, bountyInfo.std), type=''), internal=True) else: if config.DEBUGVERBOSE: print("Not suggesting a payout beause config.payoutDB is falsy")
def processUnstructured(report: ReportWrapper, token: str='') -> VulnTestInfo: """ Process the given report into a VulnTestInfo named tuple given that it doesn't contain structured data """ urls = extractURLs(report.getLatestActivity()) if config.DEBUG: print("URLs=%s" % str(urls)) if len(urls) > 5: if config.DEBUG: print("User submitted %s URLs. Skipping...") return VulnTestInfo(reproduced=False, message=('Found %s URLs. Please resubmit with a single URL to test.'), type='Open Redirect', info={'URLs': str(urls), 'method': 'structured'}) testedURLs = [] for url in urls: if isProgramURL(url): res = testGETOpenRedirect(url, {}) print("res=%s" % str(urlparse(res).hostname)) if res and token.lower() in urlparse(res).hostname.lower(): return VulnTestInfo(reproduced=True, message=('Successfully found and confirmed an open redirect from `%s` to `%s`!\n' 'Metadata: {"vulnDomain": "%s"}') % (url, res, urlparse(url).hostname), type='Open Redirect', info={'src': url, 'redirect': res, 'method': 'unstructured', 'httpType': 'GET', 'cookies': {}}) # nopep8 elif res: return VulnTestInfo(reproduced=False, message=tokenNotFoundMessage % (url, res, token, token), type='Open Redirect', info={'src': url, 'redirect': res, 'method': 'unstructured'}) else: testedURLs.append(url) if len(testedURLs) > 0: return VulnTestInfo(reproduced=False, message=constants.structuredDataMessage % 'open redirect', type='Open Redirect', info={'method': 'unstructured'}) else: return VulnTestInfo(reproduced=False, message=constants.failedToFindURLsMessage, type='Open Redirect', info={'method': 'unstructured'})
def processReport(report: ReportWrapper, startTime: datetime) -> Optional[VulnTestInfo]: """ Attempt to verify a given report """ if report.needsBotReply(): if startTime > report.getReportedTime(): return None if config.DEBUG: print("Processing %s" % report.getReportTitle()) for module in modules: if module.match(report.getReportBody(), report.getReportWeakness()): # type: ignore if config.DEBUG: print(module.__file__.split('/')[-1] + " matched id=%s!" % report.getReportID()) vti = module.process(report) # type: ignore if config.DEBUGVERBOSE: print(vti) if vti: postComment(report.getReportID(), vti, addStopMessage=True) if vti.reproduced and config.metadataLogging: metadataVTI = generateMetadataVTI(report, vti) postComment(report.getReportID(), metadataVTI, internal=True) return vti if config.DEBUG: print("No matches") return None
def test_processUnstructured(monkeypatch): monkeypatch.setattr(sqli, 'isProgramURL', lambda u: True) report = ReportWrapper() monkeypatch.setattr(report, 'isVerified', lambda: False) monkeypatch.setattr(report, 'botHasCommented', lambda: False) monkeypatch.setattr(sqli, 'getRandInt', lambda: '12') assert sqli.process(report) == VulnTestInfo(reproduced=False, message=sqli.initialMessage % ('12', '12', '12'), type='SQLi', info={}) monkeypatch.setattr(report, 'botHasCommented', lambda: True) monkeypatch.setattr(report, 'shouldBackoff', lambda: True) monkeypatch.setattr(report, 'hasPostedBackoffComment', lambda: False) monkeypatch.setattr(report, 'getReporterUsername', lambda: 'TestFailureUser') monkeypatch.setattr(report, 'getReportID', lambda: '-1') oldCount = sqlite.countFailures("TestFailureUser") assert sqli.process(report) == VulnTestInfo( reproduced=False, message=('Automatic verification of vulnerability has failed, Backing ' 'off! Falling ' 'back to human verification. '), type='SQLi', info={}) assert sqlite.countFailures("TestFailureUser") == (oldCount + 1) monkeypatch.setattr(report, 'hasPostedBackoffComment', lambda: True) assert sqli.process(report) is None monkeypatch.setattr(report, 'shouldBackoff', lambda: False) monkeypatch.setattr(report, 'getLatestActivity', lambda: "") monkeypatch.setattr(report, 'getToken', lambda: "12") monkeypatch.setattr(report, 'isVerified', lambda: True) assert sqli.process(report) is None monkeypatch.setattr(report, 'isVerified', lambda: False) assert (sqli.process(report) == sqli.processUnstructured( report, token=report.getToken()) == VulnTestInfo( reproduced=False, message=constants.failedToFindURLsMessage, type='SQLi', info={'method': 'unstructured'})) monkeypatch.setattr( report, 'getLatestActivity', lambda: ("```\n" "http://vulnserver/sqli.php?q=12\n" "```")) vti = sqli.process(report) assert vti.reproduced is True monkeypatch.setattr( report, 'getLatestActivity', lambda: ("```\n" "http://vulnserver/noVulnerability.html" "```\n" "```\n" "http://vulnserver/sqli.php?q=12\n" "```")) vti = sqli.process(report) assert sqli.process(report) == sqli.processUnstructured( report, token=report.getToken()) == vti assert vti.reproduced is True monkeypatch.setattr( report, 'getLatestActivity', lambda: ("```\n" "http://vulnserver/sqli.php?q=15\n" "```")) vti = sqli.process(report) assert vti.reproduced is False assert vti.message == (sqli.wrongDelayMessage % ('15', '12', '12'))
def test_processStructured(monkeypatch): monkeypatch.setattr(sqli, 'isProgramURL', lambda u: True) report = ReportWrapper() monkeypatch.setattr(report, 'isVerified', lambda: False) monkeypatch.setattr(sqli, 'getRandInt', lambda: '12') monkeypatch.setattr(report, 'botHasCommented', lambda: True) monkeypatch.setattr(report, 'shouldBackoff', lambda: False) monkeypatch.setattr(report, 'getLatestActivity', lambda: "") monkeypatch.setattr(report, 'getToken', lambda: "12") monkeypatch.setattr( report, 'getLatestActivity', lambda: ('# AutoTriage Structured Data: \n' '```\n' '{No JSON!}' '```\n\n')) vti = sqli.process(report) assert vti.reproduced is False assert 'Failed to parse JSON! Please try again.' in vti.message monkeypatch.setattr(sqli, 'isProgramURL', lambda u: False) monkeypatch.setattr( report, 'getLatestActivity', lambda: ('# AutoTriage Structured Data: \n' '```\n' '{\n' ' "URL": "http://vulnserver/sqliIfCookie.php?q=12",\n' ' "cookies": {"NAME": "VALUE"}, \n' ' "type": "get" \n' '}\n' '```\n\n')) vti = sqli.process(report) assert vti.reproduced is False assert 'is not a program URL!' in vti.message monkeypatch.setattr(sqli, 'isProgramURL', lambda u: True) assert 12 < sqli.testGETSQLDelay('http://vulnserver/sqliIfCookie.php?q=12', {'NAME': 'VALUE'}) < 13 vti = sqli.process(report) assert vti.reproduced is True assert 'Successfully found and confirmed SQLi at' in vti.message monkeypatch.setattr( report, 'getLatestActivity', lambda: ('# AutoTriage Structured Data: \n' '```\n' '{\n' ' "URL": "http://vulnserver/sqliIfCookiePost.php",\n' ' "cookies": {"NAME": "VALUE"}, \n' ' "type": "post", \n' ' "data": {"q": "12"} \n' '}\n' '```\n')) vti = sqli.process(report) assert vti.reproduced is True assert 'Successfully found and confirmed SQLi at' in vti.message monkeypatch.setattr( report, 'getLatestActivity', lambda: ('# AutoTriage Structured Data: \n' '```\n' '{\n' ' "URL": "http://vulnserver/sqliIfCookiePost.php",\n' ' "cookies": {"NAME": "VALUE"}, \n' ' "type": "INVALID", \n' ' "data": {"q": "12"} \n' '}\n' '```\n')) vti = sqli.process(report) assert vti.reproduced is False assert 'Found an invalid value' in vti.message monkeypatch.setattr( report, 'getLatestActivity', lambda: ('# AutoTriage Structured Data: \n' '```\n' '{\n' ' "URL": "http://vulnserver/sqliIfCookiePost.php",\n' ' "cookies": {"NAME": "VALUE"}, \n' ' "type": "post", \n' ' "data": {"q": "18"} \n' '}\n' '```\n')) vti = sqli.process(report) assert vti.reproduced is False assert "In order to verify the vulnerability, it must have delayed for" in vti.message monkeypatch.setattr( report, 'getLatestActivity', lambda: ('# AutoTriage Structured Data: \n' '```\n' '{\n' ' "URL": "http://vulnserver/sqliIfCookiePost.php",\n' ' "cookies": {"NAME": "WRONG"}, \n' ' "type": "post", \n' ' "data": {"q": "12"} \n' '}\n' '```\n')) vti = sqli.process(report) assert vti.reproduced is False assert "Failed to validate SQLi at" in vti.message
def sameCategoryIsDuplicate(r1: ReportWrapper, r2: ReportWrapper, containsExploit: Callable[[str], bool]) -> \ DuplicateResult: """ Returns a confidence rating on whether the two given reports are duplicates of each other given that they are of the same type of vulnerability and that containsExploit returns whether or not a given URL is exploiting that class of vulnerability. """ # The links are the only things we refer to in our current duplicate detection algorithm links1, links2 = getLinks(r1.getReportBody()), getLinks(r2.getReportBody()) malLinks1 = [ link for link in links1 if containsExploit(link) or containsExploit(unquote(link)) ] malLinks2 = [ link for link in links2 if containsExploit(link) or containsExploit(unquote(link)) ] if set(malLinks1) & set(malLinks2): return DuplicateResult((99, ID('B'))) if set(links1) & set(links2): return DuplicateResult((90, ID('C'))) parsedMalLinks1 = list( filter(lambda n: n, map(AutoTriageUtils.parseURL, malLinks1))) parsedMalLinks2 = list( filter(lambda n: n, map(AutoTriageUtils.parseURL, malLinks2))) parsedLinks1 = list( filter(lambda n: n, map(AutoTriageUtils.parseURL, links1))) parsedLinks2 = list( filter(lambda n: n, map(AutoTriageUtils.parseURL, links2))) malDomainParameterTuples1 = flatten([[(x.domain, x.path, key) for key, val in x.queries.items() if containsExploit(val)] for x in parsedMalLinks1]) malDomainParameterTuples2 = flatten([[(x.domain, x.path, key) for key, val in x.queries.items() if containsExploit(val)] for x in parsedMalLinks2]) parametersInCommon = ( set(flatten([parsed.queries.keys() for parsed in parsedLinks1])) & set(flatten([parsed.queries.keys() for parsed in parsedLinks2]))) malParametersInCommon = ( set(flatten([parsed.queries.keys() for parsed in parsedMalLinks1])) & set(flatten([parsed.queries.keys() for parsed in parsedMalLinks2]))) injectionParametersInCommon = ( set([param for domain, path, param in malDomainParameterTuples1]) & set([param for domain, path, param in malDomainParameterTuples2])) malPathsInCommon = (set([ path for domain, path, param in malDomainParameterTuples1 if path != '' ]) & set([ path for domain, path, param in malDomainParameterTuples2 if path != '' ])) pathsInCommon = ( set([parsed.path for parsed in parsedLinks1 if parsed.path != '']) & set([parsed.path for parsed in parsedLinks2 if parsed.path != ''])) domains1 = set( [x.domain for x in parsedLinks1 if '[server]' not in x.domain]) domains2 = set( [x.domain for x in parsedLinks2 if '[server]' not in x.domain]) domainsInCommon = domains1 & domains2 malDomainsInCommon = ( set([x.domain for x in parsedMalLinks1 if '[server]' not in x.domain]) & set([x.domain for x in parsedMalLinks2 if '[server]' not in x.domain])) return decide(len(malLinks1), len(malLinks2), len(parametersInCommon), len(malParametersInCommon), len(pathsInCommon), len(malPathsInCommon), len(domainsInCommon), len(malDomainsInCommon), len(injectionParametersInCommon), len(domains1 ^ domains2))
def shouldProcess_match(report: ReportWrapper) -> bool: """ Whether the bot should process the given ReportWrapper according to whether any of the modules match it """ return any([ m.match(report.getReportBody(), report.getReportWeakness()) for m in modules ]) # type: ignore
def processReport(report: ReportWrapper) -> bool: """ Process a report via searching for duplicates and posting comments based off of the confidence levels Returns whether or not the report was classified as a duplicate with a high confidence """ if report.getState() == "new" and not report.hasDuplicateComment( ) and not report.isVerified(): earlierReports = getAllOpenReports( report.getReportedTime()) # type: List[ReportWrapper] idConfTuples = [] # type: List[Tuple[str, int]] matches = [] # type: List[str] for earlierReport in earlierReports: for module in modules: if (module.match(report.getReportBody(), report.getReportWeakness()) and # type: ignore module.match(earlierReport.getReportBody(), earlierReport.getReportWeakness()) ): # type: ignore matches.append(earlierReport.getReportID()) try: confidence = int(isDuplicate(earlierReport, report)[0]) except TypeError: confidence = 0 if confidence == 99: AutoTriageUtils.postComment( report.getReportID(), VulnTestInfo( message='Found a duplicate with 99%% confidence: #%s' % earlierReport.getReportID(), info={}, reproduced=False, type=''), internal=True) if config.DEBUG: print("Detected that %s (%s) is a duplicate of %s (%s)!" % (report.getReportID(), report.getReportTitle(), earlierReport.getReportID(), earlierReport.getReportTitle())) return False # Change to return True to make the bot stop interacting after finding a duplicate elif confidence > 50: idConfTuples.append((earlierReport.getReportID(), confidence)) # If you update the phrases here, you must also update them in AutoTriageUtils.ReportWrapper.hasDuplicateComment if len(idConfTuples) > 0: def idConfToStr(tuple: Tuple) -> str: return ( 'Detected a possible duplicate report with confidence of %s: #%s' % (tuple[1], tuple[0])) AutoTriageUtils.postComment(report.getReportID(), VulnTestInfo(message='\n'.join([ idConfToStr(t) for t in idConfTuples ]), info={}, reproduced=False, type=''), internal=True) if config.DEBUG: print('Found partial matches: %s' % str(idConfTuples)) if len(matches) > 0 and len(matches) <= 5: AutoTriageUtils.postComment( report.getReportID(), VulnTestInfo(message=( 'There are currently %s open reports about this type of ' 'vulnerability: %s' % (str(len(matches)), ', '.join(['#' + id for id in matches]))), info={}, reproduced=False, type=''), internal=True) if config.DEBUG: print( 'Found %s reports on the same type of vulnerability as %s: %s' % (str(len(matches)), str(report.getReportID()), ', '.join( ['#' + id for id in matches]))) return False
def test_metadataLogging(monkeypatch): from AutoTriageBot import verify mvti = VulnTestInfo(reproduced=True, message='', type='XSS', info={ 'src': 'AAA', 'method': 'BBB', 'confirmedBrowsers': 'CCC', 'alertBrowsers': 'DDD', 'httpType': 'EEE', 'cookies': 'FFF' }) r = ReportWrapper() monkeypatch.setattr(r, 'getReportID', lambda: 'GGG') monkeypatch.setattr(r, 'getReportTitle', lambda: 'HHH') monkeypatch.setattr(r, 'getReportedTime', lambda: 'III') ivti = verify.generateMetadataVTI(r, mvti) j = extractJson(ivti.message) def standardAsserts(j): assert j['id'] == 'GGG' assert j['title'] == 'HHH' assert j['reportedTime'] == 'III' assert 'verifiedTime' in j.keys( ) # we can't monkeypatch datetime, so just checking that it exists assert j['exploitURL'] == 'AAA' assert j['method'] == 'BBB' assert j['httpType'] == 'EEE' assert j['cookies'] == 'FFF' # XSS: standardAsserts(j) assert j['type'] == 'XSS' assert j['confirmedBrowsers'] == 'CCC' assert j['alertBrowsers'] == 'DDD' # SQLi: mvti = VulnTestInfo(reproduced=True, message='', type='SQLi', info={ 'src': 'AAA', 'method': 'BBB', 'delay': '12', 'httpType': 'EEE', 'cookies': 'FFF' }) ivti = verify.generateMetadataVTI(r, mvti) j = extractJson(ivti.message) standardAsserts(j) assert j['type'] == 'SQLi' assert j['delay'] == '12' # Open Redirect: mvti = VulnTestInfo(reproduced=True, message='', type='Open Redirect', info={ 'src': 'AAA', 'method': 'BBB', 'redirect': 'CCC', 'httpType': 'EEE', 'cookies': 'FFF' }) ivti = verify.generateMetadataVTI(r, mvti) j = extractJson(ivti.message) standardAsserts(j) assert j['type'] == 'Open Redirect' assert j['redirect'] == 'CCC'
def test_process(monkeypatch): r = ReportWrapper() monkeypatch.setattr(r, 'getState', lambda: 'not new') assert duplicates.processReport(r) is False monkeypatch.setattr(r, 'getState', lambda: 'new') monkeypatch.setattr(r, 'hasDuplicateComment', lambda: True) assert duplicates.processReport(r) is False monkeypatch.setattr(r, 'getReportTitle', lambda: 'Title') monkeypatch.setattr(r, 'getReportBody', lambda: 'Body') monkeypatch.setattr(r, 'getReportedTime', lambda: None) monkeypatch.setattr(r, 'getReportID', lambda: '0') monkeypatch.setattr(r, 'getReportWeakness', lambda: 'Weakness') monkeypatch.setattr(r, 'isVerified', lambda: False) monkeypatch.setattr(r, 'hasDuplicateComment', lambda: False) monkeypatch.setattr(duplicates, 'getAllOpenReports', lambda a: []) c = Counter() monkeypatch.setattr(duplicates.AutoTriageUtils, 'postComment', c) assert c.count == 0 assert duplicates.processReport(r) is False assert c.count == 0 # We don't post comments when there are no duplicate reports r2 = ReportData(title='A', body='A', time=None, state='new', id='1', weakness='XSS') def rdToRW(r: ReportData) -> ReportWrapper: """ Convert a ReportData to a ReportWrapper """ rw = ReportWrapper() monkeypatch.setattr(rw, 'getReportTitle', lambda: r.title) monkeypatch.setattr(rw, 'getReportBody', lambda: r.body) monkeypatch.setattr(rw, 'getReportedTime', lambda: r.time) monkeypatch.setattr(rw, 'getState', lambda: r.state) monkeypatch.setattr(rw, 'getReportID', lambda: r.id) monkeypatch.setattr(rw, 'getReportWeakness', lambda: r.weakness) monkeypatch.setattr(rw, 'isVerified', lambda: False) return rw r2 = rdToRW(r2) monkeypatch.setattr(duplicates, 'getAllOpenReports', lambda a: [r2]) monkeypatch.setattr(duplicates.modules[0], 'match', lambda a, b: True) monkeypatch.setattr(duplicates, 'isDuplicate', lambda a, b: (99, 'A')) c = Counter() monkeypatch.setattr(duplicates.AutoTriageUtils, 'postComment', c) assert c.count == 0 assert duplicates.processReport(r) is False assert c.count == 1 assert c.lastCall == (('0', VulnTestInfo(message=( 'Found a duplicate with 99% confidence: #1'), info={}, reproduced=False, type='')), { 'internal': True }) monkeypatch.setattr(duplicates, 'isDuplicate', lambda a, b: (50, 'A')) c = Counter() monkeypatch.setattr(duplicates.AutoTriageUtils, 'postComment', c) assert c.count == 0 assert duplicates.processReport(r) is False assert c.count == 1 assert c.lastCall == (('0', VulnTestInfo(message=( 'There are currently 1 open reports about' ' this type of vulnerability: #1'), info={}, reproduced=False, type='')), { 'internal': True }) monkeypatch.setattr(duplicates.modules[0], 'match', lambda a, b: False) monkeypatch.setattr(duplicates.modules[1], 'match', lambda a, b: False) monkeypatch.setattr(duplicates.modules[2], 'match', lambda a, b: False) monkeypatch.setattr(duplicates, 'isDuplicate', lambda a, b: (0, 'A')) c = Counter() monkeypatch.setattr(duplicates.AutoTriageUtils, 'postComment', c) assert c.count == 0 assert duplicates.processReport(r) is False assert c.count == 0