sqlmap/lib/utils/api.py

919 lines
34 KiB
Python
Raw Normal View History

2019-05-08 13:47:52 +03:00
#!/usr/bin/env python
2013-12-14 18:44:10 +04:00
# -*- coding: utf-8 -*-
"""
2024-01-04 01:11:52 +03:00
Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
2017-10-11 15:50:46 +03:00
See the file 'LICENSE' for copying permission
"""
2019-01-22 03:28:24 +03:00
from __future__ import print_function
2017-01-20 15:11:12 +03:00
import contextlib
import logging
import os
2015-09-10 16:01:30 +03:00
import re
import shlex
import socket
import sqlite3
import sys
import tempfile
2022-12-17 16:46:00 +03:00
import threading
import time
2015-09-10 16:01:30 +03:00
from lib.core.common import dataToStdout
from lib.core.common import getSafeExString
2019-05-13 12:51:47 +03:00
from lib.core.common import openFile
2017-04-10 15:50:17 +03:00
from lib.core.common import saveConfig
2021-09-09 21:27:45 +03:00
from lib.core.common import setColor
2012-12-20 20:53:43 +04:00
from lib.core.common import unArrayizeValue
2019-03-28 18:04:38 +03:00
from lib.core.compat import xrange
2019-05-03 14:20:15 +03:00
from lib.core.convert import decodeBase64
from lib.core.convert import dejsonize
2019-06-04 15:44:06 +03:00
from lib.core.convert import encodeBase64
from lib.core.convert import encodeHex
2020-01-16 00:47:06 +03:00
from lib.core.convert import getBytes
from lib.core.convert import getText
from lib.core.convert import jsonize
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
2019-06-04 15:44:06 +03:00
from lib.core.data import paths
from lib.core.datatype import AttribDict
2016-10-18 21:17:51 +03:00
from lib.core.defaults import _defaults
2018-06-21 00:21:55 +03:00
from lib.core.dicts import PART_RUN_CONTENT_TYPES
2018-06-21 00:04:58 +03:00
from lib.core.enums import AUTOCOMPLETE_TYPE
from lib.core.enums import CONTENT_STATUS
2016-05-31 14:02:26 +03:00
from lib.core.enums import MKSTEMP_PREFIX
2015-03-10 00:01:59 +03:00
from lib.core.exception import SqlmapConnectionException
from lib.core.log import LOGGER_HANDLER
2012-12-20 20:53:43 +04:00
from lib.core.optiondict import optDict
2015-05-07 13:36:23 +03:00
from lib.core.settings import IS_WIN
2019-06-04 15:44:06 +03:00
from lib.core.settings import RESTAPI_DEFAULT_ADAPTER
2015-12-13 01:48:30 +03:00
from lib.core.settings import RESTAPI_DEFAULT_ADDRESS
from lib.core.settings import RESTAPI_DEFAULT_PORT
2021-01-12 17:58:40 +03:00
from lib.core.settings import RESTAPI_UNSUPPORTED_OPTIONS
2020-05-20 16:20:44 +03:00
from lib.core.settings import VERSION_STRING
2018-06-21 00:04:58 +03:00
from lib.core.shell import autoCompletion
from lib.core.subprocessng import Popen
2015-09-09 16:14:04 +03:00
from lib.parse.cmdline import cmdLineParser
2013-12-15 12:22:01 +04:00
from thirdparty.bottle.bottle import error as return_error
2012-12-22 03:34:37 +04:00
from thirdparty.bottle.bottle import get
from thirdparty.bottle.bottle import hook
from thirdparty.bottle.bottle import post
from thirdparty.bottle.bottle import request
from thirdparty.bottle.bottle import response
from thirdparty.bottle.bottle import run
2017-04-07 15:46:41 +03:00
from thirdparty.bottle.bottle import server_names
2020-08-10 22:26:37 +03:00
from thirdparty import six
from thirdparty.six.moves import http_client as _http_client
2019-05-02 01:45:44 +03:00
from thirdparty.six.moves import input as _input
from thirdparty.six.moves import urllib as _urllib
# Global data storage
class DataStore(object):
2018-11-15 17:27:05 +03:00
admin_token = ""
current_db = None
tasks = dict()
username = None
password = None
2013-12-14 18:44:10 +04:00
# API objects
class Database(object):
filepath = None
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"):
2017-06-18 16:00:12 +03:00
self.connection = sqlite3.connect(self.database, timeout=3, isolation_level=None, check_same_thread=False)
self.cursor = self.connection.cursor()
2022-12-17 16:46:00 +03:00
self.lock = threading.Lock()
logger.debug("REST-JSON API %s connected to IPC database" % who)
def disconnect(self):
2015-03-10 00:01:59 +03:00
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
2013-02-06 21:09:43 +04:00
def commit(self):
self.connection.commit()
2013-02-06 21:09:43 +04:00
def execute(self, statement, arguments=None):
2022-12-17 16:46:00 +03:00
with self.lock:
while True:
try:
if arguments:
self.cursor.execute(statement, arguments)
else:
self.cursor.execute(statement)
except sqlite3.OperationalError as ex:
if "locked" not in getSafeExString(ex):
raise
else:
time.sleep(1)
2015-04-22 17:41:20 +03:00
else:
2022-12-17 16:46:00 +03:00
break
if statement.lstrip().upper().startswith("SELECT"):
return self.cursor.fetchall()
def init(self):
self.execute("CREATE TABLE logs(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, time TEXT, level TEXT, message TEXT)")
self.execute("CREATE TABLE data(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, status INTEGER, content_type INTEGER, value TEXT)")
self.execute("CREATE TABLE errors(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, error TEXT)")
class Task(object):
2015-09-15 15:37:30 +03:00
def __init__(self, taskid, remote_addr):
self.remote_addr = remote_addr
self.process = None
self.output_directory = None
self.options = None
self._original_options = None
self.initialize_options(taskid)
def initialize_options(self, taskid):
datatype = {"boolean": False, "string": None, "integer": None, "float": None}
self.options = AttribDict()
for _ in optDict:
for name, type_ in optDict[_].items():
type_ = unArrayizeValue(type_)
2016-10-18 21:17:51 +03:00
self.options[name] = _defaults.get(name, datatype[type_])
2013-12-14 18:44:10 +04:00
# 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 = Database.filepath
2013-02-06 21:45:52 +04:00
# Enforce batch mode and disable coloring and ETA
self.options.batch = True
self.options.disableColoring = True
2013-02-06 21:45:52 +04:00
self.options.eta = False
self._original_options = AttribDict(self.options)
def set_option(self, option, value):
self.options[option] = value
def get_option(self, option):
return self.options[option]
def get_options(self):
return self.options
def reset_options(self):
self.options = AttribDict(self._original_options)
def engine_start(self):
2017-04-14 14:08:51 +03:00
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
os.close(handle)
2017-04-10 15:50:17 +03:00
saveConfig(self.options, configFile)
2015-09-08 12:04:36 +03:00
if os.path.exists("sqlmap.py"):
2019-02-09 17:11:06 +03:00
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, close_fds=not IS_WIN)
2017-01-16 15:44:46 +03:00
elif os.path.exists(os.path.join(os.getcwd(), "sqlmap.py")):
2019-02-09 17:11:06 +03:00
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, cwd=os.getcwd(), close_fds=not IS_WIN)
2018-06-07 11:07:12 +03:00
elif os.path.exists(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "sqlmap.py")):
2019-02-09 17:11:06 +03:00
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, cwd=os.path.join(os.path.abspath(os.path.dirname(sys.argv[0]))), close_fds=not IS_WIN)
2015-09-08 12:04:36 +03:00
else:
2017-04-10 15:50:17 +03:00
self.process = Popen(["sqlmap", "--api", "-c", configFile], shell=False, close_fds=not IS_WIN)
def engine_stop(self):
if self.process:
self.process.terminate()
return self.process.wait()
else:
return None
2014-01-02 15:15:56 +04:00
def engine_process(self):
return self.process
def engine_kill(self):
if self.process:
2016-01-27 23:25:34 +03:00
try:
self.process.kill()
2016-01-28 11:10:04 +03:00
return self.process.wait()
2016-01-27 23:25:34 +03:00
except:
pass
2016-01-28 11:10:04 +03:00
return None
def engine_get_id(self):
if self.process:
return self.process.pid
else:
return None
def engine_get_returncode(self):
2014-01-02 13:55:40 +04:00
if self.process:
self.process.poll()
return self.process.returncode
else:
return None
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"):
# Overwrite system standard output and standard error to write
# to an IPC database
self.messagetype = messagetype
self.taskid = taskid
if self.messagetype == "stdout":
sys.stdout = self
else:
sys.stderr = self
def write(self, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None):
if self.messagetype == "stdout":
if content_type is None:
if kb.partRun is not None:
content_type = PART_RUN_CONTENT_TYPES.get(kb.partRun)
else:
# Ignore all non-relevant messages
return
2017-06-14 15:13:41 +03:00
output = conf.databaseCursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", (self.taskid, content_type))
2013-02-06 21:09:43 +04:00
# Delete partial output from IPC database if we have got a complete output
if status == CONTENT_STATUS.COMPLETE:
if len(output) > 0:
2013-12-15 19:59:47 +04:00
for index in xrange(len(output)):
2017-06-14 15:13:41 +03:00
conf.databaseCursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],))
2017-06-14 15:13:41 +03:00
conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value)))
2013-02-06 21:45:52 +04:00
if kb.partRun:
kb.partRun = None
elif status == CONTENT_STATUS.IN_PROGRESS:
if len(output) == 0:
2017-06-14 15:13:41 +03:00
conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value)))
else:
new_value = "%s%s" % (dejsonize(output[0][2]), value)
2017-06-14 15:13:41 +03:00
conf.databaseCursor.execute("UPDATE data SET value = ? WHERE id = ?", (jsonize(new_value), output[0][0]))
else:
2017-06-14 15:13:41 +03:00
conf.databaseCursor.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (self.taskid, str(value) if value else ""))
def flush(self):
pass
def close(self):
pass
def seek(self):
pass
class LogRecorder(logging.StreamHandler):
def emit(self, record):
"""
Record emitted events to IPC database for asynchronous I/O
communication with the parent process
"""
2017-06-14 15:13:41 +03:00
conf.databaseCursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, record.msg % record.args if record.args else record.msg))
def setRestAPILog():
2017-04-10 20:21:22 +03:00
if conf.api:
2015-03-10 00:01:59 +03:00
try:
2016-08-08 17:08:16 +03:00
conf.databaseCursor = Database(conf.database)
conf.databaseCursor.connect("client")
2019-01-22 02:40:48 +03:00
except sqlite3.OperationalError as ex:
2018-03-13 13:13:38 +03:00
raise SqlmapConnectionException("%s ('%s')" % (ex, conf.database))
# Set a logging handler that writes log messages to a IPC database
logger.removeHandler(LOGGER_HANDLER)
LOGGER_RECORDER = LogRecorder()
logger.addHandler(LOGGER_RECORDER)
# Generic functions
2018-11-15 17:27:05 +03:00
def is_admin(token):
return DataStore.admin_token == token
@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:
2019-05-03 00:51:54 +03:00
creds = decodeBase64(match.group(1), binary=False)
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"
2013-12-14 18:44:10 +04:00
@hook("after_request")
def security_headers(json_header=True):
"""
Set some headers across all HTTP responses
"""
response.headers["Server"] = "Server"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
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"
##############################
# HTTP Status Code functions #
##############################
2013-12-15 12:22:01 +04:00
@return_error(401) # Access Denied
def error401(error=None):
security_headers(False)
return "Access denied"
2013-12-15 12:22:01 +04:00
@return_error(404) # Not Found
def error404(error=None):
security_headers(False)
return "Nothing here"
2013-12-15 12:22:01 +04:00
@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"
2013-12-15 12:22:01 +04:00
@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():
"""
2018-11-15 17:27:05 +03:00
Create a new task
"""
2019-05-03 14:20:15 +03:00
taskid = encodeHex(os.urandom(8), binary=False)
2015-09-15 15:37:30 +03:00
remote_addr = request.remote_addr
DataStore.tasks[taskid] = Task(taskid, remote_addr)
2014-12-23 11:07:33 +03:00
logger.debug("Created new task: '%s'" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": True, "taskid": taskid})
@get("/task/<taskid>/delete")
def task_delete(taskid):
"""
2018-11-15 17:27:05 +03:00
Delete an existing task
"""
if taskid in DataStore.tasks:
DataStore.tasks.pop(taskid)
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Deleted task" % taskid)
return jsonize({"success": True})
else:
2018-11-15 17:27:05 +03:00
response.status = 404
logger.warning("[%s] Non-existing task ID provided to task_delete()" % taskid)
return jsonize({"success": False, "message": "Non-existing task ID"})
###################
# Admin functions #
###################
2018-11-15 17:27:05 +03:00
@get("/admin/list")
@get("/admin/<token>/list")
def task_list(token=None):
"""
2018-11-15 17:27:05 +03:00
Pull task list
"""
tasks = {}
for key in DataStore.tasks:
2018-11-15 17:27:05 +03:00
if is_admin(token) or DataStore.tasks[key].remote_addr == request.remote_addr:
tasks[key] = dejsonize(scan_status(key))["status"]
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Listed task pool (%s)" % (token, "admin" if is_admin(token) else request.remote_addr))
return jsonize({"success": True, "tasks": tasks, "tasks_num": len(tasks)})
2018-11-15 17:27:05 +03:00
@get("/admin/flush")
@get("/admin/<token>/flush")
def task_flush(token=None):
"""
Flush task spool (delete all tasks)
"""
2016-01-27 23:25:34 +03:00
for key in list(DataStore.tasks):
2018-11-15 17:27:05 +03:00
if is_admin(token) or DataStore.tasks[key].remote_addr == request.remote_addr:
2016-01-27 23:25:34 +03:00
DataStore.tasks[key].engine_kill()
del DataStore.tasks[key]
2015-09-15 15:37:30 +03:00
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Flushed task pool (%s)" % (token, "admin" if is_admin(token) else request.remote_addr))
2015-09-15 15:37:30 +03:00
return jsonize({"success": True})
##################################
# sqlmap core interact functions #
##################################
# Handle task's options
@get("/option/<taskid>/list")
def option_list(taskid):
"""
List options for a certain task ID
"""
if taskid not in DataStore.tasks:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to option_list()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
2018-12-17 18:08:14 +03:00
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):
"""
2018-11-15 19:13:13 +03:00
Get value of option(s) for a certain task ID
"""
if taskid not in DataStore.tasks:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to option_get()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
2018-11-15 19:13:13 +03:00
options = request.json or []
results = {}
2018-11-15 19:13:13 +03:00
for option in options:
if option in DataStore.tasks[taskid].options:
results[option] = DataStore.tasks[taskid].options[option]
else:
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Requested value for unknown option '%s'" % (taskid, option))
2018-11-15 19:13:13 +03:00
return jsonize({"success": False, "message": "Unknown option '%s'" % option})
2020-01-01 15:30:20 +03:00
logger.debug("(%s) Retrieved values for option(s) '%s'" % (taskid, ','.join(options)))
2018-11-15 19:13:13 +03:00
return jsonize({"success": True, "options": results})
@post("/option/<taskid>/set")
def option_set(taskid):
"""
2018-11-15 19:13:13 +03:00
Set value of option(s) for a certain task ID
"""
if taskid not in DataStore.tasks:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to option_set()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
2017-09-04 18:16:00 +03:00
if request.json is None:
logger.warning("[%s] Invalid JSON options provided to option_set()" % taskid)
return jsonize({"success": False, "message": "Invalid JSON options"})
for option, value in request.json.items():
DataStore.tasks[taskid].set_option(option, value)
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Requested to set options" % taskid)
return jsonize({"success": True})
# Handle scans
@post("/scan/<taskid>/start")
2012-12-15 04:29:35 +04:00
def scan_start(taskid):
"""
Launch a scan
"""
if taskid not in DataStore.tasks:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to scan_start()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
2017-09-04 18:16:00 +03:00
if request.json is None:
logger.warning("[%s] Invalid JSON options provided to scan_start()" % taskid)
return jsonize({"success": False, "message": "Invalid JSON options"})
2021-01-12 17:58:40 +03:00
for key in request.json:
if key in RESTAPI_UNSUPPORTED_OPTIONS:
logger.warning("[%s] Unsupported option '%s' provided to scan_start()" % (taskid, key))
return jsonize({"success": False, "message": "Unsupported option '%s'" % key})
# Initialize sqlmap engine's options with user's provided options, if any
for option, value in request.json.items():
DataStore.tasks[taskid].set_option(option, value)
2012-12-15 02:00:42 +04:00
# Launch sqlmap engine in a separate process
DataStore.tasks[taskid].engine_start()
2018-12-17 18:08:14 +03:00
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
"""
2018-06-10 00:38:00 +03:00
if (taskid not in DataStore.tasks or DataStore.tasks[taskid].engine_process() is None or DataStore.tasks[taskid].engine_has_terminated()):
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to scan_stop()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
DataStore.tasks[taskid].engine_stop()
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Stopped scan" % taskid)
return jsonize({"success": True})
@get("/scan/<taskid>/kill")
def scan_kill(taskid):
"""
Kill a scan
"""
2018-06-10 00:38:00 +03:00
if (taskid not in DataStore.tasks or DataStore.tasks[taskid].engine_process() is None or DataStore.tasks[taskid].engine_has_terminated()):
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to scan_kill()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
DataStore.tasks[taskid].engine_kill()
2018-12-17 18:08:14 +03:00
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:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to scan_status()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
2014-01-02 15:15:56 +04:00
if DataStore.tasks[taskid].engine_process() is None:
2014-01-02 13:55:40 +04:00
status = "not running"
else:
status = "terminated" if DataStore.tasks[taskid].engine_has_terminated() is True else "running"
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Retrieved scan status" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({
"success": True,
"status": status,
"returncode": DataStore.tasks[taskid].engine_get_returncode()
2013-12-14 19:22:30 +04:00
})
@get("/scan/<taskid>/data")
def scan_data(taskid):
"""
Retrieve the data of a scan
"""
json_data_message = list()
json_errors_message = list()
if taskid not in DataStore.tasks:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to scan_data()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
# Read all data from the IPC database for the taskid
2017-06-14 15:13:41 +03:00
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
2017-06-14 15:13:41 +03:00
for error in DataStore.current_db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)):
json_errors_message.append(error)
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Retrieved scan data and error messages" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": True, "data": json_data_message, "error": json_errors_message})
# Functions to handle scans' logs
@get("/scan/<taskid>/log/<start>/<end>")
def scan_log_limited(taskid, start, end):
"""
Retrieve a subset of log messages
"""
json_log_messages = list()
if taskid not in DataStore.tasks:
2015-09-10 12:34:03 +03:00
logger.warning("[%s] Invalid task ID provided to scan_log_limited()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
if not start.isdigit() or not end.isdigit() or int(end) < int(start):
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid start or end value provided to scan_log_limited()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid start or end value, must be digits"})
start = max(1, int(start))
end = max(1, int(end))
# Read a subset of log messages from the IPC database
2017-06-14 15:13:41 +03:00
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)):
json_log_messages.append({"time": time_, "level": level, "message": message})
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Retrieved scan log messages subset" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": True, "log": json_log_messages})
@get("/scan/<taskid>/log")
def scan_log(taskid):
"""
2012-12-17 15:28:03 +04:00
Retrieve the log messages
"""
json_log_messages = list()
if taskid not in DataStore.tasks:
2015-09-10 12:34:03 +03:00
logger.warning("[%s] Invalid task ID provided to scan_log()" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": False, "message": "Invalid task ID"})
# Read all log messages from the IPC database
2017-06-14 15:13:41 +03:00
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})
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Retrieved scan log messages" % taskid)
2013-12-14 19:22:30 +04:00
return jsonize({"success": True, "log": json_log_messages})
# Function to handle files inside the output directory
@get("/download/<taskid>/<target>/<filename:path>")
def download(taskid, target, filename):
"""
Download a certain file from the file system
"""
if taskid not in DataStore.tasks:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Invalid task ID provided to download()" % taskid)
return jsonize({"success": False, "message": "Invalid task ID"})
path = os.path.abspath(os.path.join(paths.SQLMAP_OUTPUT_PATH, target, filename))
# Prevent file path traversal
if not path.startswith(paths.SQLMAP_OUTPUT_PATH):
2013-12-15 19:59:47 +04:00
logger.warning("[%s] Forbidden path (%s)" % (taskid, target))
return jsonize({"success": False, "message": "Forbidden path"})
if os.path.isfile(path):
2018-12-17 18:08:14 +03:00
logger.debug("(%s) Retrieved content of file %s" % (taskid, target))
2019-05-13 12:51:47 +03:00
content = openFile(path, "rb").read()
return jsonize({"success": True, "file": encodeBase64(content, binary=False)})
else:
2013-12-15 19:59:47 +04:00
logger.warning("[%s] File does not exist %s" % (taskid, target))
return jsonize({"success": False, "message": "File does not exist"})
2020-05-20 16:20:44 +03:00
@get("/version")
def version(token=None):
"""
Fetch server version
"""
logger.debug("Fetched version (%s)" % ("admin" if is_admin(token) else request.remote_addr))
return jsonize({"success": True, "version": VERSION_STRING.split('/')[-1]})
2024-01-11 18:11:40 +03:00
def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None, database=None):
"""
REST-JSON API server
"""
2019-05-03 14:20:15 +03:00
DataStore.admin_token = encodeHex(os.urandom(16), binary=False)
DataStore.username = username
DataStore.password = password
2024-01-11 18:11:40 +03:00
if not database:
_, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False)
os.close(_)
else:
Database.filepath = database
2017-01-20 15:11:12 +03:00
if port == 0: # random
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind((host, 0))
port = s.getsockname()[1]
logger.info("Running REST-JSON API server at '%s:%d'.." % (host, port))
2018-11-15 17:27:05 +03:00
logger.info("Admin (secret) token: %s" % DataStore.admin_token)
2017-04-10 15:50:17 +03:00
logger.debug("IPC database: '%s'" % Database.filepath)
# Initialize IPC database
DataStore.current_db = Database()
DataStore.current_db.connect()
DataStore.current_db.init()
# Run RESTful API
try:
2017-04-07 15:30:52 +03:00
# Supported adapters: aiohttp, auto, bjoern, cgi, cherrypy, diesel, eventlet, fapws3, flup, gae, gevent, geventSocketIO, gunicorn, meinheld, paste, rocket, tornado, twisted, waitress, wsgiref
# Reference: https://bottlepy.org/docs/dev/deployment.html || bottle.server_names
2016-01-27 12:03:30 +03:00
if adapter == "gevent":
from gevent import monkey
monkey.patch_all()
2016-01-27 12:03:30 +03:00
elif adapter == "eventlet":
import eventlet
eventlet.monkey_patch()
2016-01-27 12:03:30 +03:00
logger.debug("Using adapter '%s' to run bottle" % adapter)
run(host=host, port=port, quiet=True, debug=True, server=adapter)
2019-01-22 02:40:48 +03:00
except socket.error as ex:
if "already in use" in getSafeExString(ex):
logger.error("Address already in use ('%s:%s')" % (host, port))
else:
raise
2016-01-27 12:03:30 +03:00
except ImportError:
2017-04-07 15:46:41 +03:00
if adapter.lower() not in server_names:
errMsg = "Adapter '%s' is unknown. " % adapter
errMsg += "List of supported adapters: %s" % ', '.join(sorted(list(server_names.keys())))
2017-04-07 15:46:41 +03:00
else:
2017-04-07 15:55:25 +03:00
errMsg = "Server support for adapter '%s' is not installed on this system " % adapter
2021-07-19 14:58:54 +03:00
errMsg += "(Note: you can try to install it with 'apt install python-%s' or 'pip%s install %s')" % (adapter, '3' if six.PY3 else "", adapter)
2016-01-27 12:03:30 +03:00
logger.critical(errMsg)
2013-12-14 18:44:10 +04:00
2015-09-10 16:06:07 +03:00
def _client(url, options=None):
2018-11-15 17:27:05 +03:00
logger.debug("Calling '%s'" % url)
2015-09-09 16:14:04 +03:00
try:
headers = {"Content-Type": "application/json"}
2020-01-16 00:47:06 +03:00
if options is not None:
data = getBytes(jsonize(options))
else:
data = None
if DataStore.username or DataStore.password:
2019-05-03 14:20:15 +03:00
headers["Authorization"] = "Basic %s" % encodeBase64("%s:%s" % (DataStore.username or "", DataStore.password or ""), binary=False)
req = _urllib.request.Request(url, data, headers)
response = _urllib.request.urlopen(req)
2020-01-16 00:47:06 +03:00
text = getText(response.read())
2015-09-09 16:14:04 +03:00
except:
2015-09-10 16:06:07 +03:00
if options:
2015-09-17 17:18:58 +03:00
logger.error("Failed to load and parse %s" % url)
2015-09-09 16:14:04 +03:00
raise
return text
def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=None, password=None):
2012-12-15 04:29:35 +04:00
"""
REST-JSON API client
"""
DataStore.username = username
DataStore.password = password
dbgMsg = "Example client access from command line:"
2019-05-08 13:28:50 +03:00
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 http://%s:%d/scan/$taskid/data" % (host, port)
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port)
logger.debug(dbgMsg)
addr = "http://%s:%d" % (host, port)
logger.info("Starting REST-JSON API client to '%s'..." % addr)
2015-09-09 16:14:04 +03:00
2015-09-10 16:01:30 +03:00
try:
_client(addr)
2019-01-22 02:40:48 +03:00
except Exception as ex:
if not isinstance(ex, _urllib.error.HTTPError) or ex.code == _http_client.UNAUTHORIZED:
2016-01-27 12:03:30 +03:00
errMsg = "There has been a problem while connecting to the "
2015-09-10 16:01:30 +03:00
errMsg += "REST-JSON API server at '%s' " % addr
2021-09-02 22:44:41 +03:00
errMsg += "(%s)" % getSafeExString(ex)
2015-09-10 16:01:30 +03:00
logger.critical(errMsg)
return
2020-05-20 16:20:44 +03:00
commands = ("help", "new", "use", "data", "log", "status", "option", "stop", "kill", "list", "flush", "version", "exit", "bye", "quit")
2021-09-09 21:27:45 +03:00
colors = ('red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightgrey', 'lightred', 'lightgreen', 'lightyellow', 'lightblue', 'lightmagenta', 'lightcyan')
2018-06-21 00:04:58 +03:00
autoCompletion(AUTOCOMPLETE_TYPE.API, commands=commands)
2015-09-10 16:01:30 +03:00
taskid = None
logger.info("Type 'help' or '?' for list of available commands")
2015-09-09 16:14:04 +03:00
while True:
2015-09-10 16:01:30 +03:00
try:
2021-09-09 21:27:45 +03:00
color = colors[int(taskid or "0", 16) % len(colors)]
command = _input("api%s> " % (" (%s)" % setColor(taskid, color) if taskid else "")).strip()
command = re.sub(r"\A(\w+)", lambda match: match.group(1).lower(), command)
2015-09-10 16:01:30 +03:00
except (EOFError, KeyboardInterrupt):
2019-01-22 03:28:24 +03:00
print()
2015-09-10 16:01:30 +03:00
break
2015-09-16 00:15:16 +03:00
if command in ("data", "log", "status", "stop", "kill"):
2015-09-10 16:01:30 +03:00
if not taskid:
logger.error("No task ID in use")
2015-09-09 16:14:04 +03:00
continue
2015-09-17 17:18:58 +03:00
raw = _client("%s/scan/%s/%s" % (addr, taskid, command))
2015-09-10 16:01:30 +03:00
res = dejsonize(raw)
if not res["success"]:
2015-09-17 17:18:58 +03:00
logger.error("Failed to execute command %s" % command)
2015-09-10 16:01:30 +03:00
dataToStdout("%s\n" % raw)
2017-07-05 14:51:48 +03:00
2017-02-25 09:58:59 +03:00
elif command.startswith("option"):
2017-02-25 06:24:00 +03:00
if not taskid:
logger.error("No task ID in use")
continue
try:
2018-11-15 19:13:13 +03:00
command, option = command.split(" ", 1)
2017-02-25 06:24:00 +03:00
except ValueError:
raw = _client("%s/option/%s/list" % (addr, taskid))
else:
2018-11-15 19:13:13 +03:00
options = re.split(r"\s*,\s*", option.strip())
2017-02-25 06:24:00 +03:00
raw = _client("%s/option/%s/get" % (addr, taskid), options)
res = dejsonize(raw)
if not res["success"]:
logger.error("Failed to execute command %s" % command)
dataToStdout("%s\n" % raw)
2015-09-10 16:01:30 +03:00
2015-09-16 00:15:16 +03:00
elif command.startswith("new"):
2015-09-10 16:01:30 +03:00
if ' ' not in command:
logger.error("Program arguments are missing")
2015-09-09 16:14:04 +03:00
continue
2017-02-25 09:54:54 +03:00
try:
argv = ["sqlmap.py"] + shlex.split(command)[1:]
2019-01-22 02:40:48 +03:00
except Exception as ex:
2021-09-02 22:44:41 +03:00
logger.error("Error occurred while parsing arguments ('%s')" % getSafeExString(ex))
2017-02-25 09:54:54 +03:00
taskid = None
continue
2015-09-10 16:01:30 +03:00
2015-09-09 16:14:04 +03:00
try:
2015-09-10 16:06:07 +03:00
cmdLineOptions = cmdLineParser(argv).__dict__
2015-09-09 16:14:04 +03:00
except:
2015-09-10 16:01:30 +03:00
taskid = None
continue
2015-09-15 18:23:59 +03:00
for key in list(cmdLineOptions):
if cmdLineOptions[key] is None:
del cmdLineOptions[key]
2015-09-10 16:01:30 +03:00
2015-09-17 17:18:58 +03:00
raw = _client("%s/task/new" % addr)
2015-09-10 16:01:30 +03:00
res = dejsonize(raw)
if not res["success"]:
2021-01-12 17:58:40 +03:00
logger.error("Failed to create new task ('%s')" % res.get("message", ""))
2015-09-09 16:14:04 +03:00
continue
2015-09-10 16:01:30 +03:00
taskid = res["taskid"]
logger.info("New task ID is '%s'" % taskid)
2015-09-17 17:18:58 +03:00
raw = _client("%s/scan/%s/start" % (addr, taskid), cmdLineOptions)
2015-09-10 16:01:30 +03:00
res = dejsonize(raw)
if not res["success"]:
2021-01-12 17:58:40 +03:00
logger.error("Failed to start scan ('%s')" % res.get("message", ""))
2015-09-09 16:14:04 +03:00
continue
logger.info("Scanning started")
2015-09-10 16:01:30 +03:00
2015-09-16 00:15:16 +03:00
elif command.startswith("use"):
2015-09-10 16:01:30 +03:00
taskid = (command.split()[1] if ' ' in command else "").strip("'\"")
if not taskid:
logger.error("Task ID is missing")
taskid = None
continue
elif not re.search(r"\A[0-9a-fA-F]{16}\Z", taskid):
logger.error("Invalid task ID '%s'" % taskid)
taskid = None
continue
logger.info("Switching to task ID '%s' " % taskid)
2020-05-20 16:20:44 +03:00
elif command in ("version",):
raw = _client("%s/%s" % (addr, command))
res = dejsonize(raw)
if not res["success"]:
logger.error("Failed to execute command %s" % command)
dataToStdout("%s\n" % raw)
2015-09-16 00:15:16 +03:00
elif command in ("list", "flush"):
2018-11-15 17:27:05 +03:00
raw = _client("%s/admin/%s" % (addr, command))
res = dejsonize(raw)
if not res["success"]:
2015-09-17 17:18:58 +03:00
logger.error("Failed to execute command %s" % command)
elif command == "flush":
taskid = None
dataToStdout("%s\n" % raw)
2015-09-16 00:15:16 +03:00
elif command in ("exit", "bye", "quit", 'q'):
2015-09-09 16:14:04 +03:00
return
2015-09-10 16:01:30 +03:00
2015-09-16 00:15:16 +03:00
elif command in ("help", "?"):
msg = "help Show this help message\n"
2017-02-25 09:54:54 +03:00
msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"http://testphp.vulnweb.com/artists.php?artist=1\"')\n"
msg += "use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4')\n"
msg += "data Retrieve and show data for current task\n"
msg += "log Retrieve and show log for current task\n"
msg += "status Retrieve and show status for current task\n"
msg += "option OPTION Retrieve and show option for current task\n"
msg += "options Retrieve and show all options for current task\n"
msg += "stop Stop current task\n"
msg += "kill Kill current task\n"
msg += "list Display all tasks\n"
2020-05-20 16:20:44 +03:00
msg += "version Fetch server version\n"
2017-02-25 09:54:54 +03:00
msg += "flush Flush tasks (delete all tasks)\n"
msg += "exit Exit this client\n"
2015-09-10 16:01:30 +03:00
dataToStdout(msg)
elif command:
logger.error("Unknown command '%s'" % command)