├── .gitignore ├── README.md ├── index.html ├── lib ├── __init__.py └── common.py ├── screenshot.png ├── screenshot2.png ├── static ├── swagger-ui-bundle.js ├── swagger-ui-standalone-preset.js ├── swagger-ui.css └── validator └── swagger-exp.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | chromeSwagger/ 3 | api_summary.txt 4 | api-docs.json 5 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Swagger API Exploit 1.2** 2 | 3 | 这是一个 Swagger REST API 信息泄露利用工具。 主要功能有: 4 | 5 | * 遍历所有API接口,自动填充参数 6 | * 尝试 GET / POST 所有接口,返回 Response Code / Content-Type / Content-Length ,用于分析接口是否可以未授权访问利用 7 | * 分析接口是否存在敏感参数,例如url参数,容易引入外网的SSRF漏洞 8 | * 检测 API认证绕过漏洞 9 | * 在本地监听一个Web Server,打开Swagger UI界面,供分析接口使用 10 | * 使用Chrome打开本地Web服务器,并禁用CORS,解决部分API接口无法跨域请求的问题 11 | * 当工具检测到HTTP认证绕过漏洞时,本地服务器拦截API文档,修改path,以便直接在Swagger UI中进行测试 12 | 13 | ## ChangeLog 14 | * [2024-06-07] 增加支持 OpenAPI 3.0 15 | * [2022-08-08] Fix chromeSwagger permission error 16 | * [2021-04-04] 支持 Python3 17 | 18 | ## 扫描器改进建议 19 | 20 | * 分析json文档,将发现的URL,自动添加到爬虫中 21 | 22 | ## Usage 23 | 24 | 需要介入分析 api_summary.txt 文件中的内容 25 | 26 | * 扫描所有API集,打开Swagger UI 27 | 28 | > python swagger-exp.py http://site.com/swagger-resources/ 29 | 30 | * 扫描一个API集,打开Swagger UI 31 | 32 | > python swagger-exp.py http://site.com/v2/api-docs 33 | 34 | * 只打开Swagger UI,不扫描接口 35 | 36 | > python swagger-exp.py 37 | 38 | ## 工具截图 39 | 40 | ![](screenshot.png) 41 | 42 | ![](screenshot2.png) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Swagger API Exploit 8 | 9 | 27 | 28 | 29 | 30 | 31 |

Swagger API Exploit

32 |
33 |
34 |
35 | 36 |
37 |
38 |
40 | 41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |

Swagger Petstore 82 |
 
 83 |                                             1.0.5
 84 |                                             
85 |

86 |
[ Base URL: 
 87 |                                         petstore.swagger.io
 88 |                                         /v2 ]
 89 |                                         
90 | 92 | https://petstore.swagger.io/v2/swagger.json 93 |
94 |
95 |

This is a sample server Petstore server. You can find out 96 | more about Swagger at http://swagger.io or on 98 | irc.freenode.net, 99 | #swagger. For this sample, you can use the api key 100 | special-key to test the authorization filters.

101 |
102 |
103 | 106 | 108 |
Apache 2.0 111 |
112 | Find out more about Swagger
114 |
115 |
116 |
117 |
118 |
122 |
123 | 128 |
129 |
130 |
131 |
132 |
133 |
134 |

pet

Everything about your Pets

139 |
Find out more: 140 | http://swagger.io

POST​/pet​/{petId}​/uploadImage
uploads an image
156 |
POST​/pet
Add a new pet to the store
166 |
PUT​/pet
Update an existing pet
176 |
GET​/pet​/findByStatus
Finds Pets by status
186 |
GET​/pet​/findByTags
Finds Pets by tags
196 |
GET​/pet​/{petId}
Find pet by ID
207 |
POST​/pet​/{petId}
Updates a pet in the store with form data
217 |
DELETE​/pet​/{petId}
Deletes a pet
226 |
227 |

store

Access to Petstore orders

232 |

POST​/store​/order
Place an order for a pet
243 |
GET​/store​/order​/{orderId}
Find purchase order by ID
250 |
DELETE​/store​/order​/{orderId}
Delete purchase order by ID
256 |
GET​/store​/inventory
Returns pet inventories by status
266 |
267 |

user

Operations about user

272 |
Find out more about our store 273 | : http://swagger.io

