diff --git a/.gitignore b/.gitignore index 7e26c41..288d2aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ api/uploads/* api/logs/* gui/guienv/* *.pyc -.DS_Store -api/uploads/* +**/.DS_Store +**/config.yaml env diff --git a/api/apiserver.py b/api/apiserver.py index 336c09f..347ef5e 100755 --- a/api/apiserver.py +++ b/api/apiserver.py @@ -16,6 +16,12 @@ import sys import os import io +import boto3 +import datetime +from botocore.exceptions import ClientError +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.application import MIMEApplication from models.initiate_database import * from tornado import gen @@ -26,26 +32,28 @@ from models.collected_page import CollectedPage from binascii import a2b_base64 -logging.basicConfig(filename="logs/detailed.log",level=logging.DEBUG) +logging.basicConfig(filename="logs/detailed.log", level=logging.DEBUG) try: - with open( '../config.yaml', 'r' ) as f: - settings = yaml.safe_load( f ) + with open('../config.yaml', 'r') as f: + settings = yaml.safe_load(f) except IOError: print "Error reading config.yaml, have you created one? (Hint: Try running ./generate_config.py)" exit() -CSRF_EXEMPT_ENDPOINTS = [ "/api/contactus", "/api/register", "/", "/api/login", "/health", "/favicon.ico", "/page_callback", "/api/record_injection" ] -FORBIDDEN_SUBDOMAINS = [ "www", "api" ] +CSRF_EXEMPT_ENDPOINTS = ["/api/contactus", "/api/register", "/", "/api/login", + "/health", "/favicon.ico", "/page_callback", "/api/record_injection"] +FORBIDDEN_SUBDOMAINS = ["www", "api"] -with open( "probe.js", "r" ) as probe_handler: +with open("probe.js", "r") as probe_handler: probejs = probe_handler.read() + class BaseHandler(tornado.web.RequestHandler): def __init__(self, *args, **kwargs): super(BaseHandler, self).__init__(*args, **kwargs) - if self.request.uri.startswith( "/api/" ): + if self.request.uri.startswith("/api/"): self.set_header("Content-Type", "application/json") else: self.set_header("Content-Type", "application/javascript") @@ -54,109 +62,118 @@ def __init__(self, *args, **kwargs): self.set_header("Content-Security-Policy", "default-src 'self'") self.set_header("X-XSS-Protection", "1; mode=block") self.set_header("X-Content-Type-Options", "nosniff") - self.set_header("Access-Control-Allow-Headers", "X-CSRF-Token, Content-Type") - self.set_header("Access-Control-Allow-Origin", "https://www." + settings["domain"]) - self.set_header("Access-Control-Allow-Methods", "OPTIONS, PUT, DELETE, POST, GET") + self.set_header("Access-Control-Allow-Headers", + "X-CSRF-Token, Content-Type") + self.set_header("Access-Control-Allow-Origin", + "https://www." + settings["domain"]) + self.set_header("Access-Control-Allow-Methods", + "OPTIONS, PUT, DELETE, POST, GET") self.set_header("Access-Control-Allow-Credentials", "true") 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" ) + self.request.remote_ip = self.request.headers.get("X-Forwarded-For") - if not self.validate_csrf_token() and self.request.uri not in CSRF_EXEMPT_ENDPOINTS and not self.request.uri.startswith( "/b" ): - self.error( "Invalid CSRF token provided!" ) - self.logit( "Someone did a request with an invalid CSRF token!", "warn") + if not self.validate_csrf_token() and self.request.uri not in CSRF_EXEMPT_ENDPOINTS and not self.request.uri.startswith("/b"): + self.error("Invalid CSRF token provided!") + self.logit( + "Someone did a request with an invalid CSRF token!", "warn") self.finish() return - def logit( self, message, message_type="info" ): - user_id = self.get_secure_cookie( "user" ) + def logit(self, message, message_type="info"): + user_id = self.get_secure_cookie("user") if user_id != None: - user = session.query( User ).filter_by( id=user_id ).first() + user = session.query(User).filter_by(id=user_id).first() if user != None: message = "[" + user.username + "]" + message message = "[" + self.request.remote_ip + "] " + message if message_type == "info": - logging.info( message ) + logging.info(message) elif message_type == "warn": - logging.warn( message ) + logging.warn(message) elif message_type == "debug": - logging.debug( message ) + logging.debug(message) else: - logging.info( message ) + logging.info(message) def options(self): pass # Hack to stop Tornado from sending the Etag header - def compute_etag( self ): + def compute_etag(self): return None - def throw_404( self ): + def throw_404(self): self.set_status(404) self.write("Resource not found") - def on_finish( self ): + def on_finish(self): session.close() - def validate_csrf_token( self ): - csrf_token = self.get_secure_cookie( "csrf" ) + def validate_csrf_token(self): + csrf_token = self.get_secure_cookie("csrf") if csrf_token == None: return True - if self.request.headers.get( 'X-CSRF-Token' ) == csrf_token: + if self.request.headers.get('X-CSRF-Token') == csrf_token: return True - if self.get_argument( 'csrf', False ) == csrf_token: + if self.get_argument('csrf', False) == csrf_token: return True return False - def validate_input( self, required_field_list, input_dict ): + def validate_input(self, required_field_list, input_dict): for field in required_field_list: if field not in input_dict: - self.error( "Missing required field '" + field + "', this endpoint requires the following parameters: " + ', '.join( required_field_list ) ) + self.error("Missing required field '" + field + + "', this endpoint requires the following parameters: " + ', '.join(required_field_list)) return False - if input_dict[ field ] == "": - self.error( "Missing required field '" + field + "', this endpoint requires the following parameters: " + ', '.join( required_field_list ) ) + if input_dict[field] == "": + self.error("Missing required field '" + field + + "', this endpoint requires the following parameters: " + ', '.join(required_field_list)) return False return True - def error( self, error_message ): + def error(self, error_message): self.write(json.dumps({ "success": False, "error": error_message })) - def get_authenticated_user( self ): - user_id = self.get_secure_cookie( "user" ) + def get_authenticated_user(self): + user_id = self.get_secure_cookie("user") if user_id == None: - self.error( "You must be authenticated to perform this action!" ) - return session.query( User ).filter_by( id=user_id ).first() + self.error("You must be authenticated to perform this action!") + return session.query(User).filter_by(id=user_id).first() - def get_user_from_subdomain( self ): - domain = self.request.headers.get( 'Host' ) - domain_parts = domain.split( "." + settings["domain"] ) + def get_user_from_subdomain(self): + domain = self.request.headers.get('Host') + domain_parts = domain.split("." + settings["domain"]) subdomain = domain_parts[0] - return session.query( User ).filter_by( domain=subdomain ).first() + return session.query(User).filter_by(domain=subdomain).first() + -def data_uri_to_file( data_uri ): +def data_uri_to_file(data_uri): """ Turns the canvas data URI into a file handler """ - raw_base64 = data_uri.replace( 'data:image/png;base64,', '' ) - binary_data = a2b_base64( raw_base64 ) - f = io.BytesIO( binary_data ) + raw_base64 = data_uri.replace('data:image/png;base64,', '') + binary_data = a2b_base64(raw_base64) + f = io.BytesIO(binary_data) return f -def pprint( input_dict ): + +def pprint(input_dict): print json.dumps(input_dict, sort_keys=True, indent=4, separators=(',', ': ')) + class GetXSSPayloadFiresHandler(BaseHandler): """ Endpoint for querying for XSS payload fire data. @@ -167,104 +184,203 @@ class GetXSSPayloadFiresHandler(BaseHandler): offset limit """ - def get( self ): - self.logit( "User retrieved their injection results" ) + + def get(self): + self.logit("User retrieved their injection results") user = self.get_authenticated_user() - offset = abs( int( self.get_argument('offset', default=0 ) ) ) - limit = abs( int( self.get_argument('limit', default=25 ) ) ) - results = session.query( Injection ).filter_by( owner_id = user.id ).order_by( Injection.injection_timestamp.desc() ).limit( limit ).offset( offset ) - total = session.query( Injection ).filter_by( owner_id = user.id ).count() + offset = abs(int(self.get_argument('offset', default=0))) + limit = abs(int(self.get_argument('limit', default=25))) + results = session.query(Injection).filter_by(owner_id=user.id).order_by( + Injection.injection_timestamp.desc()).limit(limit).offset(offset) + total = session.query(Injection).filter_by(owner_id=user.id).count() return_list = [] for result in results: - return_list.append( result.get_injection_blob() ) + return_list.append(result.get_injection_blob()) return_dict = { "results": return_list, "total": total, "success": True } - self.write( json.dumps( return_dict ) ) + self.write(json.dumps(return_dict)) -def upload_screenshot( base64_screenshot_data_uri ): - screenshot_filename = "uploads/xsshunter_screenshot_" + binascii.hexlify( os.urandom( 100 ) ) + ".png" - screenshot_file_handler = data_uri_to_file( base64_screenshot_data_uri ) - local_file_handler = open( screenshot_filename, "w" ) # Async IO http://stackoverflow.com/a/13644499/1195812 - local_file_handler.write( screenshot_file_handler.read() ) + +def upload_screenshot(base64_screenshot_data_uri): + screenshot_filename = "uploads/xsshunter_screenshot_" + \ + binascii.hexlify(os.urandom(100)) + ".png" + screenshot_file_handler = data_uri_to_file(base64_screenshot_data_uri) + # Async IO http://stackoverflow.com/a/13644499/1195812 + local_file_handler = open(screenshot_filename, "w") + local_file_handler.write(screenshot_file_handler.read()) local_file_handler.close() return screenshot_filename -def record_callback_in_database( callback_data, request_handler ): - screenshot_file_path = upload_screenshot( callback_data["screenshot"] ) - - injection = Injection( vulnerable_page=callback_data["uri"].encode("utf-8"), - victim_ip=callback_data["ip"].encode("utf-8"), - referer=callback_data["referrer"].encode("utf-8"), - user_agent=callback_data["user-agent"].encode("utf-8"), - cookies=callback_data["cookies"].encode("utf-8"), - dom=callback_data["dom"].encode("utf-8"), - origin=callback_data["origin"].encode("utf-8"), - screenshot=screenshot_file_path.encode("utf-8"), - injection_timestamp=int(time.time()), - browser_time=int(callback_data["browser-time"]) - ) + +def record_callback_in_database(callback_data, request_handler): + screenshot_file_path = upload_screenshot(callback_data["screenshot"]) + + injection = Injection(vulnerable_page=callback_data["uri"].encode("utf-8"), + victim_ip=callback_data["ip"].encode("utf-8"), + referer=callback_data["referrer"].encode("utf-8"), + user_agent=callback_data["user-agent"].encode( + "utf-8"), + cookies=callback_data["cookies"].encode("utf-8"), + dom=callback_data["dom"].encode("utf-8"), + origin=callback_data["origin"].encode("utf-8"), + screenshot=screenshot_file_path.encode("utf-8"), + injection_timestamp=int(time.time()), + browser_time=int(callback_data["browser-time"]) + ) injection.generate_injection_id() owner_user = request_handler.get_user_from_subdomain() injection.owner_id = owner_user.id # Check if this is correlated to someone's request. if callback_data["injection_key"] != "[PROBE_ID]": - correlated_request_entry = session.query( InjectionRequest ).filter_by( injection_key=callback_data["injection_key"] ).filter_by( owner_correlation_key=owner_user.owner_correlation_key ).first() + correlated_request_entry = session.query(InjectionRequest).filter_by( + injection_key=callback_data["injection_key"]).filter_by(owner_correlation_key=owner_user.owner_correlation_key).first() if correlated_request_entry != None: injection.correlated_request = correlated_request_entry.request else: injection.correlated_request = "Could not correlate XSS payload fire with request!" - session.add( injection ) + session.add(injection) session.commit() return injection -def email_sent_callback( response ): + +def email_sent_callback(response): print response.body -def send_email( to, subject, body, attachment_file, body_type="html" ): + +def send_email_old(to, subject, body, attachment_file, body_type="html"): if body_type == "html": - body += "
" # I'm so sorry. + # I'm so sorry. + body += "
" email_data = { - "from": urllib.quote_plus( settings["email_from"] ), - "to": urllib.quote_plus( to ), - "subject": urllib.quote_plus( subject ), - body_type: urllib.quote_plus( body ), + "from": urllib.quote_plus(settings["email_from"]), + "to": urllib.quote_plus(to), + "subject": urllib.quote_plus(subject), + body_type: urllib.quote_plus(body), } - thread = unirest.post( "https://api.mailgun.net/v3/" + settings["mailgun_sending_domain"] + "/messages", - headers={"Accept": "application/json"}, - params=email_data, - auth=("api", settings["mailgun_api_key"] ), - callback=email_sent_callback) + thread = unirest.post("https://api.mailgun.net/v3/" + settings["mailgun_sending_domain"] + "/messages", + headers={"Accept": "application/json"}, + params=email_data, + auth=("api", settings["mailgun_api_key"]), + callback=email_sent_callback) + + +def send_email(to, subject, body, attachment_file, body_type="html"): + if body_type == "html": + # I'm so sorry. + body += "
" + + # Replace sender@example.com with your "From" address. + # This address must be verified with Amazon SES. + SENDER = urllib.unquote(settings["email_from"]).decode('utf8') + + # Replace recipient@example.com with a "To" address. If your account + # is still in the sandbox, this address must be verified. + RECIPIENT = urllib.unquote(to).decode('utf8') + + # Specify a configuration set. If you do not want to use a configuration + # set, comment the following variable, and the + # ConfigurationSetName=CONFIGURATION_SET argument below. + # CONFIGURATION_SET = "ConfigSet" + + # If necessary, replace us-west-2 with the AWS Region you're using for Amazon SES. + AWS_REGION = "eu-west-1" + + # The subject line for the email. + SUBJECT = subject + + # The full path to the file that will be attached to the email. + BODY_TEXT = "xsshunter yeee" + BODY_HTML = body + # The character encoding for the email. + CHARSET = "utf-8" + + # Create a new SES resource and specify a region. + client = boto3.client('ses', region_name=AWS_REGION) + + # Create a multipart/mixed parent container. + msg = MIMEMultipart('mixed') + # Add subject, from and to lines. + msg['Subject'] = SUBJECT + msg['From'] = SENDER + msg['To'] = RECIPIENT + + # Create a multipart/alternative child container. + msg_body = MIMEMultipart('alternative') + + # Encode the text and HTML content and set the character encoding. This step is + # necessary if you're sending a message with characters outside the ASCII range. + htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET) + textpart = MIMEText(BODY_TEXT.encode(CHARSET), 'plain', CHARSET) + + # Add the text and HTML parts to the child container. + msg_body.attach(textpart) + msg_body.attach(htmlpart) + + # Define the attachment part and encode it using MIMEApplication. + # Attach the multipart/alternative child container to the multipart/mixed + # parent container. + msg.attach(msg_body) + + # Add the attachment to the parent container. + # print(msg) + try: + # Provide the contents of the email. + response = client.send_raw_email( + Source=SENDER, + Destinations=[ + RECIPIENT + ], + RawMessage={ + 'Data': msg.as_string(), + } + # ConfigurationSetName=CONFIGURATION_SET + ) + # Display an error if something goes wrong. + except ClientError as e: + print("[AWS SES] Error: ") + print(e.response['Error']['Message']) + else: + print("[AWS SES]: Email sent! Message ID:"), + print(response['MessageId']) + + +def send_javascript_pgp_encrypted_callback_message(email_data, email): + return send_email(email, "[XSS Hunter] XSS Payload Message (PGP Encrypted)", email_data, False, "text") -def send_javascript_pgp_encrypted_callback_message( email_data, email ): - return send_email( email, "[XSS Hunter] XSS Payload Message (PGP Encrypted)", email_data, False, "text" ) -def send_javascript_callback_message( email, injection_db_record ): - loader = tornado.template.Loader( "templates/" ) +def send_javascript_callback_message(email, injection_db_record): + loader = tornado.template.Loader("templates/") injection_data = injection_db_record.get_injection_blob() - email_html = loader.load( "xss_email_template.htm" ).generate( injection_data=injection_data, domain=settings["domain"] ) - return send_email( email, "[XSS Hunter] XSS Payload Fired On " + injection_data['vulnerable_page'], email_html, injection_db_record.screenshot ) + email_html = loader.load("xss_email_template.htm").generate( + injection_data=injection_data, domain=settings["domain"]) + return send_email(email, "[XSS Hunter] XSS Payload Fired On " + injection_data['vulnerable_page'], email_html, injection_db_record.screenshot) + class UserInformationHandler(BaseHandler): def get(self): user = self.get_authenticated_user() - self.logit( "User grabbed their profile information" ) + self.logit("User grabbed their profile information") if user == None: return - self.write( json.dumps( user.get_user_blob() ) ) + self.write(json.dumps(user.get_user_blob())) def put(self): user = self.get_authenticated_user() @@ -274,14 +390,15 @@ def put(self): user_data = json.loads(self.request.body) # Mass assignment is dangerous mmk - allowed_attributes = ["pgp_key", "full_name", "email", "password", "email_enabled", "chainload_uri", "page_collection_paths_list" ] + allowed_attributes = ["pgp_key", "full_name", "email", "password", + "email_enabled", "chainload_uri", "page_collection_paths_list"] invalid_attribute_list = [] tmp_domain = user.domain for key, value in user_data.iteritems(): if key in allowed_attributes: - return_data = user.set_attribute( key, user_data.get( key ) ) + return_data = user.set_attribute(key, user_data.get(key)) if return_data != True: - invalid_attribute_list.append( key ) + invalid_attribute_list.append(key) session.commit() @@ -291,57 +408,60 @@ def put(self): return_data["success"] = False return_data["invalid_fields"] = invalid_attribute_list else: - self.logit( "User just updated their profile information." ) + self.logit("User just updated their profile information.") return_data["success"] = True - self.write( json.dumps( return_data ) ) + self.write(json.dumps(return_data)) + -def authenticate_user( request_handler, in_username ): - user = session.query( User ).filter_by( username=in_username ).first() +def authenticate_user(request_handler, in_username): + user = session.query(User).filter_by(username=in_username).first() - csrf_token = binascii.hexlify( os.urandom( 50 ) ) - request_handler.set_secure_cookie( "user", user.id, httponly=True ) - request_handler.set_secure_cookie( "csrf", csrf_token, httponly=True ) + csrf_token = binascii.hexlify(os.urandom(50)) + request_handler.set_secure_cookie("user", user.id, httponly=True) + request_handler.set_secure_cookie("csrf", csrf_token, httponly=True) request_handler.write(json.dumps({ "success": True, "csrf_token": csrf_token, })) + class RegisterHandler(BaseHandler): @gen.coroutine def post(self): user_data = json.loads(self.request.body) user_data["email_enabled"] = True - if not self.validate_input( ["email","username","password", "domain"], user_data ): + if not self.validate_input(["email", "username", "password", "domain"], user_data): return - if session.query( User ).filter_by( username=user_data.get( "username" ) ).first(): + if session.query(User).filter_by(username=user_data.get("username")).first(): return_dict = { "success": False, "invalid_fields": ["username (already registered!)"], } - self.write( json.dumps( return_dict ) ) + self.write(json.dumps(return_dict)) return - domain = user_data.get( "domain" ) - if session.query( User ).filter_by( domain=domain ).first() or domain in FORBIDDEN_SUBDOMAINS: + domain = user_data.get("domain") + if session.query(User).filter_by(domain=domain).first() or domain in FORBIDDEN_SUBDOMAINS: return_dict = { "success": False, "invalid_fields": ["domain (already registered!)"], } - self.write( json.dumps( return_dict ) ) + self.write(json.dumps(return_dict)) return new_user = User() return_dict = {} - allowed_attributes = ["pgp_key", "full_name", "domain", "email", "password", "username", "email_enabled" ] + allowed_attributes = ["pgp_key", "full_name", "domain", + "email", "password", "username", "email_enabled"] invalid_attribute_list = [] for key, value in user_data.iteritems(): if key in allowed_attributes: - return_data = new_user.set_attribute( key, user_data.get( key ) ) + return_data = new_user.set_attribute(key, user_data.get(key)) if return_data != True: - invalid_attribute_list.append( key ) + invalid_attribute_list.append(key) new_user.generate_user_id() @@ -352,45 +472,51 @@ def post(self): "success": False, "invalid_fields": ["username (already registered!)"], } - self.write( json.dumps( return_dict ) ) + self.write(json.dumps(return_dict)) return - self.logit( "New user successfully registered with username of " + user_data["username"] ) - session.add( new_user ) + self.logit( + "New user successfully registered with username of " + user_data["username"]) + session.add(new_user) session.commit() - authenticate_user( self, user_data.get( "username" ) ) + authenticate_user(self, user_data.get("username")) return + class LoginHandler(BaseHandler): @gen.coroutine def post(self): user_data = json.loads(self.request.body) - if not self.validate_input( ["username","password"], user_data ): + if not self.validate_input(["username", "password"], user_data): return - user = session.query( User ).filter_by( username=user_data.get( "username" ) ).first() + user = session.query(User).filter_by( + username=user_data.get("username")).first() if user is None: - self.error( "Invalid username or password supplied" ) - self.logit( "Someone failed to log in as " + user_data["username"], "warn" ) + self.error("Invalid username or password supplied") + self.logit("Someone failed to log in as " + + user_data["username"], "warn") return - elif user.compare_password( user_data.get( "password" ) ): - authenticate_user( self, user_data.get( "username" ) ) - self.logit( "Someone logged in as " + user_data["username"] ) + elif user.compare_password(user_data.get("password")): + authenticate_user(self, user_data.get("username")) + self.logit("Someone logged in as " + user_data["username"]) return - self.error( "Invalid username or password supplied" ) + self.error("Invalid username or password supplied") return + class CallbackHandler(BaseHandler): """ This is the handler that receives the XSS payload data upon it firing in someone's browser, it contains things such as session cookies, the page DOM, a screenshot of the page, etc. """ - def post( self ): - self.set_header( 'Access-Control-Allow-Origin', '*' ) - self.set_header( 'Access-Control-Allow-Methods', 'POST, GET, HEAD, OPTIONS' ) - self.set_header( 'Access-Control-Allow-Headers', 'X-Requested-With' ) + def post(self): + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Methods', + 'POST, GET, HEAD, OPTIONS') + self.set_header('Access-Control-Allow-Headers', 'X-Requested-With') owner_user = self.get_user_from_subdomain() @@ -400,26 +526,34 @@ def post( self ): if "-----BEGIN PGP MESSAGE-----" in self.request.body: if owner_user.email_enabled: - self.logit( "User " + owner_user.username + " just got a PGP encrypted XSS callback, passing it along." ) - send_javascript_pgp_encrypted_callback_message( self.request.body, owner_user.email ) + self.logit("User " + owner_user.username + + " just got a PGP encrypted XSS callback, passing it along.") + send_javascript_pgp_encrypted_callback_message( + self.request.body, owner_user.email) else: - callback_data = json.loads( self.request.body ) + 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 ) + 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( '{}' ) + send_javascript_callback_message( + owner_user.email, injection_db_record) + self.write('{}') + class HomepageHandler(BaseHandler): def get(self, path): self.set_header("Access-Control-Allow-Origin", "*") - self.set_header("Access-Control-Allow-Methods", "OPTIONS, PUT, DELETE, POST, GET") - self.set_header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Origin, Authorization, Accept, Accept-Encoding") + self.set_header("Access-Control-Allow-Methods", + "OPTIONS, PUT, DELETE, POST, GET") + self.set_header("Access-Control-Allow-Headers", + "X-Requested-With, Content-Type, Origin, Authorization, Accept, Accept-Encoding") - domain = self.request.headers.get( 'Host' ) + domain = self.request.headers.get('Host') user = self.get_user_from_subdomain() @@ -428,84 +562,100 @@ def get(self, path): return new_probe = probejs - new_probe = new_probe.replace( '[HOST_URL]', "https://" + domain ) - new_probe = new_probe.replace( '[PGP_REPLACE_ME]', json.dumps( user.pgp_key ) ) - new_probe = new_probe.replace( '[CHAINLOAD_REPLACE_ME]', json.dumps( user.chainload_uri ) ) - new_probe = new_probe.replace( '[COLLECT_PAGE_LIST_REPLACE_ME]', json.dumps( user.get_page_collection_path_list() ) ) + new_probe = new_probe.replace('[HOST_URL]', "https://" + domain) + new_probe = new_probe.replace( + '[PGP_REPLACE_ME]', json.dumps(user.pgp_key)) + new_probe = new_probe.replace( + '[CHAINLOAD_REPLACE_ME]', json.dumps(user.chainload_uri)) + new_probe = new_probe.replace('[COLLECT_PAGE_LIST_REPLACE_ME]', json.dumps( + user.get_page_collection_path_list())) if user.pgp_key != "": - with open( "templates/pgp_encrypted_template.txt", "r" ) as template_handler: - new_probe = new_probe.replace( '[TEMPLATE_REPLACE_ME]', json.dumps( template_handler.read() )) + with open("templates/pgp_encrypted_template.txt", "r") as template_handler: + new_probe = new_probe.replace( + '[TEMPLATE_REPLACE_ME]', json.dumps(template_handler.read())) else: - new_probe = new_probe.replace( '[TEMPLATE_REPLACE_ME]', json.dumps( "" )) + new_probe = new_probe.replace( + '[TEMPLATE_REPLACE_ME]', json.dumps("")) if self.request.uri != "/": - probe_id = self.request.uri.split( "/" )[1] - self.write( new_probe.replace( "[PROBE_ID]", probe_id ) ) + probe_id = self.request.uri.split("/")[1] + self.write(new_probe.replace("[PROBE_ID]", probe_id)) else: - self.write( new_probe ) + self.write(new_probe) + class ContactUsHandler(BaseHandler): - def post( self ): + def post(self): contact_data = json.loads(self.request.body) - if not self.validate_input( ["name","email", "body"], contact_data ): + if not self.validate_input(["name", "email", "body"], contact_data): return - self.logit( "Someone just used the 'Contact Us' form." ) + self.logit("Someone just used the 'Contact Us' form.") email_body = "Name: " + contact_data["name"] + "\n" email_body += "Email: " + contact_data["email"] + "\n" email_body += "Message: " + contact_data["body"] + "\n" - send_email( settings["abuse_email"], "XSSHunter Contact Form Submission", email_body, "", "text" ) + send_email(settings["abuse_email"], + "XSSHunter Contact Form Submission", email_body, "", "text") self.write({ "success": True, }) + class ResendInjectionEmailHandler(BaseHandler): - def post( self ): + def post(self): post_data = json.loads(self.request.body) - if not self.validate_input( ["id"], post_data ): + if not self.validate_input(["id"], post_data): return - injection_db_record = session.query( Injection ).filter_by( id=str( post_data.get( "id" ) ) ).first() + injection_db_record = session.query(Injection).filter_by( + id=str(post_data.get("id"))).first() user = self.get_authenticated_user() if injection_db_record.owner_id != user.id: - self.logit( "Just tried to resend an injection email that wasn't theirs! (ID:" + post_data["id"] + ")", "warn") - self.error( "Fuck off <3" ) + self.logit( + "Just tried to resend an injection email that wasn't theirs! (ID:" + post_data["id"] + ")", "warn") + self.error("Fuck off <3") return - self.logit( "User just requested to resend the injection record email for URI " + injection_db_record.vulnerable_page ) + self.logit("User just requested to resend the injection record email for URI " + + injection_db_record.vulnerable_page) - send_javascript_callback_message( user.email, injection_db_record ) + send_javascript_callback_message(user.email, injection_db_record) self.write({ "success": True, "message": "Email sent!", }) + class DeleteInjectionHandler(BaseHandler): - def delete( self ): + def delete(self): delete_data = json.loads(self.request.body) - if not self.validate_input( ["id"], delete_data ): + if not self.validate_input(["id"], delete_data): return - injection_db_record = session.query( Injection ).filter_by( id=str( delete_data.get( "id" ) ) ).first() + injection_db_record = session.query(Injection).filter_by( + id=str(delete_data.get("id"))).first() user = self.get_authenticated_user() if injection_db_record.owner_id != user.id: - self.logit( "Just tried to delete an injection email that wasn't theirs! (ID:" + delete_data["id"] + ")", "warn") - self.error( "Fuck off <3" ) + self.logit("Just tried to delete an injection email that wasn't theirs! (ID:" + + delete_data["id"] + ")", "warn") + self.error("Fuck off <3") return - self.logit( "User delted injection record with an id of " + injection_db_record.id + "(" + injection_db_record.vulnerable_page + ")") + 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 ) + os.remove(injection_db_record.screenshot) - injection_db_record = session.query( Injection ).filter_by( id=str( delete_data.get( "id" ) ) ).delete() + injection_db_record = session.query(Injection).filter_by( + id=str(delete_data.get("id"))).delete() session.commit() self.write({ @@ -513,23 +663,27 @@ def delete( self ): "message": "Injection deleted!", }) + class HealthHandler(BaseHandler): - def get( self ): + def get(self): try: - injection_db_record = session.query( Injection ).filter_by( id="test" ).limit( 1 ) - self.write( "XSSHUNTER_OK" ) + injection_db_record = session.query( + Injection).filter_by(id="test").limit(1) + self.write("XSSHUNTER_OK") except: - self.write( "ERROR" ) + self.write("ERROR") self.set_status(500) -class LogoutHandler( BaseHandler ): - def get( self ): - self.logit( "User is logging out." ) + +class LogoutHandler(BaseHandler): + def get(self): + self.logit("User is logging out.") self.clear_cookie("user") self.clear_cookie("csrf") - self.write( "{}" ) + self.write("{}") -class InjectionRequestHandler( BaseHandler ): + +class InjectionRequestHandler(BaseHandler): """ This endpoint is for recording injection attempts. @@ -541,48 +695,54 @@ class InjectionRequestHandler( BaseHandler ): Sending two correlation requests means that the previous injection_key entry will be replaced. """ - def post( self ): + + def post(self): return_data = {} - request_dict = json.loads( self.request.body ) - if not self.validate_input( ["request", "owner_correlation_key", "injection_key"], request_dict ): + request_dict = json.loads(self.request.body) + if not self.validate_input(["request", "owner_correlation_key", "injection_key"], request_dict): return - injection_key = request_dict.get( "injection_key" ) + injection_key = request_dict.get("injection_key") injection_request = InjectionRequest() injection_request.injection_key = injection_key - injection_request.request = request_dict.get( "request" ) - owner_correlation_key = request_dict.get( "owner_correlation_key" ) + injection_request.request = request_dict.get("request") + owner_correlation_key = request_dict.get("owner_correlation_key") injection_request.owner_correlation_key = owner_correlation_key # Ensure that this is an existing correlation key - owner_user = session.query( User ).filter_by( owner_correlation_key=owner_correlation_key ).first() + owner_user = session.query(User).filter_by( + owner_correlation_key=owner_correlation_key).first() if owner_user is None: return_data["success"] = False return_data["message"] = "Invalid owner correlation key provided!" - self.write( json.dumps( return_data ) ) + self.write(json.dumps(return_data)) return - self.logit( "User " + owner_user.username + " just sent us an injection attempt with an ID of " + injection_request.injection_key ) + self.logit("User " + owner_user.username + + " just sent us an injection attempt with an ID of " + injection_request.injection_key) # Replace any previous injections with the same key and owner - session.query( InjectionRequest ).filter_by( injection_key=injection_key ).filter_by( owner_correlation_key=owner_correlation_key ).delete() + session.query(InjectionRequest).filter_by(injection_key=injection_key).filter_by( + owner_correlation_key=owner_correlation_key).delete() return_data["success"] = True return_data["message"] = "Injection request successfully recorded!" - session.add( injection_request ) + session.add(injection_request) session.commit() - self.write( json.dumps( return_data ) ) + self.write(json.dumps(return_data)) -class CollectPageHandler( BaseHandler ): - def post( self ): - self.set_header( 'Access-Control-Allow-Origin', '*' ) - self.set_header( 'Access-Control-Allow-Methods', 'POST, GET, HEAD, OPTIONS' ) - self.set_header( 'Access-Control-Allow-Headers', 'X-Requested-With' ) + +class CollectPageHandler(BaseHandler): + def post(self): + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Methods', + 'POST, GET, HEAD, OPTIONS') + self.set_header('Access-Control-Allow-Headers', 'X-Requested-With') user = self.get_user_from_subdomain() - request_dict = json.loads( self.request.body ) - if not self.validate_input( ["page_html", "uri"], request_dict ): + request_dict = json.loads(self.request.body) + if not self.validate_input(["page_html", "uri"], request_dict): return if user == None: @@ -590,17 +750,19 @@ def post( self ): return page = CollectedPage() - page.uri = request_dict.get( "uri" ) - page.page_html = request_dict.get( "page_html" ) + page.uri = request_dict.get("uri") + page.page_html = request_dict.get("page_html") page.owner_id = user.id page.timestamp = int(time.time()) - self.logit( "Received a collected page for user " + user.username + " with a URI of " + page.uri ) + self.logit("Received a collected page for user " + + user.username + " with a URI of " + page.uri) - session.add( page ) + session.add(page) session.commit() -class GetCollectedPagesHandler( BaseHandler ): + +class GetCollectedPagesHandler(BaseHandler): """ Endpoint for querying for collected pages. @@ -610,44 +772,52 @@ class GetCollectedPagesHandler( BaseHandler ): offset limit """ - def get( self ): + + def get(self): user = self.get_authenticated_user() - offset = abs( int( self.get_argument('offset', default=0 ) ) ) - limit = abs( int( self.get_argument('limit', default=25 ) ) ) - results = session.query( CollectedPage ).filter_by( owner_id = user.id ).order_by( CollectedPage.timestamp.desc() ).limit( limit ).offset( offset ) - total = session.query( CollectedPage ).filter_by( owner_id = user.id ).count() + offset = abs(int(self.get_argument('offset', default=0))) + limit = abs(int(self.get_argument('limit', default=25))) + results = session.query(CollectedPage).filter_by(owner_id=user.id).order_by( + CollectedPage.timestamp.desc()).limit(limit).offset(offset) + total = session.query(CollectedPage).filter_by( + owner_id=user.id).count() - self.logit( "User is retrieving collected pages.") + self.logit("User is retrieving collected pages.") return_list = [] for result in results: - return_list.append( result.to_dict() ) + return_list.append(result.to_dict()) return_dict = { "results": return_list, "total": total, "success": True } - self.write( json.dumps( return_dict ) ) + self.write(json.dumps(return_dict)) + class DeleteCollectedPageHandler(BaseHandler): - def delete( self ): + def delete(self): delete_data = json.loads(self.request.body) - if not self.validate_input( ["id"], delete_data ): + if not self.validate_input(["id"], delete_data): return - collected_page_db_record = session.query( CollectedPage ).filter_by( id=str( delete_data.get( "id" ) ) ).first() + collected_page_db_record = session.query(CollectedPage).filter_by( + id=str(delete_data.get("id"))).first() user = self.get_authenticated_user() if collected_page_db_record.owner_id != user.id: - self.logit( "Just tried to delete a collected page that wasn't theirs! (ID:" + delete_data["id"] + ")", "warn") - self.error( "Fuck off <3" ) + self.logit("Just tried to delete a collected page that wasn't theirs! (ID:" + + delete_data["id"] + ")", "warn") + self.error("Fuck off <3") return - self.logit( "User is deleting collected page with the URI of " + collected_page_db_record.uri ) - collected_page_db_record = session.query( CollectedPage ).filter_by( id=str( delete_data.get( "id" ) ) ).delete() + self.logit("User is deleting collected page with the URI of " + + collected_page_db_record.uri) + collected_page_db_record = session.query(CollectedPage).filter_by( + id=str(delete_data.get("id"))).delete() session.commit() self.write({ @@ -655,8 +825,9 @@ def delete( self ): "message": "Collected page deleted!", }) + def make_app(): - return tornado.web.Application([ + app_routes = [ (r"/api/register", RegisterHandler), (r"/api/login", LoginHandler), (r"/api/collected_pages", GetCollectedPagesHandler), @@ -670,10 +841,14 @@ def make_app(): (r"/js_callback", CallbackHandler), (r"/page_callback", CollectPageHandler), (r"/health", HealthHandler), - (r"/uploads/(.*)", tornado.web.StaticFileHandler, {"path": "uploads/"}), + (r"/uploads/(.*)", tornado.web.StaticFileHandler, + {"path": "uploads/"}), (r"/api/record_injection", InjectionRequestHandler), - (r"/(.*)", HomepageHandler), - ], cookie_secret=settings["cookie_secret"]) + (r"/(.*)", HomepageHandler)] + if not settings['self_registration']: + app_routes.remove((r"/api/register", RegisterHandler)) + return tornado.web.Application(app_routes, cookie_secret=settings["cookie_secret"]) + if __name__ == "__main__": args = sys.argv @@ -681,5 +856,5 @@ def make_app(): tornado.options.parse_command_line(args) Base.metadata.create_all(engine) app = make_app() - app.listen( 8888 ) + app.listen(8888, "localhost") tornado.ioloop.IOLoop.current().start() diff --git a/generate_config.py b/generate_config.py index 2af17d7..726779f 100755 --- a/generate_config.py +++ b/generate_config.py @@ -99,6 +99,7 @@ "domain": "", "abuse_email": "", "cookie_secret": "", + "self_registration": "yes", } print """ @@ -173,6 +174,9 @@ /etc/nginx/ssl/""" + hostname + """.crt; # Wildcard SSL certificate /etc/nginx/ssl/""" + hostname + """.key; # Wildcard SSL key +Note: by default self-registration is enabled. You can disable this by changing the 'self_registration' option in the config file to +"no". + Good luck hunting for XSS! -mandatory """ diff --git a/gui/guiserver.py b/gui/guiserver.py index 563a12a..0aef44c 100755 --- a/gui/guiserver.py +++ b/gui/guiserver.py @@ -54,18 +54,21 @@ def get(self): self.write( loader.load( "contact.htm" ).generate() ) def make_app(): - return tornado.web.Application([ - (r"/", HomepageHandler), - (r"/app", XSSHunterApplicationHandler), - (r"/features", FeaturesHandler), - (r"/signup", SignUpHandler), - (r"/contact", ContactHandler), - (r"/static/(.*)", tornado.web.StaticFileHandler, {"path": "static/"}), - ]) + app_routes = [ + (r"/", HomepageHandler), + (r"/app", XSSHunterApplicationHandler), + (r"/features", FeaturesHandler), + (r"/contact", ContactHandler), + (r"/signup", SignUpHandler), + (r"/static/(.*)", tornado.web.StaticFileHandler, {"path": "static/"}) + ] + if not settings['self_registration']: + app_routes.remove((r"/signup", SignUpHandler)) + return tornado.web.Application(app_routes) if __name__ == "__main__": DOMAIN = settings["domain"] API_SERVER = "https://api." + DOMAIN app = make_app() - app.listen( 1234 ) + app.listen( 1234, "localhost") tornado.ioloop.IOLoop.current().start()