├── LICENSE ├── ReadMe.md ├── app ├── __init__.py ├── __init__.pyc ├── controllers │ ├── __init__.py │ ├── __init__.pyc │ ├── api.py │ ├── api.pyc │ ├── smoke.py │ └── smoke.pyc ├── env │ ├── __init__.py │ ├── __init__.pyc │ ├── config.py │ └── config.pyc ├── helpers │ ├── __init__.py │ ├── __init__.pyc │ ├── constants.py │ ├── constants.pyc │ ├── helper.py │ ├── helper.pyc │ ├── utils.py │ └── utils.pyc └── models │ ├── Scan.py │ ├── Scan.pyc │ ├── __init__.py │ └── __init__.pyc ├── db └── .gitkeep ├── example ├── sample.yaml └── smoke.yaml ├── requirements.txt └── susanoo.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chaithu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | Susanoo: 2 | ================================================= 3 | 4 | Susanoo is a REST API security testing framework. 5 | 6 | ## Features 7 | 8 | - Configurable inputs/outputs formats 9 | - API Vulnerability Scan: Normal scanning engine that scans for IDOR, Authentication issues, SQL injections, Error stacks. 10 | - Smoke Scan: Custom output checks for known pocs can be configured to run daily. 11 | 12 | ## Types of Scans: 13 | * API Vulnerability Scan 14 | ** Scans for following bugs: 15 | *** Indirect Object References 16 | *** Authentication issues 17 | *** SQL injections 18 | *** Error stacks 19 | 20 | * Smoke Scan 21 | ** A known Proof-of-concept can be configured to run daily/weekly etc. 22 | 23 | 24 | ## Configuration: 25 | 26 | Susanoo takes yaml files in configuration. Please check the examples folder for sample configuration files. 27 | 28 | 29 | ## Parameter Types: 30 | ~~~ 31 | resource --> static 32 | Eg: In the following example the value "password" is used for grant_type: 33 | 34 | password: {"type":"resource", "required":True, "value":"p@ssw0rd"} 35 | 36 | hex-n: 37 | Generate hex of length n. 38 | Eg: a hex value of length 16 is generated for uniqueId in below example: 39 | 40 | id: {'type':'hex-16', 'required': True} 41 | 42 | int-n: 43 | Generates int of size n 44 | Eg: a int value of size 4 is generated for uniqueId in below example: 45 | 46 | bonus: {'type':'int-4', 'required':'True'} 47 | 48 | email: 49 | Generates random email id 50 | Eg: a random email id is generated and assigned for email_id 51 | 52 | email_id: {"type":"email", "required":True} 53 | 54 | username: 55 | Generates random username 56 | Eg: a random username is generated and assigned for username 57 | 58 | username: {"type":"username", "required":True} 59 | 60 | string: 61 | Generates random strings 62 | Eg: generates random strings of variable length. 63 | 64 | string: {"type":"string", "required":True} 65 | 66 | ~~~ 67 | 68 | ## Donation: 69 | 70 | If you like the project, you can buy me beers :) 71 | 72 | [![Donate Bitcoin](https://img.shields.io/badge/donate-bitcoin-green.svg)](https://ant4g0nist.github.io) 73 | 74 | 75 | ## Installation: 76 | 77 | ^^/D/projects >>> git clone https://github.com/ant4g0nist/susanoo 78 | ^^/D/projects >>> cd susanoo 79 | ^^/D/p/susanoo >>> sudo pip install -r requirements.txt 80 | 81 | ## Usage: 82 | 83 | ^^/D/p/susanoo >>> cd db 84 | ^^/D/p/s/db >>> sudo mongod --dbpath . --bind_ip=127.0.0.1 85 | 86 | ^^/D/p/susanoo >>> python susanoo.py 87 | 88 | 89 | ## TODO: 90 | 91 | - [ ] Use celery/scheduler to schedule the scans 92 | - [ ] Chain apis together? pickup value from one api and use in another 93 | - [ ] Add more vulnerability checks 94 | - [ ] Make it more reliable 95 | - [ ] Parallelize scans using Celery 96 | - [ ] Add better reporting 97 | 98 | ## Thanks: 99 | 100 | - Go-Jek Security Team 101 | - [restfuzz](https://github.com/redhat-cip/restfuzz) -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/__init__.py -------------------------------------------------------------------------------- /app/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/__init__.pyc -------------------------------------------------------------------------------- /app/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/controllers/__init__.py -------------------------------------------------------------------------------- /app/controllers/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/controllers/__init__.pyc -------------------------------------------------------------------------------- /app/controllers/api.py: -------------------------------------------------------------------------------- 1 | # @ant4g0nist 2 | 3 | import os 4 | import json 5 | import time 6 | import uuid 7 | import yaml 8 | import numpy 9 | import random 10 | import hashlib 11 | import requests 12 | import itertools 13 | from ..helpers.constants import * 14 | from ..models import Scan as ScanModel 15 | from ..helpers.helper import Generator 16 | from ..helpers.utils import TerminalColors 17 | 18 | tty_colors = TerminalColors(True) 19 | 20 | class SusanooConfig: 21 | def __init__(self, config): 22 | config = open(config,'r') 23 | self.scanId = str(uuid.uuid1()) 24 | self.config = yaml.load(config) 25 | self.hash = getHash(config.read()) 26 | self.host = self.config['host'] 27 | self.userA_Authorization = self.config['UserA_Authorization'] 28 | self.userB_Authorization = self.config['UserB_Authorization'] 29 | self.headers_yaml = self.config['headers'] 30 | 31 | self.generator = Generator() 32 | 33 | self.headers = {'User-Agent': 'mozilla/5.0 (iphone; cpu iphone os 7_0_2 like mac os x) applewebkit/537.51.1 (khtml, like gecko) version/7.0 mobile/11a501 safari/9537.5', 'Accept': 'application/json', 'Content-Type':'application/x-www-form-urlencoded'} 34 | 35 | self.set_headers() 36 | 37 | #save to db 38 | apiscan = ScanModel.APIScanResults(scanId=self.scanId, hash=self.hash, updatedTime=time.strftime("%Y.%m.%d %H:%M:%S"), config=config.read().encode('hex')) 39 | apiscan.save() 40 | 41 | def set_headers(self, userB=None): 42 | headers = {} 43 | for i in self.headers_yaml: 44 | name = i["name"] 45 | value = i["value"] 46 | values = self.generator.generate_inputs(i["inputs"]) 47 | for k,v in values.items(): 48 | headers[name] = value.replace("%%(%s)"%k,v) 49 | 50 | for i in headers: 51 | self.headers[i] = headers[i] 52 | 53 | 54 | if not userB: 55 | for i in self.userA_Authorization: 56 | name = i['name'] 57 | value = i['value'] 58 | 59 | self.headers[name] = value 60 | else: 61 | for i in self.userB_Authorization: 62 | name = i['name'] 63 | value = i['value'] 64 | 65 | self.headers[name] = value 66 | 67 | 68 | def get_apis(self): 69 | self.apis = {} 70 | 71 | for api in self.config['apis']: 72 | apic = API(api, self.host) 73 | self.apis[api['name']] = apic 74 | 75 | return self.apis 76 | 77 | class APIRequest: 78 | def __init__(self): 79 | pass 80 | 81 | def run(self, api, method, params, headers): 82 | 83 | try: 84 | return requests.request(method, url=api, headers=headers, data=params) 85 | except Exception as e: 86 | api_request_error_handler(api) 87 | return None 88 | 89 | class API: 90 | def __init__(self, args, host): 91 | self.host = host 92 | self.args = args 93 | self.generator = Generator() 94 | self.name = self.args['name'] 95 | self.raw_url = None 96 | self.api = self.args['api'] 97 | self.method = self.args['method'] 98 | self.inputs = {} 99 | self.url_inputs = [] 100 | 101 | # def set_url_inputs() 102 | if 'inputs' in self.args.keys() and self.args['inputs']: 103 | if 'url_input' in self.args['inputs']: 104 | url_inputs = dict(self.args['inputs']['url_input']) 105 | self.url_inputs.append(url_inputs) 106 | url = self.api 107 | self.raw_url = self.api 108 | for j in url_inputs: 109 | input_name = j 110 | type_ = url_inputs[j]['type'] 111 | value = str(self.generator.generate(type_)) 112 | url = url.replace("%%(%s)"%input_name, value) 113 | 114 | self.api = url 115 | del self.args['inputs']['url_input'] 116 | 117 | 118 | # print self.args["inputs"] 119 | self.inputs = self.generator.generate_inputs(self.args["inputs"]) 120 | print self.inputs 121 | 122 | class Scans: 123 | def __init__(self, api, susanoo): 124 | self.susanoo = susanoo 125 | self.scanId = self.susanoo.scanId 126 | self.hash = self.susanoo.hash 127 | self.headers = self.susanoo.headers 128 | self.url_inputs = api.url_inputs 129 | self.url = susanoo.host+api.api 130 | try: 131 | self.raw_url = susanoo.host+api.raw_url 132 | except: 133 | self.raw_url = None 134 | self.method = api.method 135 | self.params = api.inputs 136 | self.name = api.name 137 | self.authorization_header = susanoo.userA_Authorization 138 | 139 | 140 | self.orig_request = APIRequest() 141 | self.orig_resp = self.orig_request.run(self.url, self.method, self.params, self.headers) 142 | 143 | 144 | def authentication_check(self): 145 | 146 | headers = dict(self.headers) 147 | 148 | # check 1: with empty authorization header 149 | for i in self.authorization_header: 150 | # orignal request 151 | del headers[i['name']] 152 | 153 | fuzz_request = APIRequest() 154 | fuzz_resp = fuzz_request.run(self.url, self.method, self.params, headers) 155 | 156 | vuln = False 157 | if fuzz_resp: 158 | # check if fuzz_resp gives 401 159 | if fuzz_resp.status_code==401: 160 | vuln = True 161 | print tty_colors.green()+'%s api is authenticated'%self.name+tty_colors.default() 162 | 163 | elif fuzz_resp.status_code==200: 164 | if self.orig_resp.text == fuzz_resp.text: 165 | vuln = True 166 | print tty_colors.red()+'holy cow, %s api is unauthenticated'%self.name+tty_colors.default() 167 | 168 | # check 2: with random header 169 | headers = self.susanoo.generator.generate_inputs(self.susanoo.headers_yaml) 170 | 171 | fuzz_request = APIRequest() 172 | fuzz_resp = fuzz_request.run(self.url, self.method, self.params, headers) 173 | 174 | if fuzz_resp: 175 | if fuzz_resp.status_code==401: 176 | vuln = True 177 | print tty_colors.green()+'%s api is authenticated'%self.name+tty_colors.default() 178 | 179 | elif fuzz_resp.status_code>=500: 180 | handle500(self.url, self.method, self.params) 181 | 182 | elif fuzz_resp.status_code==200: 183 | if self.orig_resp.text == fuzz_resp.text: 184 | vuln = True 185 | print tty_colors.red()+'holy cow, %s api is unauthenticated'%self.name+tty_colors.default() 186 | 187 | if vuln: 188 | self.save_to_db("unauthenticated", self.url, self.headers, self.method, self.params) 189 | 190 | def authorization_check(self): 191 | headers_a = dict(self.headers) 192 | 193 | self.susanoo.set_headers(1) 194 | 195 | headers_b = self.susanoo.headers 196 | 197 | fuzz_request = APIRequest() 198 | fuzz_resp = fuzz_request.run(self.url, self.method, self.params, headers_b) 199 | 200 | vuln = False 201 | if fuzz_resp: 202 | # check if fuzz_resp gives 401 203 | if fuzz_resp.status_code==401: 204 | vuln = True 205 | print tty_colors.green()+'%s api is authorized'%self.name+tty_colors.default() 206 | 207 | elif fuzz_resp.status_code>=500: 208 | handle500(self.url, self.method, self.params) 209 | 210 | elif fuzz_resp.status_code==200: 211 | if self.orig_resp.text == fuzz_resp.text: 212 | vuln = True 213 | print tty_colors.red()+'holy cow, %s api is unauthorized'%self.name+tty_colors.default() 214 | 215 | self.susanoo.set_headers(0) 216 | 217 | if vuln: 218 | self.save_to_db("unauthorized", self.url, self.headers, self.method, self.params) 219 | 220 | def sqlinjection_heuristic_check(self): 221 | sql_injection_found = False 222 | 223 | for payload in HEURISTIC_CHECK_ALPHABET: 224 | 225 | params = dict(self.params) 226 | 227 | for param in params: 228 | request_params = dict(params) 229 | request_params[param] = "%s%s"%(request_params[param],payload) 230 | 231 | fuzz_request = APIRequest() 232 | fuzz_resp = fuzz_request.run(self.url, self.method, request_params, self.headers) 233 | 234 | vuln = False 235 | if fuzz_resp: 236 | if any(i in fuzz_resp.text for i in FORMAT_EXCEPTION_STRINGS) and not any(i in self.orig_resp.text for i in FORMAT_EXCEPTION_STRINGS): 237 | vuln = True 238 | print tty_colors.red()+'server error in %s api'%self.name+tty_colors.default() 239 | print tty_colors.green()+"poc: %s"%json.dumps(params)+tty_colors.default() 240 | sql_injection_found = True 241 | 242 | if vuln: 243 | self.save_to_db("possible SQLi", self.url, self.headers, self.method, request_params, param) 244 | 245 | if not sql_injection_found: 246 | print tty_colors.green()+'heuristic checks for sql injection failed to find sqli for %s api'%self.name+tty_colors.default() 247 | 248 | def ratelimit_check(self): 249 | rate_limit = False 250 | count = 0 251 | while count<30: 252 | if not rate_limit: 253 | fuzz_request = APIRequest() 254 | fuzz_resp = fuzz_request.run(self.url, self.method, self.params, self.headers) 255 | 256 | if any(i in fuzz_resp.text for i in RATE_LIMIT_STRINGS) and not any(i in self.orig_resp.text for i in RATE_LIMIT_STRINGS): 257 | print tty_colors.green()+"api: %s seems rate limited."%self.name+tty_colors.default() 258 | rate_limit = True 259 | break 260 | 261 | count+=1 262 | 263 | if not rate_limit: 264 | print tty_colors.red()+"api: %s is not properly rate limited."%self.name+tty_colors.default() 265 | self.save_to_db("rate limiting", self.url, self.headers, self.method, self.params) 266 | 267 | 268 | def fuzz(self): 269 | print tty_colors.red()+"[:Fuzz:] Not yet Implemented"+tty_colors.default() 270 | return 271 | 272 | def url_param_fuzz(self): 273 | url = self.raw_url 274 | count=10 275 | while count>0: 276 | for input in self.url_inputs: 277 | url = self.raw_url 278 | 279 | for j in input: 280 | input_name = j 281 | type_ = input[j]['type'] 282 | value = str(self.susanoo.generator.generate(type_)) + self.susanoo.generator.gen_unicode() 283 | url = url.replace("%%(%s)"%input_name, value) 284 | 285 | fuzz_request = APIRequest() 286 | fuzz_resp = self.orig_request.run(url, self.method, self.params, self.headers) 287 | 288 | vuln = False 289 | if fuzz_resp: 290 | print tty_colors.cyan()+"*"*100+tty_colors.default() 291 | 292 | if any(i in fuzz_resp.text for i in FORMAT_EXCEPTION_STRINGS) and not any(i in self.orig_resp.text for i in FORMAT_EXCEPTION_STRINGS): 293 | vuln = True 294 | print tty_colors.red()+'server error in %s api'%self.name+tty_colors.default() 295 | print tty_colors.green()+"poc %s"%json.dumps(url)+tty_colors.default() 296 | server_error_stack = True 297 | 298 | if vuln: 299 | self.save_to_db("Server Error Trace", url, self.headers, self.method, self.params, input_name) 300 | 301 | count-=1 302 | 303 | def save_to_db(self, vuln, api, headers, method, parameters, param=None): 304 | 305 | authApiScanResults = ScanModel.APIScanResults.objects(scanId=self.scanId) 306 | 307 | if authApiScanResults: 308 | authApiScanResults=authApiScanResults[0] 309 | apiscan = ScanModel.APIScan(api=api, headers=json.dumps(headers), parameters=json.dumps(parameters), method=method, vulnerability=vuln, scanId=self.scanId, updatedTime=time.strftime("%Y.%m.%d %H:%M:%S"), parameter=param) 310 | apiscan.save() 311 | 312 | authApiScanResults.apiscans.append(apiscan) 313 | authApiScanResults.save() 314 | 315 | def api_request_error_handler(api): 316 | print tty_colors.blue()+'There was an error trying to request %s '%(api)+tty_colors.default() 317 | 318 | def handle500(api, method, params): 319 | print tty_colors.red()+'%s is throwing 500 with following params for method %s'%(api, method) 320 | print tty_colors.blue()+'%s'%json.dumps(params)+tty_colors.default() 321 | 322 | def getHash(config): 323 | m = hashlib.sha256() 324 | m.update(config) 325 | return m.hexdigest() 326 | -------------------------------------------------------------------------------- /app/controllers/api.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/controllers/api.pyc -------------------------------------------------------------------------------- /app/controllers/smoke.py: -------------------------------------------------------------------------------- 1 | # @ant4g0nist 2 | 3 | import os 4 | import json 5 | import yaml 6 | import numpy 7 | import random 8 | import requests 9 | import itertools 10 | from ..helpers.constants import * 11 | from ..helpers.helper import Generator 12 | from ..helpers.utils import TerminalColors 13 | 14 | tty_colors = TerminalColors(True) 15 | 16 | class SusanooConfig: 17 | def __init__(self, config): 18 | config = open(config,'r') 19 | self.config = yaml.load(config) 20 | self.host = self.config['host'] 21 | self.headers_yaml = self.config['headers'] 22 | self.generator = Generator() 23 | self.userA_Authorization = self.config['UserA_Authorization'] 24 | self.userB_Authorization = self.config['UserB_Authorization'] 25 | self.headers = {'User-Agent': 'mozilla/5.0 (iphone; cpu iphone os 7_0_2 like mac os x) applewebkit/537.51.1 (khtml, like gecko) version/7.0 mobile/11a501 safari/9537.5', 'Accept': 'application/json', 'Content-Type':'application/x-www-form-urlencoded'} 26 | 27 | self.set_headers() 28 | 29 | def set_headers(self, userB=None): 30 | headers = {} 31 | for i in self.headers_yaml: 32 | name = i["name"] 33 | value = i["value"] 34 | values = self.generator.generate_inputs(i["inputs"]) 35 | for k,v in values.items(): 36 | headers[name] = value.replace("%%(%s)"%k,v) 37 | 38 | for i in headers: 39 | self.headers[i] = headers[i] 40 | 41 | 42 | if not userB: 43 | for i in self.userA_Authorization: 44 | name = i['name'] 45 | value = i['value'] 46 | 47 | self.headers[name] = value 48 | else: 49 | for i in self.userB_Authorization: 50 | name = i['name'] 51 | value = i['value'] 52 | 53 | self.headers[name] = value 54 | 55 | def get_apis(self): 56 | self.apis = {} 57 | 58 | for api in self.config['apis']: 59 | apic = API(api, self.host) 60 | self.apis[api['name']] = apic 61 | 62 | return self.apis 63 | 64 | class APIRequest: 65 | def __init__(self): 66 | pass 67 | 68 | def run(self, api, method, params, headers): 69 | 70 | try: 71 | return requests.request(method, url=api, headers=headers, data=params) 72 | except Exception as e: 73 | print e 74 | api_request_error_handler(api) 75 | return None 76 | 77 | class API: 78 | def __init__(self, args, host): 79 | self.host = host 80 | self.args = args 81 | self.generator = Generator() 82 | self.name = self.args['name'] 83 | 84 | if "headers" in self.args.keys(): 85 | self.headers = self.args['headers'] 86 | 87 | self.status_code = self.args['status_code'] 88 | self.raw_url = None 89 | self.api = self.args['api'] 90 | self.method = self.args['method'] 91 | self.inputs = {} 92 | self.outputs = args.setdefault('outputs', {}) 93 | self.url_inputs = [] 94 | 95 | 96 | # def set_url_inputs() 97 | if 'inputs' in self.args.keys(): 98 | if 'url_input' in self.args['inputs']: 99 | url_inputs = dict(self.args['inputs']['url_input']) 100 | self.url_inputs.append(url_inputs) 101 | url = self.api 102 | self.raw_url = self.api 103 | for j in url_inputs: 104 | input_name = j 105 | type_ = url_inputs[j]['type'] 106 | value = str(self.generator.generate(type_)) 107 | url = url.replace("%%(%s)"%input_name, value) 108 | 109 | self.api = url 110 | del self.args['inputs']['url_input'] 111 | 112 | self.inputs = self.generator.generate_inputs(self.args) 113 | 114 | class Scans: 115 | def __init__(self, api, susanoo): 116 | self.susanoo = susanoo 117 | self.headers = self.susanoo.headers 118 | self.url_inputs = api.url_inputs 119 | self.url = susanoo.host+api.api 120 | 121 | try: 122 | self.raw_url = susanoo.host+api.raw_url 123 | except: 124 | self.raw_url = None 125 | 126 | self.outputs = api.outputs 127 | self.status_code = api.status_code 128 | self.method = api.method 129 | self.params = api.inputs 130 | self.name = api.name 131 | self.authorization_header = susanoo.userA_Authorization 132 | 133 | def run(self): 134 | self.orig_request = APIRequest() 135 | self.orig_resp = self.orig_request.run(self.url, self.method, self.params, self.headers) 136 | 137 | if self.orig_resp: 138 | if self.orig_resp.status_code>=500: 139 | handle500(self.url, self.method, self.params) 140 | return 141 | 142 | elif self.orig_resp.status_code==self.status_code: 143 | try: 144 | json_output = self.orig_resp.json() 145 | except ValueError: 146 | api_request_error_handler(self.url) 147 | return 148 | 149 | outputs = {} 150 | for output in self.outputs: 151 | if "json_extract" in self.outputs[output].keys(): 152 | value = eval(self.outputs[output]['json_extract'])(json_output) 153 | if not value: 154 | continue 155 | 156 | if self.outputs[output]["value"] in value: 157 | print tty_colors.green()+'Smoke scan for api: %s success. Given value of output found in api response.'%(self.name)+tty_colors.default() 158 | 159 | else: 160 | print tty_colors.red()+'Smoke scan for api: %s failed. Given value of output was not found in api response.'%(self.name)+tty_colors.default() 161 | 162 | if "list_extract" in self.outputs[output].keys(): 163 | 164 | value = map(eval(self.outputs[output]['list_extract']),json_output) 165 | 166 | if not value: 167 | continue 168 | 169 | if self.outputs[output]["value"] in value: 170 | print tty_colors.green()+'Smoke scan for api: %s success. Given value of output found in api response.'%(self.name)+tty_colors.default() 171 | 172 | else: 173 | print tty_colors.red()+'Smoke scan for api: %s failed. Given value of output was not found in api response.'%(self.name)+tty_colors.default() 174 | else: 175 | # handle500(self.url, self.method, self.params) 176 | unexpected_status_code(self.url, self.method, self.params, self.orig_resp.status_code) 177 | return 178 | 179 | def api_request_error_handler(api): 180 | print tty_colors.blue()+'There was an error trying to request %s '%(api)+tty_colors.default() 181 | 182 | def handle500(api, method, params): 183 | print tty_colors.red()+'%s is throwing 500 with following params for method %s'%(api, method) 184 | print tty_colors.blue()+'%s'%json.dumps(params)+tty_colors.default() 185 | 186 | def unexpected_status_code(api, method, params, status_code): 187 | print tty_colors.red()+'%s is giving %s with following params for method %s'%(api, status_code, method) 188 | if params: 189 | print tty_colors.blue()+'%s'%json.dumps(params)+tty_colors.default() 190 | -------------------------------------------------------------------------------- /app/controllers/smoke.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/controllers/smoke.pyc -------------------------------------------------------------------------------- /app/env/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/env/__init__.py -------------------------------------------------------------------------------- /app/env/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/env/__init__.pyc -------------------------------------------------------------------------------- /app/env/config.py: -------------------------------------------------------------------------------- 1 | from mongoengine import * 2 | 3 | MONGO_DBNAME = "susanoo" 4 | MONGO_URI = "mongodb://localhost:27017/"+MONGO_DBNAME 5 | 6 | db = connect(host=MONGO_URI) 7 | -------------------------------------------------------------------------------- /app/env/config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/env/config.pyc -------------------------------------------------------------------------------- /app/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/helpers/__init__.py -------------------------------------------------------------------------------- /app/helpers/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/helpers/__init__.pyc -------------------------------------------------------------------------------- /app/helpers/constants.py: -------------------------------------------------------------------------------- 1 | # sql payloads for heuristic checks 2 | 3 | HEURISTIC_CHECK_ALPHABET = ('"', '\'', ')', '(', ',', '.') 4 | 5 | FORMAT_EXCEPTION_STRINGS = ("Type mismatch", "Error converting", "Conversion failed", "String or binary data would be truncated", "Failed to convert", "unable to interpret text value", "Input string was not in a correct format", "System.FormatException", "java.lang.NumberFormatException", "ValueError: invalid literal", "DataTypeMismatchException", "CF_SQL_INTEGER", " for CFSQLTYPE ", "cfqueryparam cfsqltype", "InvalidParamTypeException", "Invalid parameter type", "is not of type numeric", " 1", 60 | "' OR 'whatever' in ('whatever')", 61 | "' OR 'text' > 't'", 62 | "' OR 2 BETWEEN 1 and 3", 63 | "' or username like char(37);", 64 | "' union select * from users where login = char(114,111,111,116);", 65 | "' union select ", 66 | "Password:*/=1--", 67 | "UNI/**/ON SEL/**/ECT", 68 | "'; EXECUTE IMMEDIATE 'SEL' || 'ECT US' || 'ER'", 69 | "'; EXEC ('SEL' + 'ECT US' + 'ER')", 70 | "'/**/OR/**/1/**/=/**/1", 71 | "' or 1/*", 72 | "+or+isnull%281%2F0%29+%2F*", 73 | "%27+OR+%277659%27%3D%277659", 74 | "%22+or+isnull%281%2F0%29+%2F*", 75 | "%27+--+&password=", 76 | "'; begin declare @var varchar(8000) set @var=':' select @var=@var+'+login+'/'+password+' ' from users where login > ", 77 | " @var select @var as var into temp end --", 78 | "' and 1 in (select var from temp)--", 79 | "' union select 1,load_file('/etc/passwd'),1,1,1;", 80 | "1;(load_file(char(47,101,116,99,47,112,97,115,115,119,100))),1,1,1;", 81 | "' and 1=( if((load_file(char(110,46,101,120,116))<>char(39,39)),1,0));" 82 | ) -------------------------------------------------------------------------------- /app/helpers/constants.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/helpers/constants.pyc -------------------------------------------------------------------------------- /app/helpers/helper.py: -------------------------------------------------------------------------------- 1 | # @ant4g0nist 2 | 3 | import os 4 | import uuid 5 | import string 6 | import struct 7 | import random 8 | import constants 9 | 10 | class Generator(object): 11 | """ 12 | Class to generate random inputs 13 | """ 14 | def __init__(self): 15 | seed = os.urandom(10) 16 | random.seed(seed) 17 | 18 | def generate_inputs(self, input_description): 19 | """ 20 | Generate inputs 21 | """ 22 | call_params = {} 23 | 24 | def walk_inputs(data_set, params, parent_key=None, type_=None): 25 | if isinstance(data_set, dict): 26 | for input_name, value in data_set.items(): 27 | if 'type' not in value: 28 | type_ = type(value) 29 | 30 | parent_key = input_name 31 | params[parent_key] = [] 32 | 33 | inputs = walk_inputs(value, {}, parent_key, type_) 34 | if inputs: 35 | if type_==dict: 36 | params[input_name] = inputs 37 | elif type_==list: 38 | 39 | params[parent_key].append(inputs) 40 | continue 41 | 42 | if 'required' in value or self.once_every(5): 43 | 44 | resource_name = None 45 | 46 | if value['type'] in ('resource', 'list_resource'): 47 | resource_name = value.setdefault('resource_name', input_name) 48 | if "value" in value.keys(): 49 | new_input = self.generate_input(value['type'], resource_name, value["value"]) 50 | else: 51 | new_input = self.generate_input(value['type'], resource_name) 52 | if "expand" in value and isinstance(new_input, dict): 53 | for k, value in new_input.items(): 54 | params[k] = value 55 | else: 56 | params[input_name] = new_input 57 | return params 58 | 59 | elif isinstance(data_set, list): 60 | # print data_set 61 | for i in data_set: 62 | inputs = walk_inputs(i, params, parent_key, list) 63 | if parent_key in params.keys(): 64 | if inputs: 65 | params[parent_key].append(inputs) 66 | 67 | return params 68 | 69 | walk_inputs(input_description, call_params, type_=dict) 70 | 71 | return call_params 72 | 73 | 74 | def generate_input(self, input_type=None, resource_name=None, value=None): 75 | 76 | # Check if it's a list 77 | if value: 78 | result = value 79 | return result 80 | 81 | is_list = False 82 | if input_type.startswith('list_'): 83 | is_list = True 84 | input_type = input_type[5:] 85 | 86 | if is_list: 87 | result = [] 88 | for i in xrange(0, random.randint(0, 5)): 89 | result.append(self.generate(input_type, resource_name)) 90 | else: 91 | result = self.generate(input_type, resource_name) 92 | return result 93 | 94 | def generate(self, input_type, resource_name=None): 95 | if 'hex-' in input_type: 96 | size = int(input_type.replace('hex-',"")) 97 | 98 | generator = self.__getattribute__("gen_hex") 99 | return generator(size) 100 | 101 | elif 'int-' in input_type: 102 | size = int(input_type.replace('int-',"")) 103 | generator = self.__getattribute__("gen_int") 104 | return generator(size) 105 | 106 | generator = self.__getattribute__("gen_%s" % input_type) 107 | 108 | return generator() 109 | 110 | def gen_hex(self, size): 111 | hex = os.urandom(size/2).encode('hex') 112 | return hex[:size] 113 | 114 | def gen_int(self, size): 115 | def random_range(n): 116 | range_start = 10**(n-1) 117 | range_end = (10**n)-1 118 | return random.randint(range_start, range_end) 119 | return random_range(size) 120 | 121 | def gen_unicode(self): 122 | chunk = [] 123 | for i in xrange(random.randint(1, 128)): 124 | chunk.append(struct.pack("f", random.random())) 125 | return unicode("".join(chunk), errors='ignore') 126 | 127 | def gen_ascii(self): 128 | return ''.join(random.choice(string.letters + string.digits) for _ in range(random.randint(1, 512))) 129 | 130 | def gen_string(self): 131 | if random.randint(0,1): 132 | return self.gen_unicode() 133 | 134 | if random.randint(0,1): 135 | return self.gen_ascii() 136 | 137 | return random.choice(constants.PAYLOADS) 138 | 139 | def gen_email(self): 140 | return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))+"@gmail.com" 141 | 142 | def gen_username(self): 143 | return ''.join(random.choice(string.ascii_lowercase + string.digits+"_") for _ in range(10)) 144 | 145 | def gen_latlng(self, seprator=","): 146 | return str(random.uniform(-180,180))+seprator+str(random.uniform(-90, 90)) 147 | 148 | def gen_uuid(self): 149 | return str(uuid.uuid1()) -------------------------------------------------------------------------------- /app/helpers/helper.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/helpers/helper.pyc -------------------------------------------------------------------------------- /app/helpers/utils.py: -------------------------------------------------------------------------------- 1 | # @ant4g0nist 2 | 3 | class TerminalColors: 4 | '''Simple terminal colors class''' 5 | def __init__(self, enabled=True): 6 | # TODO: discover terminal type from "file" and disable if 7 | # it can't handle the color codes 8 | self.enabled = enabled 9 | 10 | def reset(self): 11 | '''Reset all terminal colors and formatting.''' 12 | if self.enabled: 13 | return "\x1b[0m" 14 | return '' 15 | 16 | def bold(self, on=True): 17 | '''Enable or disable bold depending on the "on" parameter.''' 18 | if self.enabled: 19 | return "\x1b[1m" if on else "\x1b[22m" 20 | return '' 21 | 22 | def italics(self, on=True): 23 | '''Enable or disable italics depending on the "on" parameter.''' 24 | if self.enabled: 25 | return "\x1b[3m" if on else "\x1b[23m" 26 | return '' 27 | 28 | def underline(self, on=True): 29 | '''Enable or disable underline depending on the "on" parameter.''' 30 | if self.enabled: 31 | return "\x1b[4m" if on else "\x1b[24m" 32 | return '' 33 | 34 | def inverse(self, on=True): 35 | '''Enable or disable inverse depending on the "on" parameter.''' 36 | if self.enabled: 37 | return "\x1b[7m" if on else "\x1b[27m" 38 | return '' 39 | 40 | def strike(self, on=True): 41 | '''Enable or disable strike through depending on the "on" parameter.''' 42 | if self.enabled: 43 | return "\x1b[9m" if on else "\x1b[29m" 44 | return '' 45 | 46 | def black(self, fg=True): 47 | '''Set the foreground or background color to black. 48 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 49 | if self.enabled: 50 | return "\x1b[30m" if fg else "\x1b[40m" 51 | return '' 52 | 53 | def red(self, fg=True): 54 | '''Set the foreground or background color to red. 55 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 56 | if self.enabled: 57 | return "\x1b[31m" if fg else "\x1b[41m" 58 | return '' 59 | 60 | def green(self, fg=True): 61 | '''Set the foreground or background color to green. 62 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 63 | if self.enabled: 64 | return "\x1b[32m" if fg else "\x1b[42m" 65 | return '' 66 | 67 | def yellow(self, fg=True): 68 | '''Set the foreground or background color to yellow. 69 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 70 | if self.enabled: 71 | return "\x1b[43m" if fg else "\x1b[33m" 72 | return '' 73 | 74 | def blue(self, fg=True): 75 | '''Set the foreground or background color to blue. 76 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 77 | if self.enabled: 78 | return "\x1b[34m" if fg else "\x1b[44m" 79 | return '' 80 | 81 | def magenta(self, fg=True): 82 | '''Set the foreground or background color to magenta. 83 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 84 | if self.enabled: 85 | return "\x1b[35m" if fg else "\x1b[45m" 86 | return '' 87 | 88 | def cyan(self, fg=True): 89 | '''Set the foreground or background color to cyan. 90 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 91 | if self.enabled: 92 | return "\x1b[36m" if fg else "\x1b[46m"; 93 | return '' 94 | 95 | def white(self, fg=True): 96 | '''Set the foreground or background color to white. 97 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 98 | if self.enabled: 99 | return "\x1b[37m" if fg else "\x1b[47m" 100 | return '' 101 | 102 | def default(self, fg=True): 103 | '''Set the foreground or background color to the default. 104 | The foreground color will be set if "fg" tests True. The background color will be set if "fg" tests False.''' 105 | if self.enabled: 106 | return "\x1b[39m" if fg else "\x1b[49m" 107 | return '' 108 | -------------------------------------------------------------------------------- /app/helpers/utils.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/helpers/utils.pyc -------------------------------------------------------------------------------- /app/models/Scan.py: -------------------------------------------------------------------------------- 1 | from mongoengine import * 2 | from ..env.config import db 3 | # from User import User 4 | 5 | class APIScan(Document): 6 | api = StringField(required=True) 7 | headers = StringField(required=True) 8 | method = StringField(required=True) 9 | parameters = StringField(required=True) 10 | parameter = StringField() 11 | vulnerability = StringField(required=True) 12 | scanId = StringField(required=True) 13 | updatedTime = StringField() 14 | 15 | #should get reports from here 16 | class APIScanResults(Document): 17 | apiscans = ListField(ReferenceField(APIScan)) 18 | scanId = StringField(required=True) 19 | hash = StringField(required=True) 20 | updatedTime = StringField() 21 | config = StringField() 22 | -------------------------------------------------------------------------------- /app/models/Scan.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/models/Scan.pyc -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/models/__init__.py -------------------------------------------------------------------------------- /app/models/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/app/models/__init__.pyc -------------------------------------------------------------------------------- /db/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant4g0nist/Susanoo/f629138fafa3cd7f78a7fb7d3a838c668cf47dd2/db/.gitkeep -------------------------------------------------------------------------------- /example/sample.yaml: -------------------------------------------------------------------------------- 1 | host: 'https://api.example.com' 2 | 3 | # mention the header name that will be used to verify Authorization bugs 4 | UserA_Authorization: 5 | - name: 'Authorization' 6 | value: 'hello e165ecd8c8fd8b34baf2d46ed8517a0e17c543759940dc155cd2f76ed086fd93' 7 | 8 | UserB_Authorization: 9 | - name: 'Authorization' 10 | value: 'hello 50e03bcd0e9ef03883df7519b7fdcf366c2e82ada334dffc5f88b9ecd947ace1' 11 | 12 | # headers for all the api calls 13 | headers: 14 | - name: 'Authorization' 15 | value: 'hello %(auth)' 16 | inputs: 17 | auth: {'type':'hex-64', 'required': True} 18 | 19 | - name: 'X-UniqueId' 20 | value: '%(uniqueId) %(blahId)' 21 | inputs: 22 | uniqueId: {'type':'hex-16', 'required': True} 23 | blahId: {'type':'hex-64', 'required': True} 24 | 25 | # api calls 26 | apis: 27 | - name: 'url_parameters_example' 28 | api: '/sample/url/that/takes/url/parameters?location=%(latlong)' 29 | method: 'GET' 30 | inputs: 31 | url_input: 32 | latlong: {'type':'latlng','required': True} 33 | 34 | - name: 'post_parameters_example' 35 | api: '/post/parameters/example' 36 | method: 'POST' 37 | inputs: 38 | dob: {'required': True, 'type': 'string'} 39 | email: {'required': True, 'type': 'email'} 40 | first_name: {'required': True, 'type': 'string'} 41 | gender: {'required': True, 'type': 'string'} 42 | last_name: {'required': True, 'type': 'string'} 43 | details: 44 | phone: {'required': True, 'type':'int-10'} 45 | country: {'required': True, 'type':'int-2'} 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/smoke.yaml: -------------------------------------------------------------------------------- 1 | host: 'https://api.example.com' 2 | 3 | UserA_Authorization: 4 | - name: 'Authorization' 5 | value: 'hello e165ecd8c8fd8b34baf2d46ed8517a0e17c543759940dc155cd2f76ed086fd93' 6 | 7 | UserB_Authorization: 8 | - name: 'Authorization' 9 | value: 'hello 50e03bcd0e9ef03883df7519b7fdcf366c2e82ada334dffc5f88b9ecd947ace1' 10 | 11 | # headers for all the api calls 12 | headers: 13 | - name: 'Authorization' 14 | value: 'hello %(auth)' 15 | inputs: 16 | auth: {'type':'hex-64', 'required': True} 17 | 18 | - name: 'X-UniqueId' 19 | value: '%(uniqueId) %(blahId)' 20 | inputs: 21 | uniqueId: {'type':'hex-16', 'required': True} 22 | blahId: {'type':'hex-64', 'required': True} 23 | 24 | # api calls 25 | apis: 26 | - name: 'sample_request_1' 27 | api: '/sample/request/1' 28 | method: 'GET' 29 | status_code: 200 30 | outputs: 31 | message: {'json_extract':'lambda x: x["message"]', 'value': "well should be present"} #returns true if given value is present in response 32 | 33 | - name: 'sample_request_2' 34 | api: '/sample/request/2' 35 | method: 'GET' 36 | status_code: 401 37 | outputs: 38 | message: {'list_extract':'lambda x: x["name"]', 'value': "well should be present"} #returns true if given value is present in response 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=4.2b1 2 | requests>=2.20.0 3 | mongoengine==0.11.0 4 | -------------------------------------------------------------------------------- /susanoo.py: -------------------------------------------------------------------------------- 1 | # @ant4g0nist 2 | 3 | import os 4 | import sys 5 | import gzip 6 | import time 7 | import argparse 8 | from app.helpers.utils import TerminalColors 9 | from app.controllers import api as APIController 10 | from app.controllers import smoke as SmokeController 11 | 12 | def banner(): 13 | ban =""" 14 | ,--. ,----.. ,----.. 15 | .--.--. .--.--. ,---, ,--.'| / / \ / / \ 16 | / / '. ,--, / / '. ' .' \ ,--,: : | / . : / . : 17 | | : /`. / ,'_ /|| : /`. / / ; '. ,`--.'`| ' : . / ;. \ . / ;. \ 18 | ; | |--` .--. | | :; | |--` : : \ | : : | |. ; / ` ;. ; / ` ; 19 | | : ;_ ,'_ /| : . || : ;_ : | /\ \ : | \ | :; | ; \ ; |; | ; \ ; | 20 | \ \ `. | ' | | . . \ \ `. | : ' ;. : | : ' '; || : | ; | '| : | ; | ' 21 | `----. \| | ' | | | `----. \| | ;/ \ \' ' ;. ;. | ' ' ' :. | ' ' ' : 22 | __ \ \ |: | | : ' ; __ \ \ |' : | \ \ ,'| | | \ |' ; \; / |' ; \; / | 23 | / /`--' /| ; ' | | ' / /`--' /| | ' '--' ' : | ; .' \ \ ', / \ \ ', / 24 | '--'. / : | : ; ; |'--'. / | : : | | '`--' ; : / ; : / 25 | `--'---' ' : `--' \ `--'---' | | ,' ' : | \ \ .' \ \ .' 26 | : , .-./ `--'' ; |.' `---` `---` 27 | `--`----' '---' 28 | 29 | """ 30 | print ban 31 | 32 | 33 | if __name__ == '__main__': 34 | 35 | banner() 36 | parser = argparse.ArgumentParser() 37 | parser.add_argument("-c","--config", help="run api security scan on given config file", required=True) 38 | parser.add_argument("-s","--smoke", help="run smoke scan on the given config file?True/False", action='store_true', default=False) 39 | args = parser.parse_args() 40 | 41 | tty_colors = TerminalColors(True) 42 | 43 | if not os.path.exists(args.config): 44 | print tty_colors.red()+'Make sure config file exists'+tty_colors.default() 45 | sys.exit(0) 46 | 47 | if args.smoke: 48 | smokeSusanoo = SmokeController.SusanooConfig(args.config) 49 | apis = smokeSusanoo.get_apis() 50 | 51 | for api in apis: 52 | print tty_colors.cyan()+"testing %s"%(api)+tty_colors.default() 53 | 54 | 55 | scan = SmokeController.Scans(apis[api], smokeSusanoo) 56 | scan.run() 57 | 58 | print tty_colors.cyan()+"*"*100+tty_colors.default() 59 | 60 | if not args.smoke: 61 | susanoo = APIController.SusanooConfig(args.config) 62 | apis = susanoo.get_apis() 63 | 64 | for api in apis: 65 | 66 | name = api 67 | scan = APIController.Scans(apis[api], susanoo) 68 | 69 | print tty_colors.cyan()+"*"*100+tty_colors.default() 70 | 71 | scan.authentication_check() 72 | 73 | scan.authorization_check() 74 | 75 | scan.sqlinjection_heuristic_check() 76 | 77 | if apis[api].raw_url: 78 | scan.url_param_fuzz() 79 | 80 | scan.fuzz() 81 | # scan.ratelimit_check() 82 | print tty_colors.cyan()+"*"*100+tty_colors.default() 83 | --------------------------------------------------------------------------------