Skip to content

Commit fac6712

Browse files
committed
Implements sqlmapproject#2647 (Basic authorization for sqlmapapi)
1 parent 68ee1f3 commit fac6712

File tree

4 files changed

+73
-34
lines changed

4 files changed

+73
-34
lines changed

lib/core/settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from lib.core.enums import OS
2020

2121
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
22-
VERSION = "1.1.7.31"
22+
VERSION = "1.1.8.0"
2323
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2424
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2525
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

lib/utils/api.py

+65-28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import contextlib
10+
import httplib
1011
import logging
1112
import os
1213
import re
@@ -43,6 +44,7 @@
4344
from lib.core.settings import RESTAPI_DEFAULT_PORT
4445
from lib.core.subprocessng import Popen
4546
from lib.parse.cmdline import cmdLineParser
47+
from thirdparty.bottle.bottle import abort
4648
from thirdparty.bottle.bottle import error as return_error
4749
from thirdparty.bottle.bottle import get
4850
from thirdparty.bottle.bottle import hook
@@ -52,13 +54,13 @@
5254
from thirdparty.bottle.bottle import run
5355
from thirdparty.bottle.bottle import server_names
5456

55-
56-
# global settings
57+
# Global data storage
5758
class DataStore(object):
5859
admin_id = ""
5960
current_db = None
6061
tasks = dict()
61-
62+
username = None
63+
password = None
6264

6365
# API objects
6466
class Database(object):
@@ -118,7 +120,6 @@ def init(self):
118120
"taskid INTEGER, error TEXT"
119121
")")
120122

121-
122123
class Task(object):
123124
def __init__(self, taskid, remote_addr):
124125
self.remote_addr = remote_addr
@@ -283,11 +284,32 @@ def setRestAPILog():
283284
LOGGER_RECORDER = LogRecorder()
284285
logger.addHandler(LOGGER_RECORDER)
285286

286-
287287
# Generic functions
288288
def is_admin(taskid):
289289
return DataStore.admin_id == taskid
290290

291+
@hook('before_request')
292+
def check_authentication():
293+
if not any((DataStore.username, DataStore.password)):
294+
return
295+
296+
authorization = request.headers.get("Authorization", "")
297+
match = re.search(r"(?i)\ABasic\s+([^\s]+)", authorization)
298+
299+
if not match:
300+
request.environ["PATH_INFO"] = "/error/401"
301+
302+
try:
303+
creds = match.group(1).decode("base64")
304+
except:
305+
request.environ["PATH_INFO"] = "/error/401"
306+
else:
307+
if creds.count(':') != 1:
308+
request.environ["PATH_INFO"] = "/error/401"
309+
else:
310+
username, password = creds.split(':')
311+
if username.strip() != (DataStore.username or "") or password.strip() != (DataStore.password or ""):
312+
request.environ["PATH_INFO"] = "/error/401"
291313

292314
@hook("after_request")
293315
def security_headers(json_header=True):
@@ -301,42 +323,47 @@ def security_headers(json_header=True):
301323
response.headers["Pragma"] = "no-cache"
302324
response.headers["Cache-Control"] = "no-cache"
303325
response.headers["Expires"] = "0"
326+
304327
if json_header:
305328
response.content_type = "application/json; charset=UTF-8"
306329

307330
##############################
308331
# HTTP Status Code functions #
309332
##############################
310333

311-
312334
@return_error(401) # Access Denied
313335
def error401(error=None):
314336
security_headers(False)
315337
return "Access denied"
316338

317-
318339
@return_error(404) # Not Found
319340
def error404(error=None):
320341
security_headers(False)
321342
return "Nothing here"
322343

323-
324344
@return_error(405) # Method Not Allowed (e.g. when requesting a POST method via GET)
325345
def error405(error=None):
326346
security_headers(False)
327347
return "Method not allowed"
328348

329-
330349
@return_error(500) # Internal Server Error
331350
def error500(error=None):
332351
security_headers(False)
333352
return "Internal server error"
334353

