From fac6712a3597f59b57fd24b688e971de1c08926a Mon Sep 17 00:00:00 2001 From: Miroslav Stampar Date: Fri, 4 Aug 2017 13:37:49 +0200 Subject: [PATCH] Implements #2647 (Basic authorization for sqlmapapi) --- lib/core/settings.py | 2 +- lib/utils/api.py | 93 +++++++++++++++++++++++++++++++------------- sqlmapapi.py | 6 ++- txt/checksum.md5 | 6 +-- 4 files changed, 73 insertions(+), 34 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 0211d8800..40cb8ea53 100755 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -19,7 +19,7 @@ from lib.core.enums import DBMS_DIRECTORY_NAME from lib.core.enums import OS # sqlmap version (...) -VERSION = "1.1.7.31" +VERSION = "1.1.8.0" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" 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) diff --git a/lib/utils/api.py b/lib/utils/api.py index 829c2fff3..d9ed6dbf7 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -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//delete") def task_delete(taskid): """ @@ -370,7 +396,6 @@ def task_delete(taskid): # Admin functions # ################### - @get("/admin//list") def task_list(taskid=None): """ @@ -403,7 +428,6 @@ def task_flush(taskid): # sqlmap core interact functions # ################################## - # Handle task's options @get("/option//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//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//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//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//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//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//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//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 diff --git a/sqlmapapi.py b/sqlmapapi.py index 09c1a9340..244296b7c 100755 --- a/sqlmapapi.py +++ b/sqlmapapi.py @@ -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("-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("--username", help="Basic authentication username (optional)", action="store") + apiparser.add_option("--password", help="Basic authentication password (optional)", action="store") (args, _) = apiparser.parse_args() # Start the client or the server 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: - client(args.host, args.port) + client(args.host, args.port, username=args.username, password=args.password) else: apiparser.print_help() diff --git a/txt/checksum.md5 b/txt/checksum.md5 index f1856a464..98572fe27 100644 --- a/txt/checksum.md5 +++ b/txt/checksum.md5 @@ -46,7 +46,7 @@ fbf750dc617c3549ee423d6c2334ba4d lib/core/option.py d8e9250f3775119df07e9070eddccd16 lib/core/replication.py 785f86e3f963fa3798f84286a4e83ff2 lib/core/revision.py 40c80b28b3a5819b737a5a17d4565ae9 lib/core/session.py -d6dc3f75b2f3aff43a7f3382059bea76 lib/core/settings.py +2a84a6aba8a3c553fdf6058a011a491f lib/core/settings.py d91291997d2bd2f6028aaf371bf1d3b6 lib/core/shell.py 2ad85c130cc5f2b3701ea85c2f6bbf20 lib/core/subprocessng.py 85e3a98bc9ba62125baa13e864f37a3f lib/core/target.py @@ -98,7 +98,7 @@ d3da4c7ceaf57c4687a052d58722f6bb lib/techniques/dns/use.py 310efc965c862cfbd7b0da5150a5ad36 lib/techniques/union/__init__.py d71e48e6fd08f75cc612bf8b260994ce lib/techniques/union/test.py db3090ff9a740ba096ba676fcf44ebfc lib/techniques/union/use.py -9e903297f6d6bb11660af5c7b109ccab lib/utils/api.py +720e899d5097d701d258bdc30eb8aa51 lib/utils/api.py 7d10ba0851da8ee9cd3c140dcd18798e lib/utils/brute.py c08d2487a53a1db8170178ebcf87c864 lib/utils/crawler.py ba12c69a90061aa14d848b8396e79191 lib/utils/deps.py @@ -223,7 +223,7 @@ b04db3e861edde1f9dd0a3850d5b96c8 shell/backdoor.asp_ c3cc8b7727161e64ab59f312c33b541a shell/stager.aspx_ 1f7f125f30e0e800beb21e2ebbab18e1 shell/stager.jsp_ 01e3505e796edf19aad6a996101c81c9 shell/stager.php_ -0751a45ac4c130131f2cdb74d866b664 sqlmapapi.py +8755985bcb91e3fea7aaaea3e98ec2dc sqlmapapi.py 41a637eda3e182d520fa4fb435edc1ec sqlmap.py 08c711a470d7e0bf705320ba3c48b886 tamper/apostrophemask.py e8509df10d3f1c28014d7825562d32dd tamper/apostrophenullencode.py