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

@ -19,7 +19,7 @@ from lib.core.enums import DBMS_DIRECTORY_NAME
from lib.core.enums import OS from lib.core.enums import OS
# sqlmap version (<major>.<minor>.<month>.<monthly commit>) # sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.1.7.31" VERSION = "1.1.8.0"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

View File

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

View File

@ -40,13 +40,15 @@ def main():
apiparser.add_option("-H", "--host", help="Host of the REST-JSON API server (default \"%s\")" % RESTAPI_DEFAULT_ADDRESS, default=RESTAPI_DEFAULT_ADDRESS, action="store") apiparser.add_option("-H", "--host", help="Host of the REST-JSON API server (default \"%s\")" % RESTAPI_DEFAULT_ADDRESS, default=RESTAPI_DEFAULT_ADDRESS, action="store")
apiparser.add_option("-p", "--port", help="Port of the the REST-JSON API server (default %d)" % RESTAPI_DEFAULT_PORT, default=RESTAPI_DEFAULT_PORT, type="int", action="store") apiparser.add_option("-p", "--port", help="Port of the the REST-JSON API server (default %d)" % RESTAPI_DEFAULT_PORT, default=RESTAPI_DEFAULT_PORT, type="int", action="store")
apiparser.add_option("--adapter", help="Server (bottle) adapter to use (default \"%s\")" % RESTAPI_DEFAULT_ADAPTER, default=RESTAPI_DEFAULT_ADAPTER, action="store") apiparser.add_option("--adapter", help="Server (bottle) adapter to use (default \"%s\")" % RESTAPI_DEFAULT_ADAPTER, default=RESTAPI_DEFAULT_ADAPTER, action="store")
apiparser.add_option("--username", help="Basic authentication username (optional)", action="store")
apiparser.add_option("--password", help="Basic authentication password (optional)", action="store")
(args, _) = apiparser.parse_args() (args, _) = apiparser.parse_args()
# Start the client or the server # Start the client or the server
if args.server is True: if args.server is True:
server(args.host, args.port, adapter=args.adapter) server(args.host, args.port, adapter=args.adapter, username=args.username, password=args.password)
elif args.client is True: elif args.client is True:
client(args.host, args.port) client(args.host, args.port, username=args.username, password=args.password)
else: else:
apiparser.print_help() apiparser.print_help()

View File

@ -46,7 +46,7 @@ fbf750dc617c3549ee423d6c2334ba4d lib/core/option.py
d8e9250f3775119df07e9070eddccd16 lib/core/replication.py d8e9250f3775119df07e9070eddccd16 lib/core/replication.py
785f86e3f963fa3798f84286a4e83ff2 lib/core/revision.py 785f86e3f963fa3798f84286a4e83ff2 lib/core/revision.py
40c80b28b3a5819b737a5a17d4565ae9 lib/core/session.py 40c80b28b3a5819b737a5a17d4565ae9 lib/core/session.py
d6dc3f75b2f3aff43a7f3382059bea76 lib/core/settings.py 2a84a6aba8a3c553fdf6058a011a491f lib/core/settings.py
d91291997d2bd2f6028aaf371bf1d3b6 lib/core/shell.py d91291997d2bd2f6028aaf371bf1d3b6 lib/core/shell.py
2ad85c130cc5f2b3701ea85c2f6bbf20 lib/core/subprocessng.py 2ad85c130cc5f2b3701ea85c2f6bbf20 lib/core/subprocessng.py
85e3a98bc9ba62125baa13e864f37a3f lib/core/target.py 85e3a98bc9ba62125baa13e864f37a3f lib/core/target.py
@ -98,7 +98,7 @@ d3da4c7ceaf57c4687a052d58722f6bb lib/techniques/dns/use.py
310efc965c862cfbd7b0da5150a5ad36 lib/techniques/union/__init__.py 310efc965c862cfbd7b0da5150a5ad36 lib/techniques/union/__init__.py
d71e48e6fd08f75cc612bf8b260994ce lib/techniques/union/test.py d71e48e6fd08f75cc612bf8b260994ce lib/techniques/union/test.py
db3090ff9a740ba096ba676fcf44ebfc lib/techniques/union/use.py db3090ff9a740ba096ba676fcf44ebfc lib/techniques/union/use.py
9e903297f6d6bb11660af5c7b109ccab lib/utils/api.py 720e899d5097d701d258bdc30eb8aa51 lib/utils/api.py
7d10ba0851da8ee9cd3c140dcd18798e lib/utils/brute.py 7d10ba0851da8ee9cd3c140dcd18798e lib/utils/brute.py
c08d2487a53a1db8170178ebcf87c864 lib/utils/crawler.py c08d2487a53a1db8170178ebcf87c864 lib/utils/crawler.py
ba12c69a90061aa14d848b8396e79191 lib/utils/deps.py ba12c69a90061aa14d848b8396e79191 lib/utils/deps.py
@ -223,7 +223,7 @@ b04db3e861edde1f9dd0a3850d5b96c8 shell/backdoor.asp_
c3cc8b7727161e64ab59f312c33b541a shell/stager.aspx_ c3cc8b7727161e64ab59f312c33b541a shell/stager.aspx_
1f7f125f30e0e800beb21e2ebbab18e1 shell/stager.jsp_ 1f7f125f30e0e800beb21e2ebbab18e1 shell/stager.jsp_
01e3505e796edf19aad6a996101c81c9 shell/stager.php_ 01e3505e796edf19aad6a996101c81c9 shell/stager.php_
0751a45ac4c130131f2cdb74d866b664 sqlmapapi.py 8755985bcb91e3fea7aaaea3e98ec2dc sqlmapapi.py
41a637eda3e182d520fa4fb435edc1ec sqlmap.py 41a637eda3e182d520fa4fb435edc1ec sqlmap.py
08c711a470d7e0bf705320ba3c48b886 tamper/apostrophemask.py 08c711a470d7e0bf705320ba3c48b886 tamper/apostrophemask.py
e8509df10d3f1c28014d7825562d32dd tamper/apostrophenullencode.py e8509df10d3f1c28014d7825562d32dd tamper/apostrophenullencode.py