354+
#############
355+
# Auxiliary #
356+
#############
357+
358+
@get('/error/401')
359+
def path_401():
360+
response.status = 401
361+
return response
362+
335363
#############################
336364
# Task management functions #
337365
#############################
338366

339-
340367
# Users' methods
341368
@get("/task/new")
342369
def task_new():
@@ -351,7 +378,6 @@ def task_new():
351378
logger.debug("Created new task: '%s'" % taskid)
352379
return jsonize({"success": True, "taskid": taskid})
353380

354-
355381
@get("/task/<taskid>/delete")
356382
def task_delete(taskid):
357383
"""
@@ -370,7 +396,6 @@ def task_delete(taskid):
370396
# Admin functions #
371397
###################
372398

373-
374399
@get("/admin/<taskid>/list")
375400
def task_list(taskid=None):
376401
"""
@@ -403,7 +428,6 @@ def task_flush(taskid):
403428
# sqlmap core interact functions #
404429
##################################
405430

406-
407431
# Handle task's options
408432
@get("/option/<taskid>/list")
409433
def option_list(taskid):
@@ -417,7 +441,6 @@ def option_list(taskid):
417441
logger.debug("[%s] Listed task options" % taskid)
418442
return jsonize({"success": True, "options": DataStore.tasks[taskid].get_options()})
419443

