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()