Implements #2647 (Basic authorization for sqlmapapi)

This commit is contained in:
Miroslav Stampar
2017-08-04 13:37:49 +02:00
parent 68ee1f361b
commit fac6712a35
4 changed files with 73 additions and 34 deletions

View File

@@ -7,6 +7,7 @@ See the file 'doc/COPYING' for copying permission
"""
import contextlib
import httplib
import logging
import os
import re
@@ -43,6 +44,7 @@ from lib.core.settings import RESTAPI_DEFAULT_ADDRESS
from lib.core.settings import RESTAPI_DEFAULT_PORT
from lib.core.subprocessng import Popen
from lib.parse.cmdline import cmdLineParser
from thirdparty.bottle.bottle import abort
from thirdparty.bottle.bottle import error as return_error
from thirdparty.bottle.bottle import get
from thirdparty.bottle.bottle import hook
@@ -52,13 +54,13 @@ from thirdparty.bottle.bottle import response
from thirdparty.bottle.bottle import run
from thirdparty.bottle.bottle import server_names
# global settings
# Global data storage
class DataStore(object):
admin_id = ""
current_db = None
tasks = dict()
username = None
password = None
# API objects
class Database(object):
@@ -118,7 +120,6 @@ class Database(object):
"taskid INTEGER, error TEXT"
")")
class Task(object):
def __init__(self, taskid, remote_addr):
self.remote_addr = remote_addr
@@ -283,11 +284,32 @@ def setRestAPILog():
LOGGER_RECORDER = LogRecorder()
logger.addHandler(LOGGER_RECORDER)
# Generic functions
def is_admin(taskid):
return DataStore.admin_id == taskid
@hook('before_request')
def check_authentication():
if not any((DataStore.username, DataStore.password)):
return
authorization = request.headers.get("Authorization", "")
match = re.search(r"(?i)\ABasic\s+([^\s]+)", authorization)
if not match:
request.environ["PATH_INFO"] = "/error/401"
try:
creds = match.group(1).decode("base64")
except:
request.environ["PATH_INFO"] = "/error/401"
else:
if creds.count(':') != 1:
request.environ["PATH_INFO"] = "/error/401"
else:
username, password = creds.split(':')
if username.strip() != (DataStore.username or "") or password.strip() != (DataStore.password or ""):
request.environ["PATH_INFO"] = "/error/401"
@hook("after_request")
def security_headers(json_header=True):
@@ -301,6 +323,7 @@ def security_headers(json_header=True):
response.headers["Pragma"] = "no-cache"
response.headers["Cache-Control"] = "no-cache"
response.headers["Expires"] = "0"
if json_header:
response.content_type = "application/json; charset=UTF-8"
@@ -308,35 +331,39 @@ def security_headers(json_header=True):
# HTTP Status Code functions #
##############################
@return_error(401) # Access Denied
def error401(error=None):
security_headers(False)
return "Access denied"
@return_error(404) # Not Found
def error404(error=None):
security_headers(False)
return "Nothing here"
@return_error(405) # Method Not Allowed (e.g. when requesting a POST method via GET)
def error405(error=None):
security_headers(False)
return "Method not allowed"
@return_error(500) # Internal Server Error
def error500(error=None):
security_headers(False)
return "Internal server error"
#############
# Auxiliary #
#############
@get('/error/401')
def path_401():
response.status = 401
return response
#############################
# Task management functions #
#############################
# Users' methods
@get("/task/new")
def task_new():
@@ -351,7 +378,6 @@ def task_new():
logger.debug("Created new task: '%s'" % taskid)
return jsonize({"success": True, "taskid": taskid})
@get("/task/<taskid>/delete")
def task_delete(taskid):
"""
@@ -370,7 +396,6 @@ def task_delete(taskid):
# Admin functions #
###################
@get("/admin/<taskid>/list")
def task_list(taskid=None):
"""
@@ -403,7 +428,6 @@ def task_flush(taskid):
# sqlmap core interact functions #
##################################
# Handle task's options
@get("/option/<taskid>/list")
def option_list(taskid):
@@ -417,7 +441,6 @@ def option_list(taskid):
logger.debug("[%s] Listed task options" % taskid)
return jsonize({"success": True, "options": DataStore.tasks[taskid].get_options()})
@post("/option/<taskid>/get")
def option_get(taskid):
"""
@@ -436,12 +459,12 @@ def option_get(taskid):
logger.debug("[%s] Requested value for unknown option %s" % (taskid, option))
return jsonize({"success": False, "message": "Unknown option", option: "not set"})
@post("/option/<taskid>/set")
def option_set(taskid):
"""
Set an option (command line switch) for a certain task ID
"""
if taskid not in DataStore.tasks:
logger.warning("[%s] Invalid task ID provided to option_set()" % taskid)
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -452,13 +475,13 @@ def option_set(taskid):
logger.debug("[%s] Requested to set options" % taskid)
return jsonize({"success": True})
# Handle scans
@post("/scan/<taskid>/start")
def scan_start(taskid):
"""
Launch a scan
"""
if taskid not in DataStore.tasks:
logger.warning("[%s] Invalid task ID provided to scan_start()" % taskid)
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -473,12 +496,12 @@ def scan_start(taskid):
logger.debug("[%s] Started scan" % taskid)
return jsonize({"success": True, "engineid": DataStore.tasks[taskid].engine_get_id()})
@get("/scan/<taskid>/stop")
def scan_stop(taskid):
"""
Stop a scan
"""
if (taskid not in DataStore.tasks or
DataStore.tasks[taskid].engine_process() is None or
DataStore.tasks[taskid].engine_has_terminated()):
@@ -490,12 +513,12 @@ def scan_stop(taskid):
logger.debug("[%s] Stopped scan" % taskid)
return jsonize({"success": True})
@get("/scan/<taskid>/kill")
def scan_kill(taskid):
"""
Kill a scan
"""
if (taskid not in DataStore.tasks or
DataStore.tasks[taskid].engine_process() is None or
DataStore.tasks[taskid].engine_has_terminated()):
@@ -507,12 +530,12 @@ def scan_kill(taskid):
logger.debug("[%s] Killed scan" % taskid)
return jsonize({"success": True})
@get("/scan/<taskid>/status")
def scan_status(taskid):
"""
Returns status of a scan
"""
if taskid not in DataStore.tasks:
logger.warning("[%s] Invalid task ID provided to scan_status()" % taskid)
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -529,12 +552,12 @@ def scan_status(taskid):
"returncode": DataStore.tasks[taskid].engine_get_returncode()
})
@get("/scan/<taskid>/data")
def scan_data(taskid):
"""
Retrieve the data of a scan
"""
json_data_message = list()
json_errors_message = list()
@@ -560,6 +583,7 @@ def scan_log_limited(taskid, start, end):
"""
Retrieve a subset of log messages
"""
json_log_messages = list()
if taskid not in DataStore.tasks:
@@ -586,6 +610,7 @@ def scan_log(taskid):
"""
Retrieve the log messages
"""
json_log_messages = list()
if taskid not in DataStore.tasks:
@@ -606,6 +631,7 @@ def download(taskid, target, filename):
"""
Download a certain file from the file system
"""
if taskid not in DataStore.tasks:
logger.warning("[%s] Invalid task ID provided to download()" % taskid)
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -626,13 +652,17 @@ def download(taskid, target, filename):
return jsonize({"success": False, "message": "File does not exist"})
def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER):
def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None):
"""
REST-JSON API server
"""
DataStore.admin_id = hexencode(os.urandom(16))
handle, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False)
os.close(handle)
DataStore.username = username
DataStore.password = password
_, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False)
os.close(_)
if port == 0: # random
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
@@ -660,7 +690,7 @@ def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=REST
import eventlet
eventlet.monkey_patch()
logger.debug("Using adapter '%s' to run bottle" % adapter)
run(host=host, port=port, quiet=True, debug=False, server=adapter)
run(host=host, port=port, quiet=True, debug=True, server=adapter)
except socket.error, ex:
if "already in use" in getSafeExString(ex):
logger.error("Address already in use ('%s:%s')" % (host, port))
@@ -681,7 +711,12 @@ def _client(url, options=None):
data = None
if options is not None:
data = jsonize(options)
req = urllib2.Request(url, data, {"Content-Type": "application/json"})
headers = {"Content-Type": "application/json"}
if DataStore.username or DataStore.password:
headers["Authorization"] = "Basic %s" % ("%s:%s" % (DataStore.username or "", DataStore.password or "")).encode("base64").strip()
req = urllib2.Request(url, data, headers)
response = urllib2.urlopen(req)
text = response.read()
except:
@@ -690,12 +725,14 @@ def _client(url, options=None):
raise
return text
def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT):
def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=None, password=None):
"""
REST-JSON API client
"""
DataStore.username = username
DataStore.password = password
dbgMsg = "Example client access from command line:"
dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\{16\}') && echo $taskid" % (host, port)
dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"http://testphp.vulnweb.com/artists.php?artist=1\"}' http://%s:%d/scan/$taskid/start" % (host, port)
@@ -709,7 +746,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT):
try:
_client(addr)
except Exception, ex:
if not isinstance(ex, urllib2.HTTPError):
if not isinstance(ex, urllib2.HTTPError) or ex.code == httplib.UNAUTHORIZED:
errMsg = "There has been a problem while connecting to the "
errMsg += "REST-JSON API server at '%s' " % addr
errMsg += "(%s)" % ex