77"""
88
99import contextlib
10+ import httplib
1011import logging
1112import os
1213import re
4344from lib .core .settings import RESTAPI_DEFAULT_PORT
4445from lib .core .subprocessng import Popen
4546from lib .parse .cmdline import cmdLineParser
47+ from thirdparty .bottle .bottle import abort
4648from thirdparty .bottle .bottle import error as return_error
4749from thirdparty .bottle .bottle import get
4850from thirdparty .bottle .bottle import hook
5254from thirdparty .bottle .bottle import run
5355from thirdparty .bottle .bottle import server_names
5456
55-
56- # global settings
57+ # Global data storage
5758class DataStore (object ):
5859 admin_id = ""
5960 current_db = None
6061 tasks = dict ()
61-
62+ username = None
63+ password = None
6264
6365# API objects
6466class Database (object ):
@@ -118,7 +120,6 @@ def init(self):
118120 "taskid INTEGER, error TEXT"
119121 ")" )
120122
121-
122123class 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
288288def 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" )
293315def 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
313335def error401 (error = None ):
314336 security_headers (False )
315337 return "Access denied"
316338
317-
318339@return_error (404 ) # Not Found
319340def 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)
325345def error405 (error = None ):
326346 security_headers (False )
327347 return "Method not allowed"
328348
329-
330349@return_error (500 ) # Internal Server Error
331350def 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" )
342369def 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" )
356382def task_delete (taskid ):
357383 """
@@ -370,7 +396,6 @@ def task_delete(taskid):
370396# Admin functions #
371397###################
372398
373-
374399@get ("/admin/<taskid>/list" )
375400def 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" )
409433def 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" )
422445def 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" )
441463def 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" )
458480def 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" )
478500def 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" )
495517def 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" )
512534def 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" )
534556def 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
0 commit comments