From 8a946509b9fb77d19519674dc8630c57262deea3 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sat, 14 Dec 2013 15:44:10 +0100 Subject: [PATCH 01/11] PEP8 --- lib/utils/api.py | 84 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index c5f43fc0a..cb52b2d7c 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- """ Copyright (c) 2006-2013 sqlmap developers (http://sqlmap.org/) @@ -52,13 +53,25 @@ db = None db_filepath = None tasks = dict() + # API objects class Database(object): global db_filepath - LOGS_TABLE = "CREATE TABLE logs(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, time TEXT, level TEXT, message TEXT)" - DATA_TABLE = "CREATE TABLE data(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, status INTEGER, content_type INTEGER, value TEXT)" - ERRORS_TABLE = "CREATE TABLE errors(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, error TEXT)" + LOGS_TABLE = ("CREATE TABLE logs(" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "taskid INTEGER, time TEXT, " + "level TEXT, message TEXT" + ")") + DATA_TABLE = ("CREATE TABLE data(" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "taskid INTEGER, status INTEGER, " + "content_type INTEGER, value TEXT" + ")") + ERRORS_TABLE = ("CREATE TABLE errors(" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "taskid INTEGER, error TEXT" + ")") def __init__(self, database=None): if database: @@ -92,6 +105,7 @@ class Database(object): self.execute(self.DATA_TABLE) self.execute(self.ERRORS_TABLE) + class Task(object): global db_filepath @@ -111,7 +125,8 @@ class Task(object): type_ = unArrayizeValue(type_) self.options[name] = _defaults.get(name, datatype[type_]) - # Let sqlmap engine knows it is getting called by the API, the task ID and the file path of the IPC database + # Let sqlmap engine knows it is getting called by the API, + # the task ID and the file path of the IPC database self.options.api = True self.options.taskid = taskid self.options.database = db_filepath @@ -145,7 +160,8 @@ class Task(object): shutil.rmtree(self.output_directory) def engine_start(self): - self.process = Popen("python sqlmap.py --pickled-options %s" % base64pickle(self.options), shell=True, stdin=PIPE, close_fds=False) + self.process = Popen("python sqlmap.py --pickled-options %s" % base64pickle(self.options), + shell=True, stdin=PIPE, close_fds=False) def engine_stop(self): if self.process: @@ -194,25 +210,27 @@ class StdDbOut(object): # Ignore all non-relevant messages return - output = conf.database_cursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", - (self.taskid, content_type)) - - #print >>sys.__stdout__, "output: %s\nvalue: %s\nstatus: %d\ncontent_type: %d\nkb.partRun: %s\n--------------" % (output, value, status, content_type, kb.partRun) + output = conf.database_cursor.execute( + "SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", + (self.taskid, content_type)) # Delete partial output from IPC database if we have got a complete output if status == CONTENT_STATUS.COMPLETE: if len(output) > 0: for index in xrange(0, len(output)): - conf.database_cursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],)) + conf.database_cursor.execute("DELETE FROM data WHERE id = ?", + (output[index][0],)) - conf.database_cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value))) + conf.database_cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", + (self.taskid, status, content_type, jsonize(value))) if kb.partRun: kb.partRun = None elif status == CONTENT_STATUS.IN_PROGRESS: if len(output) == 0: conf.database_cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", - (self.taskid, status, content_type, jsonize(value))) + (self.taskid, status, content_type, + jsonize(value))) else: new_value = "%s%s" % (dejsonize(output[0][2]), value) conf.database_cursor.execute("UPDATE data SET value = ? WHERE id = ?", @@ -230,6 +248,7 @@ class StdDbOut(object): def seek(self): pass + class LogRecorder(logging.StreamHandler): def emit(self, record): """ @@ -238,7 +257,8 @@ class LogRecorder(logging.StreamHandler): """ conf.database_cursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, - record.msg % record.args if record.args else record.msg)) + record.msg % record.args if record.args else record.msg)) + def setRestAPILog(): if hasattr(conf, "api"): @@ -250,6 +270,7 @@ def setRestAPILog(): LOGGER_RECORDER = LogRecorder() logger.addHandler(LOGGER_RECORDER) + # Generic functions def is_admin(taskid): global adminid @@ -258,6 +279,7 @@ def is_admin(taskid): else: return True + @hook("after_request") def security_headers(json_header=True): """ @@ -282,16 +304,19 @@ def error401(error=None): security_headers(False) return "Access denied" + @error(404) # Not Found def error404(error=None): security_headers(False) return "Nothing here" + @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" + @error(500) # Internal Server Error def error500(error=None): security_headers(False) @@ -315,6 +340,7 @@ def task_new(): logger.debug("Created new task ID: %s" % taskid) return jsonize({"taskid": taskid}) + @get("/task//delete") def task_delete(taskid): """ @@ -345,6 +371,7 @@ def task_list(taskid): else: abort(401) + @get("/admin//flush") def task_flush(taskid): """ @@ -377,6 +404,7 @@ def option_list(taskid): return jsonize({"options": tasks[taskid].get_options()}) + @post("/option//get") def option_get(taskid): """ @@ -394,6 +422,7 @@ def option_get(taskid): else: return jsonize({option: "not set"}) + @post("/option//set") def option_set(taskid): """ @@ -435,6 +464,7 @@ def scan_start(taskid): logger.debug("Started scan for task ID %s" % taskid) return jsonize({"success": True, "engineid": tasks[taskid].engine_get_id()}) + @get("/scan//stop") def scan_stop(taskid): """ @@ -450,6 +480,7 @@ def scan_stop(taskid): logger.debug("Stopped scan for task ID %s" % taskid) return jsonize({"success": True}) + @get("/scan//kill") def scan_kill(taskid): """ @@ -465,6 +496,7 @@ def scan_kill(taskid): logger.debug("Killed scan for task ID %s" % taskid) return jsonize({"success": True}) + @get("/scan//status") def scan_status(taskid): """ @@ -480,6 +512,7 @@ def scan_status(taskid): logger.debug("Requested status of scan for task ID %s" % taskid) return jsonize({"status": status, "returncode": tasks[taskid].engine_get_returncode()}) + @get("/scan//data") def scan_data(taskid): """ @@ -494,11 +527,15 @@ def scan_data(taskid): abort(500, "Invalid task ID") # Read all data from the IPC database for the taskid - for status, content_type, value in db.execute("SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)): - json_data_message.append({"status": status, "type": content_type, "value": dejsonize(value)}) + for status, content_type, value in db.execute( + "SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", + (taskid,)): + json_data_message.append( + {"status": status, "type": content_type, "value": dejsonize(value)}) # Read all error messages from the IPC database - for error in db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)): + for error in db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", + (taskid,)): json_errors_message.append(error) logger.debug("Retrieved data and error messages for scan for task ID %s" % taskid) @@ -524,12 +561,16 @@ def scan_log_limited(taskid, start, end): end = max(1, int(end)) # Read a subset of log messages from the IPC database - for time_, level, message in db.execute("SELECT time, level, message FROM logs WHERE taskid = ? AND id >= ? AND id <= ? ORDER BY id ASC", (taskid, start, end)): + for time_, level, message in db.execute( + ("SELECT time, level, message FROM logs WHERE " + "taskid = ? AND id >= ? AND id <= ? ORDER BY id ASC"), + (taskid, start, end)): json_log_messages.append({"time": time_, "level": level, "message": message}) logger.debug("Retrieved subset of log messages for scan for task ID %s" % taskid) return jsonize({"log": json_log_messages}) + @get("/scan//log") def scan_log(taskid): """ @@ -543,7 +584,8 @@ def scan_log(taskid): abort(500, "Invalid task ID") # Read all log messages from the IPC database - for time_, level, message in db.execute("SELECT time, level, message FROM logs WHERE taskid = ? ORDER BY id ASC", (taskid,)): + for time_, level, message in db.execute( + "SELECT time, level, message FROM logs WHERE taskid = ? ORDER BY id ASC", (taskid,)): json_log_messages.append({"time": time_, "level": level, "message": message}) logger.debug("Retrieved log messages for scan for task ID %s" % taskid) @@ -569,6 +611,7 @@ def download(taskid, target, filename): else: abort(500, "File does not exist") + def server(host="0.0.0.0", port=RESTAPI_SERVER_PORT): """ REST-JSON API server @@ -592,6 +635,7 @@ def server(host="0.0.0.0", port=RESTAPI_SERVER_PORT): # Run RESTful API run(host=host, port=port, quiet=True, debug=False) + def client(host=RESTAPI_SERVER_HOST, port=RESTAPI_SERVER_PORT): """ REST-JSON API client @@ -602,6 +646,8 @@ def client(host=RESTAPI_SERVER_HOST, port=RESTAPI_SERVER_PORT): # TODO: write a simple client with requests, for now use curl from command line logger.error("Not yet implemented, use curl from command line instead for now, for example:") print "\n\t$ curl http://%s:%d/task/new" % (host, port) - print "\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) + print ("\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) print "\t$ curl http://%s:%d/scan/:taskid/data" % (host, port) print "\t$ curl http://%s:%d/scan/:taskid/log\n" % (host, port) From c5a3f54b898979a3533898a33ff752e9fc85cd35 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sat, 14 Dec 2013 15:47:26 +0100 Subject: [PATCH 02/11] remove unused imports --- lib/utils/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index cb52b2d7c..61a15b5ba 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -32,8 +32,6 @@ from lib.core.enums import PART_RUN_CONTENT_TYPES from lib.core.log import LOGGER_HANDLER from lib.core.optiondict import optDict from lib.core.subprocessng import Popen -from lib.core.subprocessng import send_all -from lib.core.subprocessng import recv_some from thirdparty.bottle.bottle import abort from thirdparty.bottle.bottle import error from thirdparty.bottle.bottle import get From af7ad31182a471f40681b6e6cfdb43f4e64db767 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sat, 14 Dec 2013 15:58:09 +0100 Subject: [PATCH 03/11] fix commit method usage (belongs to connection, not cursor) --- lib/utils/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index 61a15b5ba..c8171d482 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -87,7 +87,7 @@ class Database(object): self.connection.close() def commit(self): - self.cursor.commit() + self.connection.commit() def execute(self, statement, arguments=None): if arguments: From 72137e85f981aeee6d7a1252c34b19eb9b31874d Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sat, 14 Dec 2013 15:59:47 +0100 Subject: [PATCH 04/11] do not reset options when firing a scan --- lib/utils/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index c8171d482..3db8ce3c0 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -447,8 +447,6 @@ def scan_start(taskid): if taskid not in tasks: abort(500, "Invalid task ID") - tasks[taskid].reset_options() - # Initialize sqlmap engine's options with user's provided options, if any for option, value in request.json.items(): tasks[taskid].set_option(option, value) From c87ad1bab5aa9aa78895aaafde59cf717bd0a108 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sat, 14 Dec 2013 16:22:30 +0100 Subject: [PATCH 05/11] make returned values more coherent --- lib/utils/api.py | 50 ++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index 3db8ce3c0..453181b6a 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -336,7 +336,7 @@ def task_new(): tasks[taskid] = Task(taskid) logger.debug("Created new task ID: %s" % taskid) - return jsonize({"taskid": taskid}) + return jsonize({"success": True, "taskid": taskid}) @get("/task//delete") @@ -351,7 +351,7 @@ def task_delete(taskid): logger.debug("Deleted task ID: %s" % taskid) return jsonize({"success": True}) else: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) ################### # Admin functions # @@ -365,9 +365,9 @@ def task_list(taskid): if is_admin(taskid): logger.debug("Listed task pull") task_list = list(tasks) - return jsonize({"tasks": task_list, "tasks_num": len(tasks)}) + return jsonize({"success": True, "tasks": task_list, "tasks_num": len(tasks)}) else: - abort(401) + return jsonize({"success": False, "message": "Unauthorized"}) @get("/admin//flush") @@ -385,7 +385,7 @@ def task_flush(taskid): logger.debug("Flushed task pull") return jsonize({"success": True}) else: - abort(401) + return jsonize({"success": False, "message": "Unauthorized"}) ################################## # sqlmap core interact functions # @@ -398,9 +398,9 @@ def option_list(taskid): List options for a certain task ID """ if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) - return jsonize({"options": tasks[taskid].get_options()}) + return jsonize({"success": True, "options": tasks[taskid].get_options()}) @post("/option//get") @@ -411,14 +411,14 @@ def option_get(taskid): global tasks if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) option = request.json.get("option", "") if option in tasks[taskid].options: - return jsonize({option: tasks[taskid].get_option(option)}) + return jsonize({"success": True, option: tasks[taskid].get_option(option)}) else: - return jsonize({option: "not set"}) + return jsonize({"success": False, "message": "Unknown option", option: "not set"}) @post("/option//set") @@ -429,7 +429,7 @@ def option_set(taskid): global tasks if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) for option, value in request.json.items(): tasks[taskid].set_option(option, value) @@ -445,7 +445,7 @@ def scan_start(taskid): global tasks if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) # Initialize sqlmap engine's options with user's provided options, if any for option, value in request.json.items(): @@ -469,7 +469,7 @@ def scan_stop(taskid): global tasks if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) tasks[taskid].engine_stop() @@ -485,7 +485,7 @@ def scan_kill(taskid): global tasks if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) tasks[taskid].engine_kill() @@ -501,12 +501,16 @@ def scan_status(taskid): global tasks if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) status = "terminated" if tasks[taskid].engine_has_terminated() is True else "running" logger.debug("Requested status of scan for task ID %s" % taskid) - return jsonize({"status": status, "returncode": tasks[taskid].engine_get_returncode()}) + return jsonize({ + "success": True, + "status": status, + "returncode": tasks[taskid].engine_get_returncode() + }) @get("/scan//data") @@ -520,7 +524,7 @@ def scan_data(taskid): json_errors_message = list() if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) # Read all data from the IPC database for the taskid for status, content_type, value in db.execute( @@ -535,7 +539,7 @@ def scan_data(taskid): json_errors_message.append(error) logger.debug("Retrieved data and error messages for scan for task ID %s" % taskid) - return jsonize({"data": json_data_message, "error": json_errors_message}) + return jsonize({"success": True, "data": json_data_message, "error": json_errors_message}) # Functions to handle scans' logs @get("/scan//log//") @@ -548,10 +552,10 @@ def scan_log_limited(taskid, start, end): json_log_messages = list() if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) if not start.isdigit() or not end.isdigit() or end < start: - abort(500, "Invalid start or end value, must be digits") + return jsonize({"success": False, "message": "Invalid start or end value, must be digits"}) start = max(1, int(start)) end = max(1, int(end)) @@ -564,7 +568,7 @@ def scan_log_limited(taskid, start, end): json_log_messages.append({"time": time_, "level": level, "message": message}) logger.debug("Retrieved subset of log messages for scan for task ID %s" % taskid) - return jsonize({"log": json_log_messages}) + return jsonize({"success": True, "log": json_log_messages}) @get("/scan//log") @@ -577,7 +581,7 @@ def scan_log(taskid): json_log_messages = list() if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) # Read all log messages from the IPC database for time_, level, message in db.execute( @@ -585,7 +589,7 @@ def scan_log(taskid): json_log_messages.append({"time": time_, "level": level, "message": message}) logger.debug("Retrieved log messages for scan for task ID %s" % taskid) - return jsonize({"log": json_log_messages}) + return jsonize({"success": True, "log": json_log_messages}) # Function to handle files inside the output directory @get("/download///") From aa02019638ea957d4e38969fef7e14c2c36e483c Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sat, 14 Dec 2013 16:33:17 +0100 Subject: [PATCH 06/11] return file content in a json message when calling download endpoint --- lib/utils/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index 453181b6a..472b026ec 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -598,18 +598,20 @@ def download(taskid, target, filename): Download a certain file from the file system """ if taskid not in tasks: - abort(500, "Invalid task ID") + return jsonize({"success": False, "message": "Invalid task ID"}) # Prevent file path traversal - the lame way - if target.startswith("."): - abort(500) + if ".." in target: + return jsonize({"success": False, "message": "Forbidden path"}) path = os.path.join(paths.SQLMAP_OUTPUT_PATH, target) if os.path.exists(path): - return static_file(filename, root=path) + with open(path, 'rb') as inf: + file_content = inf.read() + return jsonize({"success": True, "file": file_content.encode("base64")}) else: - abort(500, "File does not exist") + return jsonize({"success": False, "message": "File does not exist"}) def server(host="0.0.0.0", port=RESTAPI_SERVER_PORT): From c70f2a4e6d28a27a923895a003a0379e9be3eeb6 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sun, 15 Dec 2013 00:00:08 +0100 Subject: [PATCH 07/11] unused imports --- lib/utils/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index 472b026ec..5943b33a7 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -32,7 +32,6 @@ from lib.core.enums import PART_RUN_CONTENT_TYPES from lib.core.log import LOGGER_HANDLER from lib.core.optiondict import optDict from lib.core.subprocessng import Popen -from thirdparty.bottle.bottle import abort from thirdparty.bottle.bottle import error from thirdparty.bottle.bottle import get from thirdparty.bottle.bottle import hook @@ -40,7 +39,6 @@ from thirdparty.bottle.bottle import post from thirdparty.bottle.bottle import request from thirdparty.bottle.bottle import response from thirdparty.bottle.bottle import run -from thirdparty.bottle.bottle import static_file RESTAPI_SERVER_HOST = "127.0.0.1" RESTAPI_SERVER_PORT = 8775 From 3effaee2a1f234d95318cb75364539210a5dcebe Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sun, 15 Dec 2013 00:19:58 +0100 Subject: [PATCH 08/11] avoid using global variables, use a "store" class --- lib/utils/api.py | 143 ++++++++++++++++++----------------------------- 1 file changed, 55 insertions(+), 88 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index 5943b33a7..30427ebdc 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -43,17 +43,17 @@ from thirdparty.bottle.bottle import run RESTAPI_SERVER_HOST = "127.0.0.1" RESTAPI_SERVER_PORT = 8775 -# Local global variables -adminid = "" -db = None -db_filepath = None -tasks = dict() + +# global settings +class DataStore(object): + admin_id = "" + current_db = None + tasks = dict() # API objects class Database(object): - global db_filepath - + filepath = None LOGS_TABLE = ("CREATE TABLE logs(" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "taskid INTEGER, time TEXT, " @@ -70,10 +70,7 @@ class Database(object): ")") def __init__(self, database=None): - if database: - self.database = database - else: - self.database = db_filepath + self.database = self.filepath if database is None else database def connect(self, who="server"): self.connection = sqlite3.connect(self.database, timeout=3, isolation_level=None) @@ -103,7 +100,6 @@ class Database(object): class Task(object): - global db_filepath def __init__(self, taskid): self.process = None @@ -125,7 +121,7 @@ class Task(object): # the task ID and the file path of the IPC database self.options.api = True self.options.taskid = taskid - self.options.database = db_filepath + self.options.database = Database.filepath # Enforce batch mode and disable coloring and ETA self.options.batch = True @@ -269,11 +265,7 @@ def setRestAPILog(): # Generic functions def is_admin(taskid): - global adminid - if adminid != taskid: - return False - else: - return True + return DataStore.admin_id == taskid @hook("after_request") @@ -328,10 +320,8 @@ def task_new(): """ Create new task ID """ - global tasks - taskid = hexencode(os.urandom(8)) - tasks[taskid] = Task(taskid) + DataStore.tasks[taskid] = Task(taskid) logger.debug("Created new task ID: %s" % taskid) return jsonize({"success": True, "taskid": taskid}) @@ -342,9 +332,9 @@ def task_delete(taskid): """ Delete own task ID """ - if taskid in tasks: - tasks[taskid].clean_filesystem() - tasks.pop(taskid) + if taskid in DataStore.tasks: + DataStore.tasks[taskid].clean_filesystem() + DataStore.tasks.pop(taskid) logger.debug("Deleted task ID: %s" % taskid) return jsonize({"success": True}) @@ -362,8 +352,8 @@ def task_list(taskid): """ if is_admin(taskid): logger.debug("Listed task pull") - task_list = list(tasks) - return jsonize({"success": True, "tasks": task_list, "tasks_num": len(tasks)}) + task_list = list(DataStore.tasks) + return jsonize({"success": True, "tasks": task_list, "tasks_num": len(task_list)}) else: return jsonize({"success": False, "message": "Unauthorized"}) @@ -373,13 +363,11 @@ def task_flush(taskid): """ Flush task spool (delete all tasks) """ - global tasks - if is_admin(taskid): - for task in tasks: - tasks[task].clean_filesystem() + for task in DataStore.tasks: + DataStore.tasks[task].clean_filesystem() - tasks = dict() + DataStore.tasks = dict() logger.debug("Flushed task pull") return jsonize({"success": True}) else: @@ -395,10 +383,10 @@ def option_list(taskid): """ List options for a certain task ID """ - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) - return jsonize({"success": True, "options": tasks[taskid].get_options()}) + return jsonize({"success": True, "options": DataStore.tasks[taskid].get_options()}) @post("/option//get") @@ -406,15 +394,13 @@ def option_get(taskid): """ Get the value of an option (command line switch) for a certain task ID """ - global tasks - - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) option = request.json.get("option", "") - if option in tasks[taskid].options: - return jsonize({"success": True, option: tasks[taskid].get_option(option)}) + if option in DataStore.tasks[taskid].options: + return jsonize({"success": True, option: DataStore.tasks[taskid].get_option(option)}) else: return jsonize({"success": False, "message": "Unknown option", option: "not set"}) @@ -424,13 +410,11 @@ def option_set(taskid): """ Set an option (command line switch) for a certain task ID """ - global tasks - - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) for option, value in request.json.items(): - tasks[taskid].set_option(option, value) + DataStore.tasks[taskid].set_option(option, value) return jsonize({"success": True}) @@ -440,23 +424,21 @@ def scan_start(taskid): """ Launch a scan """ - global tasks - - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) # Initialize sqlmap engine's options with user's provided options, if any for option, value in request.json.items(): - tasks[taskid].set_option(option, value) + DataStore.tasks[taskid].set_option(option, value) # Overwrite output directory value to a temporary directory - tasks[taskid].set_output_directory() + DataStore.tasks[taskid].set_output_directory() # Launch sqlmap engine in a separate process - tasks[taskid].engine_start() + DataStore.tasks[taskid].engine_start() logger.debug("Started scan for task ID %s" % taskid) - return jsonize({"success": True, "engineid": tasks[taskid].engine_get_id()}) + return jsonize({"success": True, "engineid": DataStore.tasks[taskid].engine_get_id()}) @get("/scan//stop") @@ -464,12 +446,10 @@ def scan_stop(taskid): """ Stop a scan """ - global tasks - - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) - tasks[taskid].engine_stop() + DataStore.tasks[taskid].engine_stop() logger.debug("Stopped scan for task ID %s" % taskid) return jsonize({"success": True}) @@ -480,12 +460,10 @@ def scan_kill(taskid): """ Kill a scan """ - global tasks - - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) - tasks[taskid].engine_kill() + DataStore.tasks[taskid].engine_kill() logger.debug("Killed scan for task ID %s" % taskid) return jsonize({"success": True}) @@ -496,18 +474,16 @@ def scan_status(taskid): """ Returns status of a scan """ - global tasks - - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) - status = "terminated" if tasks[taskid].engine_has_terminated() is True else "running" + status = "terminated" if DataStore.tasks[taskid].engine_has_terminated() is True else "running" logger.debug("Requested status of scan for task ID %s" % taskid) return jsonize({ "success": True, "status": status, - "returncode": tasks[taskid].engine_get_returncode() + "returncode": DataStore.tasks[taskid].engine_get_returncode() }) @@ -516,24 +492,23 @@ def scan_data(taskid): """ Retrieve the data of a scan """ - global db - global tasks json_data_message = list() json_errors_message = list() - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) # Read all data from the IPC database for the taskid - for status, content_type, value in db.execute( + for status, content_type, value in DataStore.current_db.execute( "SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)): json_data_message.append( {"status": status, "type": content_type, "value": dejsonize(value)}) # Read all error messages from the IPC database - for error in db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", - (taskid,)): + for error in DataStore.current_db.execute( + "SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", + (taskid,)): json_errors_message.append(error) logger.debug("Retrieved data and error messages for scan for task ID %s" % taskid) @@ -545,11 +520,9 @@ def scan_log_limited(taskid, start, end): """ Retrieve a subset of log messages """ - global db - global tasks json_log_messages = list() - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) if not start.isdigit() or not end.isdigit() or end < start: @@ -559,7 +532,7 @@ def scan_log_limited(taskid, start, end): end = max(1, int(end)) # Read a subset of log messages from the IPC database - for time_, level, message in db.execute( + for time_, level, message in DataStore.current_db.execute( ("SELECT time, level, message FROM logs WHERE " "taskid = ? AND id >= ? AND id <= ? ORDER BY id ASC"), (taskid, start, end)): @@ -574,15 +547,13 @@ def scan_log(taskid): """ Retrieve the log messages """ - global db - global tasks json_log_messages = list() - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) # Read all log messages from the IPC database - for time_, level, message in db.execute( + for time_, level, message in DataStore.current_db.execute( "SELECT time, level, message FROM logs WHERE taskid = ? ORDER BY id ASC", (taskid,)): json_log_messages.append({"time": time_, "level": level, "message": message}) @@ -595,7 +566,7 @@ def download(taskid, target, filename): """ Download a certain file from the file system """ - if taskid not in tasks: + if taskid not in DataStore.tasks: return jsonize({"success": False, "message": "Invalid task ID"}) # Prevent file path traversal - the lame way @@ -616,21 +587,17 @@ def server(host="0.0.0.0", port=RESTAPI_SERVER_PORT): """ REST-JSON API server """ - global adminid - global db - global db_filepath - - adminid = hexencode(os.urandom(16)) - db_filepath = tempfile.mkstemp(prefix="sqlmapipc-", text=False)[1] + DataStore.admin_id = hexencode(os.urandom(16)) + Database.filepath = tempfile.mkstemp(prefix="sqlmapipc-", text=False)[1] logger.info("Running REST-JSON API server at '%s:%d'.." % (host, port)) - logger.info("Admin ID: %s" % adminid) - logger.debug("IPC database: %s" % db_filepath) + logger.info("Admin ID: %s" % DataStore.admin_id) + logger.debug("IPC database: %s" % Database.filepath) # Initialize IPC database - db = Database() - db.connect() - db.init() + DataStore.current_db = Database() + DataStore.current_db.connect() + DataStore.current_db.init() # Run RESTful API run(host=host, port=port, quiet=True, debug=False) From eda9a3da671886038b5f4b08678b8e175e467f83 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sun, 15 Dec 2013 09:16:38 +0100 Subject: [PATCH 09/11] all instance attributes should be defined in constructor --- lib/utils/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utils/api.py b/lib/utils/api.py index 30427ebdc..e57ad0db0 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -71,6 +71,8 @@ class Database(object): def __init__(self, database=None): self.database = self.filepath if database is None else database + self.connection = None + self.cursor = None def connect(self, who="server"): self.connection = sqlite3.connect(self.database, timeout=3, isolation_level=None) From 438ad73016f893147ea4402d26a97b9316861784 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sun, 15 Dec 2013 09:22:01 +0100 Subject: [PATCH 10/11] avoid names shadowing --- lib/utils/api.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index e57ad0db0..9b8894dab 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -32,7 +32,7 @@ from lib.core.enums import PART_RUN_CONTENT_TYPES from lib.core.log import LOGGER_HANDLER from lib.core.optiondict import optDict from lib.core.subprocessng import Popen -from thirdparty.bottle.bottle import error +from thirdparty.bottle.bottle import error as return_error from thirdparty.bottle.bottle import get from thirdparty.bottle.bottle import hook from thirdparty.bottle.bottle import post @@ -182,6 +182,7 @@ class Task(object): def engine_has_terminated(self): return isinstance(self.engine_get_returncode(), int) + # Wrapper functions for sqlmap engine class StdDbOut(object): def __init__(self, taskid, messagetype="stdout"): @@ -289,25 +290,26 @@ def security_headers(json_header=True): # HTTP Status Code functions # ############################## -@error(401) # Access Denied + +@return_error(401) # Access Denied def error401(error=None): security_headers(False) return "Access denied" -@error(404) # Not Found +@return_error(404) # Not Found def error404(error=None): security_headers(False) return "Nothing here" -@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): security_headers(False) return "Method not allowed" -@error(500) # Internal Server Error +@return_error(500) # Internal Server Error def error500(error=None): security_headers(False) return "Internal server error" @@ -316,6 +318,7 @@ def error500(error=None): # Task management functions # ############################# + # Users' methods @get("/task/new") def task_new(): @@ -347,6 +350,7 @@ def task_delete(taskid): # Admin functions # ################### + @get("/admin//list") def task_list(taskid): """ @@ -354,8 +358,8 @@ def task_list(taskid): """ if is_admin(taskid): logger.debug("Listed task pull") - task_list = list(DataStore.tasks) - return jsonize({"success": True, "tasks": task_list, "tasks_num": len(task_list)}) + tasks = list(DataStore.tasks) + return jsonize({"success": True, "tasks": tasks, "tasks_num": len(tasks)}) else: return jsonize({"success": False, "message": "Unauthorized"}) @@ -379,6 +383,7 @@ def task_flush(taskid): # sqlmap core interact functions # ################################## + # Handle task's options @get("/option//list") def option_list(taskid): @@ -420,6 +425,7 @@ def option_set(taskid): return jsonize({"success": True}) + # Handle scans @post("/scan//start") def scan_start(taskid): @@ -516,6 +522,7 @@ def scan_data(taskid): logger.debug("Retrieved data and error messages for scan for task ID %s" % taskid) return jsonize({"success": True, "data": json_data_message, "error": json_errors_message}) + # Functions to handle scans' logs @get("/scan//log//") def scan_log_limited(taskid, start, end): @@ -562,6 +569,7 @@ def scan_log(taskid): logger.debug("Retrieved log messages for scan for task ID %s" % taskid) return jsonize({"success": True, "log": json_log_messages}) + # Function to handle files inside the output directory @get("/download///") def download(taskid, target, filename): From 4c9456dd72f86034c746a4488ee990921487f120 Mon Sep 17 00:00:00 2001 From: Mathieu Deous Date: Sun, 15 Dec 2013 16:59:47 +0100 Subject: [PATCH 11/11] moar logging! --- lib/utils/api.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/utils/api.py b/lib/utils/api.py index 9b8894dab..c44760698 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -212,7 +212,7 @@ class StdDbOut(object): # Delete partial output from IPC database if we have got a complete output if status == CONTENT_STATUS.COMPLETE: if len(output) > 0: - for index in xrange(0, len(output)): + for index in xrange(len(output)): conf.database_cursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],)) @@ -328,7 +328,7 @@ def task_new(): taskid = hexencode(os.urandom(8)) DataStore.tasks[taskid] = Task(taskid) - logger.debug("Created new task ID: %s" % taskid) + logger.debug(" [%s] Created new task" % taskid) return jsonize({"success": True, "taskid": taskid}) @@ -341,9 +341,10 @@ def task_delete(taskid): DataStore.tasks[taskid].clean_filesystem() DataStore.tasks.pop(taskid) - logger.debug("Deleted task ID: %s" % taskid) + logger.debug("[%s] Deleted task" % taskid) return jsonize({"success": True}) else: + logger.warning("[%s] Invalid task ID provided to task_delete()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) ################### @@ -357,10 +358,11 @@ def task_list(taskid): List task pull """ if is_admin(taskid): - logger.debug("Listed task pull") + logger.debug("[%s] Listed task pool" % taskid) tasks = list(DataStore.tasks) return jsonize({"success": True, "tasks": tasks, "tasks_num": len(tasks)}) else: + logger.warning("[%s] Unauthorized call to task_list()" % taskid) return jsonize({"success": False, "message": "Unauthorized"}) @@ -374,9 +376,10 @@ def task_flush(taskid): DataStore.tasks[task].clean_filesystem() DataStore.tasks = dict() - logger.debug("Flushed task pull") + logger.debug("[%s] Flushed task pool" % taskid) return jsonize({"success": True}) else: + logger.warning("[%s] Unauthorized call to task_flush()" % taskid) return jsonize({"success": False, "message": "Unauthorized"}) ################################## @@ -391,8 +394,10 @@ def option_list(taskid): List options for a certain task ID """ if taskid not in DataStore.tasks: + logger.warning("[%s] Invalid task ID provided to option_list()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) + logger.debug("[%s] Listed task options" % taskid) return jsonize({"success": True, "options": DataStore.tasks[taskid].get_options()}) @@ -402,13 +407,16 @@ def option_get(taskid): Get the value of 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_get()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) option = request.json.get("option", "") if option in DataStore.tasks[taskid].options: + logger.debug("[%s] Retrieved value for option %s" % (taskid, option)) return jsonize({"success": True, option: DataStore.tasks[taskid].get_option(option)}) else: + logger.debug("[%s] Requested value for unknown option %s" % (taskid, option)) return jsonize({"success": False, "message": "Unknown option", option: "not set"}) @@ -418,11 +426,13 @@ 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"}) for option, value in request.json.items(): DataStore.tasks[taskid].set_option(option, value) + logger.debug("[%s] Requested to set options" % taskid) return jsonize({"success": True}) @@ -433,6 +443,7 @@ 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"}) # Initialize sqlmap engine's options with user's provided options, if any @@ -445,7 +456,7 @@ def scan_start(taskid): # Launch sqlmap engine in a separate process DataStore.tasks[taskid].engine_start() - logger.debug("Started scan for task ID %s" % taskid) + logger.debug("[%s] Started scan" % taskid) return jsonize({"success": True, "engineid": DataStore.tasks[taskid].engine_get_id()}) @@ -455,11 +466,12 @@ def scan_stop(taskid): Stop a scan """ if taskid not in DataStore.tasks: + logger.warning("[%s] Invalid task ID provided to scan_stop()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) DataStore.tasks[taskid].engine_stop() - logger.debug("Stopped scan for task ID %s" % taskid) + logger.debug("[%s] Stopped scan" % taskid) return jsonize({"success": True}) @@ -469,11 +481,12 @@ def scan_kill(taskid): Kill a scan """ if taskid not in DataStore.tasks: + logger.warning("[%s] Invalid task ID provided to scan_kill()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) DataStore.tasks[taskid].engine_kill() - logger.debug("Killed scan for task ID %s" % taskid) + logger.debug("[%s] Killed scan" % taskid) return jsonize({"success": True}) @@ -483,11 +496,12 @@ 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"}) status = "terminated" if DataStore.tasks[taskid].engine_has_terminated() is True else "running" - logger.debug("Requested status of scan for task ID %s" % taskid) + logger.debug("[%s] Retrieved scan status" % taskid) return jsonize({ "success": True, "status": status, @@ -504,6 +518,7 @@ def scan_data(taskid): json_errors_message = list() if taskid not in DataStore.tasks: + logger.warning("[%s] Invalid task ID provided to scan_data()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) # Read all data from the IPC database for the taskid @@ -519,7 +534,7 @@ def scan_data(taskid): (taskid,)): json_errors_message.append(error) - logger.debug("Retrieved data and error messages for scan for task ID %s" % taskid) + logger.debug("[%s] Retrieved scan data and error messages" % taskid) return jsonize({"success": True, "data": json_data_message, "error": json_errors_message}) @@ -532,9 +547,11 @@ def scan_log_limited(taskid, start, end): json_log_messages = list() if taskid not in DataStore.tasks: + logger.warning("[%s] Invalid task ID provided to scan_log_limited()") return jsonize({"success": False, "message": "Invalid task ID"}) if not start.isdigit() or not end.isdigit() or end < start: + logger.warning("[%s] Invalid start or end value provided to scan_log_limited()" % taskid) return jsonize({"success": False, "message": "Invalid start or end value, must be digits"}) start = max(1, int(start)) @@ -547,7 +564,7 @@ def scan_log_limited(taskid, start, end): (taskid, start, end)): json_log_messages.append({"time": time_, "level": level, "message": message}) - logger.debug("Retrieved subset of log messages for scan for task ID %s" % taskid) + logger.debug("[%s] Retrieved scan log messages subset" % taskid) return jsonize({"success": True, "log": json_log_messages}) @@ -559,6 +576,7 @@ def scan_log(taskid): json_log_messages = list() if taskid not in DataStore.tasks: + logger.warning("[%s] Invalid task ID provided to scan_log()") return jsonize({"success": False, "message": "Invalid task ID"}) # Read all log messages from the IPC database @@ -566,7 +584,7 @@ def scan_log(taskid): "SELECT time, level, message FROM logs WHERE taskid = ? ORDER BY id ASC", (taskid,)): json_log_messages.append({"time": time_, "level": level, "message": message}) - logger.debug("Retrieved log messages for scan for task ID %s" % taskid) + logger.debug("[%s] Retrieved scan log messages" % taskid) return jsonize({"success": True, "log": json_log_messages}) @@ -577,19 +595,23 @@ 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"}) # Prevent file path traversal - the lame way if ".." in target: + logger.warning("[%s] Forbidden path (%s)" % (taskid, target)) return jsonize({"success": False, "message": "Forbidden path"}) path = os.path.join(paths.SQLMAP_OUTPUT_PATH, target) if os.path.exists(path): + logger.debug("[%s] Retrieved content of file %s" % (taskid, target)) with open(path, 'rb') as inf: file_content = inf.read() return jsonize({"success": True, "file": file_content.encode("base64")}) else: + logger.warning("[%s] File does not exist %s" % (taskid, target)) return jsonize({"success": False, "message": "File does not exist"})