POST​/user​/createWithArray
Creates list of users with given input array
287 |
POST​/user​/createWithList
Creates list of users with given input array
295 |
GET​/user​/{username}
Get user by user name
PUT​/user​/{username}
Updated user
DELETE​/user​/{username}
Delete user
GET​/user​/login
Logs user into the system
320 |
GET​/user​/logout
Logs out current logged in user session
327 |
POST​/user
Create user
334 |
335 |
336 |
337 |
338 |
339 |
340 |

Models 341 | 342 | 343 | 344 |

345 |
346 |
ApiResponse
352 |
Category
358 |
Pet
364 |
Tag
370 |
Order
376 |
User
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
Online validator badge
392 |
393 |
394 |
395 | 396 | 397 | 398 | 424 | 425 | 426 | 427 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/swagger-exp/80c90d9c81f18ec72b03049cadf9651da4efadff/lib/__init__.py -------------------------------------------------------------------------------- /lib/common.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | import os 4 | import platform 5 | 6 | 7 | def get_chrome_path_win(): 8 | try: 9 | import _winreg as reg 10 | except Exception as e: 11 | import winreg as reg 12 | # HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe\ 13 | conn = reg.ConnectRegistry(None, reg.HKEY_LOCAL_MACHINE) 14 | _path = reg.QueryValue(conn, 'Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe\\') 15 | reg.CloseKey(conn) 16 | if not os.path.exists(_path): 17 | raise Exception('chrome.exe not found.') 18 | return _path 19 | 20 | 21 | def get_chrome_path_linux(): 22 | folders = ['/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin', '/bin', '/opt/google/', 23 | '/Applications/Google Chrome.app/Contents/MacOS'] 24 | names = ['google-chrome', 'chrome', 'chromium', 'chromium-browser', 'Google Chrome'] 25 | for folder in folders: 26 | for name in names: 27 | if os.path.exists(os.path.join(folder, name)): 28 | return os.path.join(folder, name) 29 | 30 | 31 | def get_chrome_path(): 32 | if platform.system() == 'Windows': 33 | return get_chrome_path_win() 34 | else: 35 | return get_chrome_path_linux() 36 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/swagger-exp/80c90d9c81f18ec72b03049cadf9651da4efadff/screenshot.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/swagger-exp/80c90d9c81f18ec72b03049cadf9651da4efadff/screenshot2.png -------------------------------------------------------------------------------- /static/validator: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/swagger-exp/80c90d9c81f18ec72b03049cadf9651da4efadff/static/validator -------------------------------------------------------------------------------- /swagger-exp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # Swagger REST API Exploit 4 | # By LiJieJie my[at]lijiejie.com 5 | 6 | import sys 7 | import requests 8 | import json 9 | import time 10 | import codecs 11 | import subprocess 12 | import threading 13 | import copy 14 | import os 15 | from lib.common import get_chrome_path 16 | try: 17 | import urlparse 18 | from SocketServer import ThreadingMixIn 19 | from SimpleHTTPServer import SimpleHTTPRequestHandler 20 | from BaseHTTPServer import HTTPServer 21 | except Exception as e: 22 | import urllib 23 | from socketserver import ThreadingMixIn 24 | from http.server import SimpleHTTPRequestHandler, HTTPServer 25 | 26 | 27 | requests.packages.urllib3.disable_warnings() 28 | api_set_list = [] # ALL API SET 29 | scheme = 'http' # default value 30 | headers = {'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'} 31 | auth_bypass_detected = False 32 | 33 | 34 | def print_msg(msg): 35 | if msg.startswith('[GET] ') or msg.startswith('[POST] ') or msg.startswith('[PUT] '): 36 | out_file.write('\n') 37 | _msg = '[%s] %s' % (time.strftime('%H:%M:%S', time.localtime()), msg) 38 | print(_msg) 39 | out_file.write(_msg + '\n') 40 | 41 | 42 | def find_all_api_set(start_url): 43 | try: 44 | text = requests.get(start_url, headers=headers, verify=False).text 45 | json_doc = json.loads(text) 46 | if text.strip().startswith('{"swagger":"') or "openapi" in json_doc or "swagger" in json_doc: 47 | # from swagger.json 48 | api_set_list.append(start_url) 49 | print_msg('[OK] [API set] %s' % start_url) 50 | with codecs.open('api-docs.json', 'w', encoding='utf-8') as f: 51 | f.write(text) 52 | elif text.find('"swaggerVersion"') > 0: # from /swagger-resources/ 53 | base_url = start_url[:start_url.find('/swagger-resources')] 54 | for item in json_doc: 55 | url = base_url + item['location'] 56 | find_all_api_set(url) 57 | else: 58 | print_msg('[FAIL] Invalid API Doc: %s' % start_url) 59 | except Exception as e: 60 | print_msg('[find_all_api_set] process error %s' % str(e)) 61 | 62 | 63 | def process_doc(url): 64 | try: 65 | json_doc = requests.get(url, headers=headers, verify=False).json() 66 | if 'host' in json_doc: 67 | base_url = scheme + '://' + json_doc['host'] + json_doc['basePath'] 68 | elif 'servers' in json_doc: 69 | server_url = json_doc['servers'][0]['url'] 70 | if server_url.lower().startswith('http'): 71 | base_url = server_url 72 | else: 73 | base_url = scheme + '://' + urlparse.urlparse(url, 'http').netloc + server_url 74 | # update base_url to absolute url 75 | json_doc['servers'][0]['url'] = base_url 76 | with codecs.open('api-docs.json', 'w', encoding='utf-8') as f: 77 | f.write(json.dumps(json_doc, indent=4)) 78 | else: 79 | base_url = scheme + '://' + urlparse.urlparse(url, 'http').netloc 80 | base_url = base_url.rstrip('/') 81 | for path in json_doc['paths']: 82 | 83 | for method in json_doc['paths'][path]: 84 | if method.upper() not in ['GET', 'POST', 'PUT']: 85 | continue 86 | 87 | params_str = '' 88 | sensitive_words = ['url', 'path', 'uri'] 89 | sensitive_params = [] 90 | if 'parameters' in json_doc['paths'][path][method]: 91 | parameters = json_doc['paths'][path][method]['parameters'] 92 | 93 | for parameter in parameters: 94 | para_name = parameter['name'] 95 | # mark sensitive parma 96 | for word in sensitive_words: 97 | if para_name.lower().find(word) >= 0: 98 | sensitive_params.append(para_name) 99 | break 100 | 101 | if 'format' in parameter: 102 | para_format = parameter['format'] 103 | elif 'schema' in parameter and 'format' in parameter['schema']: 104 | para_format = parameter['schema']['format'] 105 | elif 'schema' in parameter and 'type' in parameter['schema']: 106 | para_format = parameter['schema']['type'] 107 | elif 'schema' in parameter and '$ref' in parameter['schema']: 108 | para_format = parameter['schema']['$ref'] 109 | para_format = para_format.replace('#/definitions/', '') 110 | para_format = '{OBJECT_%s}' % para_format 111 | else: 112 | para_format = parameter['type'] if 'type' in parameter else 'unkonwn' 113 | 114 | is_required = '' if 'required' in parameter and parameter['required'] else '*' 115 | params_str += '&%s=%s%s%s' % (para_name, is_required, para_format, is_required) 116 | params_str = params_str.strip('&') 117 | if sensitive_params: 118 | print_msg('[*] Possible vulnerable param found: %s, path is %s' % ( 119 | sensitive_params, base_url+path)) 120 | 121 | scan_api(method, base_url, path, params_str) 122 | except Exception as e: 123 | import traceback 124 | traceback.print_exc() 125 | print_msg('[process_doc error][%s] %s' % (url, e)) 126 | 127 | 128 | def scan_api(method, base_url, path, params_str, error_code=None): 129 | # place holder 130 | _params_str = params_str.replace('*string*', 'a') 131 | _params_str = _params_str.replace('*int64*', '1') 132 | _params_str = _params_str.replace('*int32*', '1') 133 | _params_str = _params_str.replace('=string', '=test') 134 | api_url = base_url + path 135 | if not error_code: 136 | print_msg('[%s] %s %s' % (method.upper(), api_url, params_str)) 137 | if method.upper() == 'GET': 138 | r = requests.get(api_url + '?' + _params_str, headers=headers, verify=False) 139 | if not error_code: 140 | print_msg('[Request] %s %s' % (method.upper(), api_url + '?' + _params_str)) 141 | else: 142 | r = requests.post(api_url, data=_params_str, headers=headers, verify=False) 143 | if not error_code: 144 | print_msg('[Request] %s %s \n%s' % (method.upper(), api_url, _params_str)) 145 | 146 | content_type = r.headers['content-type'] if 'content-type' in r.headers else '' 147 | content_length = r.headers['content-length'] if 'content-length' in r.headers else '' 148 | if not content_length: 149 | content_length = len(r.content) 150 | if not error_code: 151 | print_msg('[Response] Code: %s Content-Type: %s Content-Length: %s' % ( 152 | r.status_code, content_type, content_length)) 153 | else: 154 | if r.status_code not in [401, 403] or r.status_code != error_code: 155 | global auth_bypass_detected 156 | auth_bypass_detected = True 157 | print_msg('[VUL] *** URL Auth Bypass ***') 158 | if method.upper() == 'GET': 159 | print_msg('[Request] [%s] %s' % (method.upper(), api_url + '?' + _params_str)) 160 | else: 161 | print_msg('[Request] [%s] %s \n%s' % (method.upper(), api_url, _params_str)) 162 | 163 | # Auth Bypass Test 164 | if not error_code and r.status_code in [401, 403]: 165 | path = '/' + path 166 | scan_api(method, base_url, path, params_str, error_code=r.status_code) 167 | 168 | 169 | class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): 170 | pass 171 | 172 | 173 | class RequestHandler(SimpleHTTPRequestHandler): 174 | def do_GET(self): 175 | if self.path.startswith('/proxy?url='): 176 | url = self.path[11:] 177 | if url.lower().startswith('http') and url.find('@') < 0: 178 | text = requests.get(url, headers=headers, verify=False).content 179 | if text.find('"schemes":[') < 0: 180 | text = text[0] + '"schemes":["https","http"],' + text[1:] # HTTP(s) Switch 181 | global auth_bypass_detected 182 | if auth_bypass_detected: 183 | json_doc = json.loads(text) 184 | paths = copy.deepcopy(json_doc['paths'].keys()) 185 | for path in paths: 186 | json_doc['paths']['/' + path] = json_doc['paths'][path] 187 | del json_doc['paths'][path] 188 | 189 | text = json.dumps(json_doc) 190 | else: 191 | text = 'Request Error' 192 | self.send_response(200) 193 | self.send_header("Content-type", "application/json") 194 | self.send_header("Content-length", len(text)) 195 | self.end_headers() 196 | self.wfile.write(text) 197 | 198 | return SimpleHTTPRequestHandler.do_GET(self) 199 | 200 | 201 | def chrome_open(chrome_path, url, server): 202 | time.sleep(2.0) 203 | if chrome_path: 204 | url_txt = url + '/api_summary.txt' if len(sys.argv) > 1 else '' 205 | cwd = os.path.split(os.path.abspath(__file__))[0] 206 | user_data_dir = os.path.abspath(os.path.join(cwd, 'chromeSwagger')) 207 | cmd = '"%s" --disable-web-security --no-sandbox --new-window--disable-gpu ' \ 208 | '--user-data-dir=%s %s %s' % (chrome_path, user_data_dir, url, url_txt) 209 | p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 210 | while p.poll() is None: 211 | time.sleep(1.0) 212 | 213 | server.shutdown() 214 | print_msg('Server shutdown.') 215 | if out_file: 216 | out_file.flush() 217 | out_file.close() 218 | 219 | 220 | if __name__ == '__main__': 221 | out_file = codecs.open('api_summary.txt', 'w', encoding='utf-8') 222 | if len(sys.argv) > 1: 223 | try: 224 | _scheme = urlparse.urlparse(sys.argv[1]).scheme.lower() 225 | except Exception as e: 226 | _scheme = urllib.parse.urlparse(sys.argv[1]).scheme.lower() 227 | if _scheme.lower() == 'https': 228 | scheme = 'https' 229 | find_all_api_set(sys.argv[1]) 230 | for url in api_set_list: 231 | process_doc(url) 232 | 233 | server = ThreadingSimpleServer(('127.0.0.1', 0), RequestHandler) 234 | url = 'http://127.0.0.1:%s' % server.server_port 235 | print_msg('Swagger UI Server on: %s' % url) 236 | chrome_path = get_chrome_path() 237 | if not chrome_path: 238 | print_msg('[ERROR] Chrome executable not found') 239 | else: 240 | print_msg('Open Swagger UI with chrome') 241 | threading.Thread(target=chrome_open, args=(chrome_path, url, server)).start() 242 | 243 | server.serve_forever() 244 | --------------------------------------------------------------------------------