Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/lib/utils/api.py
2989 views
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
4
"""
5
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org)
6
See the file 'LICENSE' for copying permission
7
"""
8
9
from __future__ import print_function
10
11
import contextlib
12
import logging
13
import os
14
import re
15
import shlex
16
import socket
17
import sqlite3
18
import sys
19
import tempfile
20
import threading
21
import time
22
23
from lib.core.common import dataToStdout
24
from lib.core.common import getSafeExString
25
from lib.core.common import openFile
26
from lib.core.common import saveConfig
27
from lib.core.common import setColor
28
from lib.core.common import unArrayizeValue
29
from lib.core.compat import xrange
30
from lib.core.convert import decodeBase64
31
from lib.core.convert import dejsonize
32
from lib.core.convert import encodeBase64
33
from lib.core.convert import encodeHex
34
from lib.core.convert import getBytes
35
from lib.core.convert import getText
36
from lib.core.convert import jsonize
37
from lib.core.data import conf
38
from lib.core.data import kb
39
from lib.core.data import logger
40
from lib.core.data import paths
41
from lib.core.datatype import AttribDict
42
from lib.core.defaults import _defaults
43
from lib.core.dicts import PART_RUN_CONTENT_TYPES
44
from lib.core.enums import AUTOCOMPLETE_TYPE
45
from lib.core.enums import CONTENT_STATUS
46
from lib.core.enums import MKSTEMP_PREFIX
47
from lib.core.exception import SqlmapConnectionException
48
from lib.core.log import LOGGER_HANDLER
49
from lib.core.optiondict import optDict
50
from lib.core.settings import IS_WIN
51
from lib.core.settings import RESTAPI_DEFAULT_ADAPTER
52
from lib.core.settings import RESTAPI_DEFAULT_ADDRESS
53
from lib.core.settings import RESTAPI_DEFAULT_PORT
54
from lib.core.settings import RESTAPI_UNSUPPORTED_OPTIONS
55
from lib.core.settings import VERSION_STRING
56
from lib.core.shell import autoCompletion
57
from lib.core.subprocessng import Popen
58
from lib.parse.cmdline import cmdLineParser
59
from thirdparty.bottle.bottle import error as return_error
60
from thirdparty.bottle.bottle import get
61
from thirdparty.bottle.bottle import hook
62
from thirdparty.bottle.bottle import post
63
from thirdparty.bottle.bottle import request
64
from thirdparty.bottle.bottle import response
65
from thirdparty.bottle.bottle import run
66
from thirdparty.bottle.bottle import server_names
67
from thirdparty import six
68
from thirdparty.six.moves import http_client as _http_client
69
from thirdparty.six.moves import input as _input
70
from thirdparty.six.moves import urllib as _urllib
71
72
# Global data storage
73
class DataStore(object):
74
admin_token = ""
75
current_db = None
76
tasks = dict()
77
username = None
78
password = None
79
80
# API objects
81
class Database(object):
82
filepath = None
83
84
def __init__(self, database=None):
85
self.database = self.filepath if database is None else database
86
self.connection = None
87
self.cursor = None
88
89
def connect(self, who="server"):
90
self.connection = sqlite3.connect(self.database, timeout=3, isolation_level=None, check_same_thread=False)
91
self.cursor = self.connection.cursor()
92
self.lock = threading.Lock()
93
logger.debug("REST-JSON API %s connected to IPC database" % who)
94
95
def disconnect(self):
96
if self.cursor:
97
self.cursor.close()
98
99
if self.connection:
100
self.connection.close()
101
102
def commit(self):
103
self.connection.commit()
104
105
def execute(self, statement, arguments=None):
106
with self.lock:
107
while True:
108
try:
109
if arguments:
110
self.cursor.execute(statement, arguments)
111
else:
112
self.cursor.execute(statement)
113
except sqlite3.OperationalError as ex:
114
if "locked" not in getSafeExString(ex):
115
raise
116
else:
117
time.sleep(1)
118
else:
119
break
120
121
if statement.lstrip().upper().startswith("SELECT"):
122
return self.cursor.fetchall()
123
124
def init(self):
125
self.execute("CREATE TABLE IF NOT EXISTS logs(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, time TEXT, level TEXT, message TEXT)")
126
self.execute("CREATE TABLE IF NOT EXISTS data(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, status INTEGER, content_type INTEGER, value TEXT)")
127
self.execute("CREATE TABLE IF NOT EXISTS errors(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, error TEXT)")
128
129
class Task(object):
130
def __init__(self, taskid, remote_addr):
131
self.remote_addr = remote_addr
132
self.process = None
133
self.output_directory = None
134
self.options = None
135
self._original_options = None
136
self.initialize_options(taskid)
137
138
def initialize_options(self, taskid):
139
datatype = {"boolean": False, "string": None, "integer": None, "float": None}
140
self.options = AttribDict()
141
142
for _ in optDict:
143
for name, type_ in optDict[_].items():
144
type_ = unArrayizeValue(type_)
145
self.options[name] = _defaults.get(name, datatype[type_])
146
147
# Let sqlmap engine knows it is getting called by the API,
148
# the task ID and the file path of the IPC database
149
self.options.api = True
150
self.options.taskid = taskid
151
self.options.database = Database.filepath
152
153
# Enforce batch mode and disable coloring and ETA
154
self.options.batch = True
155
self.options.disableColoring = True
156
self.options.eta = False
157
158
self._original_options = AttribDict(self.options)
159
160
def set_option(self, option, value):
161
self.options[option] = value
162
163
def get_option(self, option):
164
return self.options[option]
165
166
def get_options(self):
167
return self.options
168
169
def reset_options(self):
170
self.options = AttribDict(self._original_options)
171
172
def engine_start(self):
173
handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True)
174
os.close(handle)
175
saveConfig(self.options, configFile)
176
177
if os.path.exists("sqlmap.py"):
178
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, close_fds=not IS_WIN)
179
elif os.path.exists(os.path.join(os.getcwd(), "sqlmap.py")):
180
self.process = Popen([sys.executable or "python", "sqlmap.py", "--api", "-c", configFile], shell=False, cwd=os.getcwd(), close_fds=not IS_WIN)
181
elif os.path.exists(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), "sqlmap.py")):
182
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)
183
else:
184
self.process = Popen(["sqlmap", "--api", "-c", configFile], shell=False, close_fds=not IS_WIN)
185
186
def engine_stop(self):
187
if self.process:
188
self.process.terminate()
189
return self.process.wait()
190
else:
191
return None
192
193
def engine_process(self):
194
return self.process
195
196
def engine_kill(self):
197
if self.process:
198
try:
199
self.process.kill()
200
return self.process.wait()
201
except:
202
pass
203
return None
204
205
def engine_get_id(self):
206
if self.process:
207
return self.process.pid
208
else:
209
return None
210
211
def engine_get_returncode(self):
212
if self.process:
213
self.process.poll()
214
return self.process.returncode
215
else:
216
return None
217
218
def engine_has_terminated(self):
219
return isinstance(self.engine_get_returncode(), int)
220
221
# Wrapper functions for sqlmap engine
222
class StdDbOut(object):
223
def __init__(self, taskid, messagetype="stdout"):
224
# Overwrite system standard output and standard error to write
225
# to an IPC database
226
self.messagetype = messagetype
227
self.taskid = taskid
228
229
if self.messagetype == "stdout":
230
sys.stdout = self
231
else:
232
sys.stderr = self
233
234
def write(self, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None):
235
if self.messagetype == "stdout":
236
if content_type is None:
237
if kb.partRun is not None:
238
content_type = PART_RUN_CONTENT_TYPES.get(kb.partRun)
239
else:
240
# Ignore all non-relevant messages
241
return
242
243
output = conf.databaseCursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", (self.taskid, content_type))
244
245
# Delete partial output from IPC database if we have got a complete output
246
if status == CONTENT_STATUS.COMPLETE:
247
if len(output) > 0:
248
for index in xrange(len(output)):
249
conf.databaseCursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],))
250
251
conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value)))
252
if kb.partRun:
253
kb.partRun = None
254
255
elif status == CONTENT_STATUS.IN_PROGRESS:
256
if len(output) == 0:
257
conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value)))
258
else:
259
new_value = "%s%s" % (dejsonize(output[0][2]), value)
260
conf.databaseCursor.execute("UPDATE data SET value = ? WHERE id = ?", (jsonize(new_value), output[0][0]))
261
else:
262
conf.databaseCursor.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (self.taskid, str(value) if value else ""))
263
264
def flush(self):
265
pass
266
267
def close(self):
268
pass
269
270
def seek(self):
271
pass
272
273
class LogRecorder(logging.StreamHandler):
274
def emit(self, record):
275
"""
276
Record emitted events to IPC database for asynchronous I/O
277
communication with the parent process
278
"""
279
conf.databaseCursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, str(record.msg % record.args if record.args else record.msg)))
280
281
def setRestAPILog():
282
if conf.api:
283
try:
284
conf.databaseCursor = Database(conf.database)
285
conf.databaseCursor.connect("client")
286
except sqlite3.OperationalError as ex:
287
raise SqlmapConnectionException("%s ('%s')" % (ex, conf.database))
288
289
# Set a logging handler that writes log messages to a IPC database
290
logger.removeHandler(LOGGER_HANDLER)
291
LOGGER_RECORDER = LogRecorder()
292
logger.addHandler(LOGGER_RECORDER)
293
294
# Generic functions
295
def is_admin(token):
296
return DataStore.admin_token == token
297
298
@hook('before_request')
299
def check_authentication():
300
if not any((DataStore.username, DataStore.password)):
301
return
302
303
authorization = request.headers.get("Authorization", "")
304
match = re.search(r"(?i)\ABasic\s+([^\s]+)", authorization)
305
306
if not match:
307
request.environ["PATH_INFO"] = "/error/401"
308
309
try:
310
creds = decodeBase64(match.group(1), binary=False)
311
except:
312
request.environ["PATH_INFO"] = "/error/401"
313
else:
314
if creds.count(':') != 1:
315
request.environ["PATH_INFO"] = "/error/401"
316
else:
317
username, password = creds.split(':')
318
if username.strip() != (DataStore.username or "") or password.strip() != (DataStore.password or ""):
319
request.environ["PATH_INFO"] = "/error/401"
320
321
@hook("after_request")
322
def security_headers(json_header=True):
323
"""
324
Set some headers across all HTTP responses
325
"""
326
response.headers["Server"] = "Server"
327
response.headers["X-Content-Type-Options"] = "nosniff"
328
response.headers["X-Frame-Options"] = "DENY"
329
response.headers["X-XSS-Protection"] = "1; mode=block"
330
response.headers["Pragma"] = "no-cache"
331
response.headers["Cache-Control"] = "no-cache"
332
response.headers["Expires"] = "0"
333
334
if json_header:
335
response.content_type = "application/json; charset=UTF-8"
336
337
##############################
338
# HTTP Status Code functions #
339
##############################
340
341
@return_error(401) # Access Denied
342
def error401(error=None):
343
security_headers(False)
344
return "Access denied"
345
346
@return_error(404) # Not Found
347
def error404(error=None):
348
security_headers(False)
349
return "Nothing here"
350
351
@return_error(405) # Method Not Allowed (e.g. when requesting a POST method via GET)
352
def error405(error=None):
353
security_headers(False)
354
return "Method not allowed"
355
356
@return_error(500) # Internal Server Error
357
def error500(error=None):
358
security_headers(False)
359
return "Internal server error"
360
361
#############
362
# Auxiliary #
363
#############
364
365
@get('/error/401')
366
def path_401():
367
response.status = 401
368
return response
369
370
#############################
371
# Task management functions #
372
#############################
373
374
# Users' methods
375
@get("/task/new")
376
def task_new():
377
"""
378
Create a new task
379
"""
380
taskid = encodeHex(os.urandom(8), binary=False)
381
remote_addr = request.remote_addr
382
383
DataStore.tasks[taskid] = Task(taskid, remote_addr)
384
385
logger.debug("Created new task: '%s'" % taskid)
386
return jsonize({"success": True, "taskid": taskid})
387
388
@get("/task/<taskid>/delete")
389
def task_delete(taskid):
390
"""
391
Delete an existing task
392
"""
393
if taskid in DataStore.tasks:
394
DataStore.tasks.pop(taskid)
395
396
logger.debug("(%s) Deleted task" % taskid)
397
return jsonize({"success": True})
398
else:
399
response.status = 404
400
logger.warning("[%s] Non-existing task ID provided to task_delete()" % taskid)
401
return jsonize({"success": False, "message": "Non-existing task ID"})
402
403
###################
404
# Admin functions #
405
###################
406
407
@get("/admin/list")
408
@get("/admin/<token>/list")
409
def task_list(token=None):
410
"""
411
Pull task list
412
"""
413
tasks = {}
414
415
for key in DataStore.tasks:
416
if is_admin(token) or DataStore.tasks[key].remote_addr == request.remote_addr:
417
tasks[key] = dejsonize(scan_status(key))["status"]
418
419
logger.debug("(%s) Listed task pool (%s)" % (token, "admin" if is_admin(token) else request.remote_addr))
420
return jsonize({"success": True, "tasks": tasks, "tasks_num": len(tasks)})
421
422
@get("/admin/flush")
423
@get("/admin/<token>/flush")
424
def task_flush(token=None):
425
"""
426
Flush task spool (delete all tasks)
427
"""
428
429
for key in list(DataStore.tasks):
430
if is_admin(token) or DataStore.tasks[key].remote_addr == request.remote_addr:
431
DataStore.tasks[key].engine_kill()
432
del DataStore.tasks[key]
433
434
logger.debug("(%s) Flushed task pool (%s)" % (token, "admin" if is_admin(token) else request.remote_addr))
435
return jsonize({"success": True})
436
437
##################################
438
# sqlmap core interact functions #
439
##################################
440
441
# Handle task's options
442
@get("/option/<taskid>/list")
443
def option_list(taskid):
444
"""
445
List options for a certain task ID
446
"""
447
if taskid not in DataStore.tasks:
448
logger.warning("[%s] Invalid task ID provided to option_list()" % taskid)
449
return jsonize({"success": False, "message": "Invalid task ID"})
450
451
logger.debug("(%s) Listed task options" % taskid)
452
return jsonize({"success": True, "options": DataStore.tasks[taskid].get_options()})
453
454
@post("/option/<taskid>/get")
455
def option_get(taskid):
456
"""
457
Get value of option(s) for a certain task ID
458
"""
459
if taskid not in DataStore.tasks:
460
logger.warning("[%s] Invalid task ID provided to option_get()" % taskid)
461
return jsonize({"success": False, "message": "Invalid task ID"})
462
463
options = request.json or []
464
results = {}
465
466
for option in options:
467
if option in DataStore.tasks[taskid].options:
468
results[option] = DataStore.tasks[taskid].options[option]
469
else:
470
logger.debug("(%s) Requested value for unknown option '%s'" % (taskid, option))
471
return jsonize({"success": False, "message": "Unknown option '%s'" % option})
472
473
logger.debug("(%s) Retrieved values for option(s) '%s'" % (taskid, ','.join(options)))
474
475
return jsonize({"success": True, "options": results})
476
477
@post("/option/<taskid>/set")
478
def option_set(taskid):
479
"""
480
Set value of option(s) for a certain task ID
481
"""
482
483
if taskid not in DataStore.tasks:
484
logger.warning("[%s] Invalid task ID provided to option_set()" % taskid)
485
return jsonize({"success": False, "message": "Invalid task ID"})
486
487
if request.json is None:
488
logger.warning("[%s] Invalid JSON options provided to option_set()" % taskid)
489
return jsonize({"success": False, "message": "Invalid JSON options"})
490
491
for option, value in request.json.items():
492
DataStore.tasks[taskid].set_option(option, value)
493
494
logger.debug("(%s) Requested to set options" % taskid)
495
return jsonize({"success": True})
496
497
# Handle scans
498
@post("/scan/<taskid>/start")
499
def scan_start(taskid):
500
"""
501
Launch a scan
502
"""
503
504
if taskid not in DataStore.tasks:
505
logger.warning("[%s] Invalid task ID provided to scan_start()" % taskid)
506
return jsonize({"success": False, "message": "Invalid task ID"})
507
508
if request.json is None:
509
logger.warning("[%s] Invalid JSON options provided to scan_start()" % taskid)
510
return jsonize({"success": False, "message": "Invalid JSON options"})
511
512
for key in request.json:
513
if key in RESTAPI_UNSUPPORTED_OPTIONS:
514
logger.warning("[%s] Unsupported option '%s' provided to scan_start()" % (taskid, key))
515
return jsonize({"success": False, "message": "Unsupported option '%s'" % key})
516
517
# Initialize sqlmap engine's options with user's provided options, if any
518
for option, value in request.json.items():
519
DataStore.tasks[taskid].set_option(option, value)
520
521
# Launch sqlmap engine in a separate process
522
DataStore.tasks[taskid].engine_start()
523
524
logger.debug("(%s) Started scan" % taskid)
525
return jsonize({"success": True, "engineid": DataStore.tasks[taskid].engine_get_id()})
526
527
@get("/scan/<taskid>/stop")
528
def scan_stop(taskid):
529
"""
530
Stop a scan
531
"""
532
533
if (taskid not in DataStore.tasks or DataStore.tasks[taskid].engine_process() is None or DataStore.tasks[taskid].engine_has_terminated()):
534
logger.warning("[%s] Invalid task ID provided to scan_stop()" % taskid)
535
return jsonize({"success": False, "message": "Invalid task ID"})
536
537
DataStore.tasks[taskid].engine_stop()
538
539
logger.debug("(%s) Stopped scan" % taskid)
540
return jsonize({"success": True})
541
542
@get("/scan/<taskid>/kill")
543
def scan_kill(taskid):
544
"""
545
Kill a scan
546
"""
547
548
if (taskid not in DataStore.tasks or DataStore.tasks[taskid].engine_process() is None or DataStore.tasks[taskid].engine_has_terminated()):
549
logger.warning("[%s] Invalid task ID provided to scan_kill()" % taskid)
550
return jsonize({"success": False, "message": "Invalid task ID"})
551
552
DataStore.tasks[taskid].engine_kill()
553
554
logger.debug("(%s) Killed scan" % taskid)
555
return jsonize({"success": True})
556
557
@get("/scan/<taskid>/status")
558
def scan_status(taskid):
559
"""
560
Returns status of a scan
561
"""
562
563
if taskid not in DataStore.tasks:
564
logger.warning("[%s] Invalid task ID provided to scan_status()" % taskid)
565
return jsonize({"success": False, "message": "Invalid task ID"})
566
567
if DataStore.tasks[taskid].engine_process() is None:
568
status = "not running"
569
else:
570
status = "terminated" if DataStore.tasks[taskid].engine_has_terminated() is True else "running"
571
572
logger.debug("(%s) Retrieved scan status" % taskid)
573
return jsonize({
574
"success": True,
575
"status": status,
576
"returncode": DataStore.tasks[taskid].engine_get_returncode()
577
})
578
579
@get("/scan/<taskid>/data")
580
def scan_data(taskid):
581
"""
582
Retrieve the data of a scan
583
"""
584
585
json_data_message = list()
586
json_errors_message = list()
587
588
if taskid not in DataStore.tasks:
589
logger.warning("[%s] Invalid task ID provided to scan_data()" % taskid)
590
return jsonize({"success": False, "message": "Invalid task ID"})
591
592
# Read all data from the IPC database for the taskid
593
for status, content_type, value in DataStore.current_db.execute("SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)):
594
json_data_message.append({"status": status, "type": content_type, "value": dejsonize(value)})
595
596
# Read all error messages from the IPC database
597
for error in DataStore.current_db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)):
598
json_errors_message.append(error)
599
600
logger.debug("(%s) Retrieved scan data and error messages" % taskid)
601
return jsonize({"success": True, "data": json_data_message, "error": json_errors_message})
602
603
# Functions to handle scans' logs
604
@get("/scan/<taskid>/log/<start>/<end>")
605
def scan_log_limited(taskid, start, end):
606
"""
607
Retrieve a subset of log messages
608
"""
609
610
json_log_messages = list()
611
612
if taskid not in DataStore.tasks:
613
logger.warning("[%s] Invalid task ID provided to scan_log_limited()" % taskid)
614
return jsonize({"success": False, "message": "Invalid task ID"})
615
616
if not start.isdigit() or not end.isdigit() or int(end) < int(start):
617
logger.warning("[%s] Invalid start or end value provided to scan_log_limited()" % taskid)
618
return jsonize({"success": False, "message": "Invalid start or end value, must be digits"})
619
620
start = max(1, int(start))
621
end = max(1, int(end))
622
623
# Read a subset of log messages from the IPC database
624
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)):
625
json_log_messages.append({"time": time_, "level": level, "message": message})
626
627
logger.debug("(%s) Retrieved scan log messages subset" % taskid)
628
return jsonize({"success": True, "log": json_log_messages})
629
630
@get("/scan/<taskid>/log")
631
def scan_log(taskid):
632
"""
633
Retrieve the log messages
634
"""
635
636
json_log_messages = list()
637
638
if taskid not in DataStore.tasks:
639
logger.warning("[%s] Invalid task ID provided to scan_log()" % taskid)
640
return jsonize({"success": False, "message": "Invalid task ID"})
641
642
# Read all log messages from the IPC database
643
for time_, level, message in DataStore.current_db.execute("SELECT time, level, message FROM logs WHERE taskid = ? ORDER BY id ASC", (taskid,)):
644
json_log_messages.append({"time": time_, "level": level, "message": message})
645
646
logger.debug("(%s) Retrieved scan log messages" % taskid)
647
return jsonize({"success": True, "log": json_log_messages})
648
649
# Function to handle files inside the output directory
650
@get("/download/<taskid>/<target>/<filename:path>")
651
def download(taskid, target, filename):
652
"""
653
Download a certain file from the file system
654
"""
655
656
if taskid not in DataStore.tasks:
657
logger.warning("[%s] Invalid task ID provided to download()" % taskid)
658
return jsonize({"success": False, "message": "Invalid task ID"})
659
660
path = os.path.abspath(os.path.join(paths.SQLMAP_OUTPUT_PATH, target, filename))
661
# Prevent file path traversal
662
if not path.startswith(paths.SQLMAP_OUTPUT_PATH):
663
logger.warning("[%s] Forbidden path (%s)" % (taskid, target))
664
return jsonize({"success": False, "message": "Forbidden path"})
665
666
if os.path.isfile(path):
667
logger.debug("(%s) Retrieved content of file %s" % (taskid, target))
668
content = openFile(path, "rb").read()
669
return jsonize({"success": True, "file": encodeBase64(content, binary=False)})
670
else:
671
logger.warning("[%s] File does not exist %s" % (taskid, target))
672
return jsonize({"success": False, "message": "File does not exist"})
673
674
@get("/version")
675
def version(token=None):
676
"""
677
Fetch server version
678
"""
679
680
logger.debug("Fetched version (%s)" % ("admin" if is_admin(token) else request.remote_addr))
681
return jsonize({"success": True, "version": VERSION_STRING.split('/')[-1]})
682
683
def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None, database=None):
684
"""
685
REST-JSON API server
686
"""
687
688
DataStore.admin_token = encodeHex(os.urandom(16), binary=False)
689
DataStore.username = username
690
DataStore.password = password
691
692
if not database:
693
_, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False)
694
os.close(_)
695
else:
696
Database.filepath = database
697
698
if port == 0: # random
699
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
700
s.bind((host, 0))
701
port = s.getsockname()[1]
702
703
logger.info("Running REST-JSON API server at '%s:%d'.." % (host, port))
704
logger.info("Admin (secret) token: %s" % DataStore.admin_token)
705
logger.debug("IPC database: '%s'" % Database.filepath)
706
707
# Initialize IPC database
708
DataStore.current_db = Database()
709
DataStore.current_db.connect()
710
DataStore.current_db.init()
711
712
# Run RESTful API
713
try:
714
# Supported adapters: aiohttp, auto, bjoern, cgi, cherrypy, diesel, eventlet, fapws3, flup, gae, gevent, geventSocketIO, gunicorn, meinheld, paste, rocket, tornado, twisted, waitress, wsgiref
715
# Reference: https://bottlepy.org/docs/dev/deployment.html || bottle.server_names
716
717
if adapter == "gevent":
718
from gevent import monkey
719
monkey.patch_all()
720
elif adapter == "eventlet":
721
import eventlet
722
eventlet.monkey_patch()
723
logger.debug("Using adapter '%s' to run bottle" % adapter)
724
run(host=host, port=port, quiet=True, debug=True, server=adapter)
725
except socket.error as ex:
726
if "already in use" in getSafeExString(ex):
727
logger.error("Address already in use ('%s:%s')" % (host, port))
728
else:
729
raise
730
except ImportError:
731
if adapter.lower() not in server_names:
732
errMsg = "Adapter '%s' is unknown. " % adapter
733
errMsg += "List of supported adapters: %s" % ', '.join(sorted(list(server_names.keys())))
734
else:
735
errMsg = "Server support for adapter '%s' is not installed on this system " % adapter
736
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)
737
logger.critical(errMsg)
738
739
def _client(url, options=None):
740
logger.debug("Calling '%s'" % url)
741
try:
742
headers = {"Content-Type": "application/json"}
743
744
if options is not None:
745
data = getBytes(jsonize(options))
746
else:
747
data = None
748
749
if DataStore.username or DataStore.password:
750
headers["Authorization"] = "Basic %s" % encodeBase64("%s:%s" % (DataStore.username or "", DataStore.password or ""), binary=False)
751
752
req = _urllib.request.Request(url, data, headers)
753
response = _urllib.request.urlopen(req)
754
text = getText(response.read())
755
except:
756
if options:
757
logger.error("Failed to load and parse %s" % url)
758
raise
759
return text
760
761
def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=None, password=None):
762
"""
763
REST-JSON API client
764
"""
765
766
DataStore.username = username
767
DataStore.password = password
768
769
dbgMsg = "Example client access from command line:"
770
dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (host, port)
771
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)
772
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port)
773
dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port)
774
logger.debug(dbgMsg)
775
776
addr = "http://%s:%d" % (host, port)
777
logger.info("Starting REST-JSON API client to '%s'..." % addr)
778
779
try:
780
_client(addr)
781
except Exception as ex:
782
if not isinstance(ex, _urllib.error.HTTPError) or ex.code == _http_client.UNAUTHORIZED:
783
errMsg = "There has been a problem while connecting to the "
784
errMsg += "REST-JSON API server at '%s' " % addr
785
errMsg += "(%s)" % getSafeExString(ex)
786
logger.critical(errMsg)
787
return
788
789
commands = ("help", "new", "use", "data", "log", "status", "option", "stop", "kill", "list", "flush", "version", "exit", "bye", "quit")
790
colors = ('red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightgrey', 'lightred', 'lightgreen', 'lightyellow', 'lightblue', 'lightmagenta', 'lightcyan')
791
autoCompletion(AUTOCOMPLETE_TYPE.API, commands=commands)
792
793
taskid = None
794
logger.info("Type 'help' or '?' for list of available commands")
795
796
while True:
797
try:
798
color = colors[int(taskid or "0", 16) % len(colors)]
799
command = _input("api%s> " % (" (%s)" % setColor(taskid, color) if taskid else "")).strip()
800
command = re.sub(r"\A(\w+)", lambda match: match.group(1).lower(), command)
801
except (EOFError, KeyboardInterrupt):
802
print()
803
break
804
805
if command in ("data", "log", "status", "stop", "kill"):
806
if not taskid:
807
logger.error("No task ID in use")
808
continue
809
raw = _client("%s/scan/%s/%s" % (addr, taskid, command))
810
res = dejsonize(raw)
811
if not res["success"]:
812
logger.error("Failed to execute command %s" % command)
813
dataToStdout("%s\n" % raw)
814
815
elif command.startswith("option"):
816
if not taskid:
817
logger.error("No task ID in use")
818
continue
819
try:
820
command, option = command.split(" ", 1)
821
except ValueError:
822
raw = _client("%s/option/%s/list" % (addr, taskid))
823
else:
824
options = re.split(r"\s*,\s*", option.strip())
825
raw = _client("%s/option/%s/get" % (addr, taskid), options)
826
res = dejsonize(raw)
827
if not res["success"]:
828
logger.error("Failed to execute command %s" % command)
829
dataToStdout("%s\n" % raw)
830
831
elif command.startswith("new"):
832
if ' ' not in command:
833
logger.error("Program arguments are missing")
834
continue
835
836
try:
837
argv = ["sqlmap.py"] + shlex.split(command)[1:]
838
except Exception as ex:
839
logger.error("Error occurred while parsing arguments ('%s')" % getSafeExString(ex))
840
taskid = None
841
continue
842
843
try:
844
cmdLineOptions = cmdLineParser(argv).__dict__
845
except:
846
taskid = None
847
continue
848
849
for key in list(cmdLineOptions):
850
if cmdLineOptions[key] is None:
851
del cmdLineOptions[key]
852
853
raw = _client("%s/task/new" % addr)
854
res = dejsonize(raw)
855
if not res["success"]:
856
logger.error("Failed to create new task ('%s')" % res.get("message", ""))
857
continue
858
taskid = res["taskid"]
859
logger.info("New task ID is '%s'" % taskid)
860
861
raw = _client("%s/scan/%s/start" % (addr, taskid), cmdLineOptions)
862
res = dejsonize(raw)
863
if not res["success"]:
864
logger.error("Failed to start scan ('%s')" % res.get("message", ""))
865
continue
866
logger.info("Scanning started")
867
868
elif command.startswith("use"):
869
taskid = (command.split()[1] if ' ' in command else "").strip("'\"")
870
if not taskid:
871
logger.error("Task ID is missing")
872
taskid = None
873
continue
874
elif not re.search(r"\A[0-9a-fA-F]{16}\Z", taskid):
875
logger.error("Invalid task ID '%s'" % taskid)
876
taskid = None
877
continue
878
logger.info("Switching to task ID '%s' " % taskid)
879
880
elif command in ("version",):
881
raw = _client("%s/%s" % (addr, command))
882
res = dejsonize(raw)
883
if not res["success"]:
884
logger.error("Failed to execute command %s" % command)
885
dataToStdout("%s\n" % raw)
886
887
elif command in ("list", "flush"):
888
raw = _client("%s/admin/%s" % (addr, command))
889
res = dejsonize(raw)
890
if not res["success"]:
891
logger.error("Failed to execute command %s" % command)
892
elif command == "flush":
893
taskid = None
894
dataToStdout("%s\n" % raw)
895
896
elif command in ("exit", "bye", "quit", 'q'):
897
return
898
899
elif command in ("help", "?"):
900
msg = "help Show this help message\n"
901
msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"http://testphp.vulnweb.com/artists.php?artist=1\"')\n"
902
msg += "use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4')\n"
903
msg += "data Retrieve and show data for current task\n"
904
msg += "log Retrieve and show log for current task\n"
905
msg += "status Retrieve and show status for current task\n"
906
msg += "option OPTION Retrieve and show option for current task\n"
907
msg += "options Retrieve and show all options for current task\n"
908
msg += "stop Stop current task\n"
909
msg += "kill Kill current task\n"
910
msg += "list Display all tasks\n"
911
msg += "version Fetch server version\n"
912
msg += "flush Flush tasks (delete all tasks)\n"
913
msg += "exit Exit this client\n"
914
915
dataToStdout(msg)
916
917
elif command:
918
logger.error("Unknown command '%s'" % command)
919
920