420-
421444
@post("/option/<taskid>/get")
422445
def option_get(taskid):
423446
"""
@@ -436,12 +459,12 @@ def option_get(taskid):
436459
logger.debug("[%s] Requested value for unknown option %s" % (taskid, option))
437460
return jsonize({"success": False, "message": "Unknown option", option: "not set"})
438461

439-
440462
@post("/option/<taskid>/set")
441463
def option_set(taskid):
442464
"""
443465
Set an option (command line switch) for a certain task ID
444466
"""
467+
445468
if taskid not in DataStore.tasks:
446469
logger.warning("[%s] Invalid task ID provided to option_set()" % taskid)
447470
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -452,13 +475,13 @@ def option_set(taskid):
452475
logger.debug("[%s] Requested to set options" % taskid)
453476
return jsonize({"success": True})
454477

455-
456478
# Handle scans
457479
@post("/scan/<taskid>/start")
458480
def scan_start(taskid):
459481
"""
460482
Launch a scan
461483
"""
484+
462485
if taskid not in DataStore.tasks:
463486
logger.warning("[%s] Invalid task ID provided to scan_start()" % taskid)
464487
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -473,12 +496,12 @@ def scan_start(taskid):
473496
logger.debug("[%s] Started scan" % taskid)
474497
return jsonize({"success": True, "engineid": DataStore.tasks[taskid].engine_get_id()})
475498

476-
477499
@get("/scan/<taskid>/stop")
478500
def scan_stop(taskid):
479501
"""
480502
Stop a scan
481503
"""
504+
482505
if (taskid not in DataStore.tasks or
483506
DataStore.tasks[taskid].engine_process() is None or
484507
DataStore.tasks[taskid].engine_has_terminated()):
@@ -490,12 +513,12 @@ def scan_stop(taskid):
490513
logger.debug("[%s] Stopped scan" % taskid)
491514
return jsonize({"success": True})
492515

493-
494516
@get("/scan/<taskid>/kill")
495517
def scan_kill(taskid):
496518
"""
497519
Kill a scan
498520
"""
521+
499522
if (taskid not in DataStore.tasks or
500523
DataStore.tasks[taskid].engine_process() is None or
501524
DataStore.tasks[taskid].engine_has_terminated()):
@@ -507,12 +530,12 @@ def scan_kill(taskid):
507530
logger.debug("[%s] Killed scan" % taskid)
508531
return jsonize({"success": True})
509532

510-
511533
@get("/scan/<taskid>/status")
512534
def scan_status(taskid):
513535
"""
514536
Returns status of a scan
515537
"""
538+
516539
if taskid not in DataStore.tasks:
517540
logger.warning("[%s] Invalid task ID provided to scan_status()" % taskid)
518541
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -529,12 +552,12 @@ def scan_status(taskid):
529552
"returncode": DataStore.tasks[taskid].engine_get_returncode()
530553
})
531554

532-
533555
@get("/scan/<taskid>/data")
534556
def scan_data(taskid):
535557
"""
536558
Retrieve the data of a scan
537559
"""
560+
538561
json_data_message = list()
539562
json_errors_message = list()
540563

@@ -560,6 +583,7 @@ def scan_log_limited(taskid, start, end):
560583
"""
561584
Retrieve a subset of log messages
562585
"""
586+
563587
json_log_messages = list()
564588

565589
if taskid not in DataStore.tasks:
@@ -586,6 +610,7 @@ def scan_log(taskid):
586610
"""
587611
Retrieve the log messages
588612
"""
613+
589614
json_log_messages = list()
590615

591616
if taskid not in DataStore.tasks:
@@ -606,6 +631,7 @@ def download(taskid, target, filename):
606631
"""
607632
Download a certain file from the file system
608633
"""
634+
609635
if taskid not in DataStore.tasks:
610636
logger.warning("[%s] Invalid task ID provided to download()" % taskid)
611637
return jsonize({"success": False, "message": "Invalid task ID"})
@@ -626,13 +652,17 @@ def download(taskid, target, filename):
626652
return jsonize({"success": False, "message": "File does not exist"})
627653

628654

629-
def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER):
655+
def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None):
630656
"""
631657
REST-JSON API server
632658
"""
659+
633660
DataStore.admin_id = hexencode(os.urandom(16))
634-
handle, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False)
635-
os.close(handle)
661+
DataStore.username = username
662+
DataStore.password = password
663+
664+
_, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False)
665+
os.close(_)
636666

637667
if port == 0: # random
638668
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
@@ -660,7 +690,7 @@ def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=REST
660690
import eventlet
661691
eventlet.monkey_patch()
662692
logger.debug("Using adapter '%s' to run bottle" % adapter)
663-
run(host=host, port=port, quiet=True, debug=False, server=adapter)
693+
run(host=host, port=port, quiet=True, debug=True, server=adapter)
664694
except socket.error, ex:
665695
if "already in use" in getSafeExString(ex):
666696
logger.error("Address already in use ('%s:%s')" % (host, port))
@@ -681,7 +711,12 @@ def _client(url, options=None):
681711
data = None
682712
if options is not None:
683713
data = jsonize(options)
684-
req = urllib2.Request(url, data, {"Content-Type": "application/json"})
714+
headers = {"Content-Type": "application/json"}
715+
716+
if DataStore.username or DataStore.password:
717+
headers["Authorization"] = "Basic %s" % ("%s:%s" % (DataStore.username or "", DataStore.password or "")).encode("base64").strip()
718+
719+
req = urllib2.Request(url, data, headers)
685720
response = urllib2.urlopen(req)
686721
text = response.read()
687722
except:
@@ -690,12 +725,14 @@ def _client(url, options=None):
690725
raise
691726
return text
692727

693-
694-
def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT):
728+
def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=None, password=None):
695729
"""
696730
REST-JSON API client
697731
"""
698732

733+
DataStore.username = username
734+
DataStore.password = password
735+
699736
dbgMsg = "Example client access from command line:"
700737
dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\{16\}') && echo $taskid" % (host, port)
701738
dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"http://testphp.vulnweb.com/artists.php?artist=1\"}' http://%s:%d/scan/$taskid/start" % (host, port)
@@ -709,7 +746,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT):
709746
try:
710747
_client(addr)
711748
except Exception, ex:
712-
if not isinstance(ex, urllib2.HTTPError):
749+
if not isinstance(ex, urllib2.HTTPError) or ex.code == httplib.UNAUTHORIZED:
713750
errMsg = "There has been a problem while connecting to the "
714751
errMsg += "REST-JSON API server at '%s' " % addr
715752
errMsg += "(%s)" % ex

sqlmapapi.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ def main():
4040
apiparser.add_option("-H", "--host", help="Host of the REST-JSON API server (default \"%s\")" % RESTAPI_DEFAULT_ADDRESS, default=RESTAPI_DEFAULT_ADDRESS, action="store")
4141
apiparser.add_option("-p", "--port", help="Port of the the REST-JSON API server (default %d)" % RESTAPI_DEFAULT_PORT, default=RESTAPI_DEFAULT_PORT, type="int", action="store")
4242
apiparser.add_option("--adapter", help="Server (bottle) adapter to use (default \"%s\")" % RESTAPI_DEFAULT_ADAPTER, default=RESTAPI_DEFAULT_ADAPTER, action="store")
43+
apiparser.add_option("--username", help="Basic authentication username (optional)", action="store")
44+
apiparser.add_option("--password", help="Basic authentication password (optional)", action="store")
4345
(args, _) = apiparser.parse_args()
4446

4547
# Start the client or the server
4648
if args.server is True:
47-
server(args.host, args.port, adapter=args.adapter)
49+
server(args.host, args.port, adapter=args.adapter, username=args.username, password=args.password)
4850
elif args.client is True:
49-
client(args.host, args.port)
51+
client(args.host, args.port, username=args.username, password=args.password)
5052
else:
5153
apiparser.print_help()
5254

txt/checksum.md5

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ fbf750dc617c3549ee423d6c2334ba4d lib/core/option.py
4646
d8e9250f3775119df07e9070eddccd16 lib/core/replication.py
4747
785f86e3f963fa3798f84286a4e83ff2 lib/core/revision.py
4848
40c80b28b3a5819b737a5a17d4565ae9 lib/core/session.py
49-
d6dc3f75b2f3aff43a7f3382059bea76 lib/core/settings.py
49+
2a84a6aba8a3c553fdf6058a011a491f lib/core/settings.py
5050
d91291997d2bd2f6028aaf371bf1d3b6 lib/core/shell.py
5151
2ad85c130cc5f2b3701ea85c2f6bbf20 lib/core/subprocessng.py
5252
85e3a98bc9ba62125baa13e864f37a3f lib/core/target.py
@@ -98,7 +98,7 @@ d3da4c7ceaf57c4687a052d58722f6bb lib/techniques/dns/use.py
9898
310efc965c862cfbd7b0da5150a5ad36 lib/techniques/union/__init__.py
9999
d71e48e6fd08f75cc612bf8b260994ce lib/techniques/union/test.py
100100
db3090ff9a740ba096ba676fcf44ebfc lib/techniques/union/use.py
101-
9e903297f6d6bb11660af5c7b109ccab lib/utils/api.py
101+
720e899d5097d701d258bdc30eb8aa51 lib/utils/api.py
102102
7d10ba0851da8ee9cd3c140dcd18798e lib/utils/brute.py
103103
c08d2487a53a1db8170178ebcf87c864 lib/utils/crawler.py
104104
ba12c69a90061aa14d848b8396e79191 lib/utils/deps.py
@@ -223,7 +223,7 @@ b04db3e861edde1f9dd0a3850d5b96c8 shell/backdoor.asp_
223223
c3cc8b7727161e64ab59f312c33b541a shell/stager.aspx_
224224
1f7f125f30e0e800beb21e2ebbab18e1 shell/stager.jsp_
225225
01e3505e796edf19aad6a996101c81c9 shell/stager.php_
226-
0751a45ac4c130131f2cdb74d866b664 sqlmapapi.py
226+
8755985bcb91e3fea7aaaea3e98ec2dc sqlmapapi.py
227227
41a637eda3e182d520fa4fb435edc1ec sqlmap.py
228228
08c711a470d7e0bf705320ba3c48b886 tamper/apostrophemask.py
229229
e8509df10d3f1c28014d7825562d32dd tamper/apostrophenullencode.py

0 commit comments

Comments
 (0)