7
7
"""
8
8
9
9
import contextlib
10
+ import httplib
10
11
import logging
11
12
import os
12
13
import re
43
44
from lib .core .settings import RESTAPI_DEFAULT_PORT
44
45
from lib .core .subprocessng import Popen
45
46
from lib .parse .cmdline import cmdLineParser
47
+ from thirdparty .bottle .bottle import abort
46
48
from thirdparty .bottle .bottle import error as return_error
47
49
from thirdparty .bottle .bottle import get
48
50
from thirdparty .bottle .bottle import hook
52
54
from thirdparty .bottle .bottle import run
53
55
from thirdparty .bottle .bottle import server_names
54
56
55
-
56
- # global settings
57
+ # Global data storage
57
58
class DataStore (object ):
58
59
admin_id = ""
59
60
current_db = None
60
61
tasks = dict ()
61
-
62
+ username = None
63
+ password = None
62
64
63
65
# API objects
64
66
class Database (object ):
@@ -118,7 +120,6 @@ def init(self):
118
120
"taskid INTEGER, error TEXT"
119
121
")" )
120
122
121
-
122
123
class Task (object ):
123
124
def __init__ (self , taskid , remote_addr ):
124
125
self .remote_addr = remote_addr
@@ -283,11 +284,32 @@ def setRestAPILog():
283
284
LOGGER_RECORDER = LogRecorder ()
284
285
logger .addHandler (LOGGER_RECORDER )
285
286
286
-
287
287
# Generic functions
288
288
def is_admin (taskid ):
289
289
return DataStore .admin_id == taskid
290
290
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"
291
313
292
314
@hook ("after_request" )
293
315
def security_headers (json_header = True ):
@@ -301,42 +323,47 @@ def security_headers(json_header=True):
301
323
response .headers ["Pragma" ] = "no-cache"
302
324
response .headers ["Cache-Control" ] = "no-cache"
303
325
response .headers ["Expires" ] = "0"
326
+
304
327
if json_header :
305
328
response .content_type = "application/json; charset=UTF-8"
306
329
307
330
##############################
308
331
# HTTP Status Code functions #
309
332
##############################
310
333
311
-
312
334
@return_error (401 ) # Access Denied
313
335
def error401 (error = None ):
314
336
security_headers (False )
315
337
return "Access denied"
316
338
317
-
318
339
@return_error (404 ) # Not Found
319
340
def error404 (error = None ):
320
341
security_headers (False )
321
342
return "Nothing here"
322
343
323
-
324
344
@return_error (405 ) # Method Not Allowed (e.g. when requesting a POST method via GET)
325
345
def error405 (error = None ):
326
346
security_headers (False )
327
347
return "Method not allowed"
328
348
329
-
330
349
@return_error (500 ) # Internal Server Error
331
350
def error500 (error = None ):
332
351
security_headers (False )
333
352
return "Internal server error"
334
353
354
+ #############
355
+ # Auxiliary #
356
+ #############
357
+
358
+ @get ('/error/401' )
359
+ def path_401 ():
360
+ response .status = 401
361
+ return response
362
+
335
363
#############################
336
364
# Task management functions #
337
365
#############################
338
366
339
-
340
367
# Users' methods
341
368
@get ("/task/new" )
342
369
def task_new ():
@@ -351,7 +378,6 @@ def task_new():
351
378
logger .debug ("Created new task: '%s'" % taskid )
352
379
return jsonize ({"success" : True , "taskid" : taskid })
353
380
354
-
355
381
@get ("/task/<taskid>/delete" )
356
382
def task_delete (taskid ):
357
383
"""
@@ -370,7 +396,6 @@ def task_delete(taskid):
370
396
# Admin functions #
371
397
###################
372
398
373
-
374
399
@get ("/admin/<taskid>/list" )
375
400
def task_list (taskid = None ):
376
401
"""
@@ -403,7 +428,6 @@ def task_flush(taskid):
403
428
# sqlmap core interact functions #
404
429
##################################
405
430
406
-
407
431
# Handle task's options
408
432
@get ("/option/<taskid>/list" )
409
433
def option_list (taskid ):
@@ -417,7 +441,6 @@ def option_list(taskid):
417
441
logger .debug ("[%s] Listed task options" % taskid )
418
442
return jsonize ({"success" : True , "options" : DataStore .tasks [taskid ].get_options ()})
419
443
420
-
421
444
@post ("/option/<taskid>/get" )
422
445
def option_get (taskid ):
423
446
"""
@@ -436,12 +459,12 @@ def option_get(taskid):
436
459
logger .debug ("[%s] Requested value for unknown option %s" % (taskid , option ))
437
460
return jsonize ({"success" : False , "message" : "Unknown option" , option : "not set" })
438
461
439
-
440
462
@post ("/option/<taskid>/set" )
441
463
def option_set (taskid ):
442
464
"""
443
465
Set an option (command line switch) for a certain task ID
444
466
"""
467
+
445
468
if taskid not in DataStore .tasks :
446
469
logger .warning ("[%s] Invalid task ID provided to option_set()" % taskid )
447
470
return jsonize ({"success" : False , "message" : "Invalid task ID" })
@@ -452,13 +475,13 @@ def option_set(taskid):
452
475
logger .debug ("[%s] Requested to set options" % taskid )
453
476
return jsonize ({"success" : True })
454
477
455
-
456
478
# Handle scans
457
479
@post ("/scan/<taskid>/start" )
458
480
def scan_start (taskid ):
459
481
"""
460
482
Launch a scan
461
483
"""
484
+
462
485
if taskid not in DataStore .tasks :
463
486
logger .warning ("[%s] Invalid task ID provided to scan_start()" % taskid )
464
487
return jsonize ({"success" : False , "message" : "Invalid task ID" })
@@ -473,12 +496,12 @@ def scan_start(taskid):
473
496
logger .debug ("[%s] Started scan" % taskid )
474
497
return jsonize ({"success" : True , "engineid" : DataStore .tasks [taskid ].engine_get_id ()})
475
498
476
-
477
499
@get ("/scan/<taskid>/stop" )
478
500
def scan_stop (taskid ):
479
501
"""
480
502
Stop a scan
481
503
"""
504
+
482
505
if (taskid not in DataStore .tasks or
483
506
DataStore .tasks [taskid ].engine_process () is None or
484
507
DataStore .tasks [taskid ].engine_has_terminated ()):
@@ -490,12 +513,12 @@ def scan_stop(taskid):
490
513
logger .debug ("[%s] Stopped scan" % taskid )
491
514
return jsonize ({"success" : True })
492
515
493
-
494
516
@get ("/scan/<taskid>/kill" )
495
517
def scan_kill (taskid ):
496
518
"""
497
519
Kill a scan
498
520
"""
521
+
499
522
if (taskid not in DataStore .tasks or
500
523
DataStore .tasks [taskid ].engine_process () is None or
501
524
DataStore .tasks [taskid ].engine_has_terminated ()):
@@ -507,12 +530,12 @@ def scan_kill(taskid):
507
530
logger .debug ("[%s] Killed scan" % taskid )
508
531
return jsonize ({"success" : True })
509
532
510
-
511
533
@get ("/scan/<taskid>/status" )
512
534
def scan_status (taskid ):
513
535
"""
514
536
Returns status of a scan
515
537
"""
538
+
516
539
if taskid not in DataStore .tasks :
517
540
logger .warning ("[%s] Invalid task ID provided to scan_status()" % taskid )
518
541
return jsonize ({"success" : False , "message" : "Invalid task ID" })
@@ -529,12 +552,12 @@ def scan_status(taskid):
529
552
"returncode" : DataStore .tasks [taskid ].engine_get_returncode ()
530
553
})
531
554
532
-
533
555
@get ("/scan/<taskid>/data" )
534
556
def scan_data (taskid ):
535
557
"""
536
558
Retrieve the data of a scan
537
559
"""
560
+
538
561
json_data_message = list ()
539
562
json_errors_message = list ()
540
563
@@ -560,6 +583,7 @@ def scan_log_limited(taskid, start, end):
560
583
"""
561
584
Retrieve a subset of log messages
562
585
"""
586
+
563
587
json_log_messages = list ()
564
588
565
589
if taskid not in DataStore .tasks :
@@ -586,6 +610,7 @@ def scan_log(taskid):
586
610
"""
587
611
Retrieve the log messages
588
612
"""
613
+
589
614
json_log_messages = list ()
590
615
591
616
if taskid not in DataStore .tasks :
@@ -606,6 +631,7 @@ def download(taskid, target, filename):
606
631
"""
607
632
Download a certain file from the file system
608
633
"""
634
+
609
635
if taskid not in DataStore .tasks :
610
636
logger .warning ("[%s] Invalid task ID provided to download()" % taskid )
611
637
return jsonize ({"success" : False , "message" : "Invalid task ID" })
@@ -626,13 +652,17 @@ def download(taskid, target, filename):
626
652
return jsonize ({"success" : False , "message" : "File does not exist" })
627
653
628
654
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 ):
630
656
"""
631
657
REST-JSON API server
632
658
"""
659
+
633
660
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 (_ )
636
666
637
667
if port == 0 : # random
638
668
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
660
690
import eventlet
661
691
eventlet .monkey_patch ()
662
692
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 )
664
694
except socket .error , ex :
665
695
if "already in use" in getSafeExString (ex ):
666
696
logger .error ("Address already in use ('%s:%s')" % (host , port ))
@@ -681,7 +711,12 @@ def _client(url, options=None):
681
711
data = None
682
712
if options is not None :
683
713
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 )
685
720
response = urllib2 .urlopen (req )
686
721
text = response .read ()
687
722
except :
@@ -690,12 +725,14 @@ def _client(url, options=None):
690
725
raise
691
726
return text
692
727
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 ):
695
729
"""
696
730
REST-JSON API client
697
731
"""
698
732
733
+ DataStore .username = username
734
+ DataStore .password = password
735
+
699
736
dbgMsg = "Example client access from command line:"
700
737
dbgMsg += "\n \t $ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\{16\}') && echo $taskid" % (host , port )
701
738
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):
709
746
try :
710
747
_client (addr )
711
748
except Exception , ex :
712
- if not isinstance (ex , urllib2 .HTTPError ):
749
+ if not isinstance (ex , urllib2 .HTTPError ) or ex . code == httplib . UNAUTHORIZED :
713
750
errMsg = "There has been a problem while connecting to the "
714
751
errMsg += "REST-JSON API server at '%s' " % addr
715
752
errMsg += "(%s)" % ex
0 commit comments