├── README.md ├── springboot_path.txt ├── springboot_scan.py └── springboot_target.txt /README.md: -------------------------------------------------------------------------------- 1 | # 框架扩容 2 | 3 | 请移步winezer0/DynaScan [ https://github.com/winezer0/dynascan], 实现动态字典渲染、动态结果判断、自动命中记录、的敏感文件扫描器 4 | 5 | winezer0/DynaScan: 完美支持springboot静态与动态字典,并支持额外的扫描,。 6 | 7 | # springboot_scan 8 | 9 | Springboot directory scanning 10 | 11 | 注意:此工具暂时不能够直接用于不可访问的域名,没有添加对应的报错处理,请确认为springboot框架再使用。 12 | 13 | 14 | # 更新记录 15 | 16 | 20220317 更新版本为0.6.3 紧急修复一个参数传递BUG 17 | 18 | 20220316 更新版本为0.6.2 更新nacos配置字典 19 | 20 | # 项目由来 21 | 22 | 23 | 项目过程中发现现有的工具对springboot路径扫描大多存在误报和遗漏现象。 24 | 25 | 26 | 往往是存在以下几个现象: 27 | 28 | 29 | 1、请求频率过高时,服务器对于返回503等出错结果,此时无法准确判断访问页面是否为正常页面。 30 | 31 | 32 | 2、heapdump等大文件路径如果存在时,会使得扫描工具产生卡顿,从而无法继续扫描。 33 | 34 | 35 | 3、使用浏览器访问时,延迟加载页面成功,使用工具扫描时无法获取所有响应页面,导致漏报。 36 | 37 | 38 | 4、对于200的扫描结果无法判断,产生极大的误报。 39 | 40 | 41 | 42 | 尝试试用了大部分公开springboot目录扫描工具,发现都不可避免的产生以上问题, 43 | 44 | [Go]springScan 45 | [PY]SB-Actuator 46 | [PY]springboot-check 47 | [PY]SpringBootScan 48 | 49 | 50 | 51 | 在此种情况下,重新编写了一个适用于springboot的目录扫描工具。 52 | 53 | 54 | 55 | 目前支持以下功能: 56 | 57 | 58 | 1、使用多种方法【get、post、head】自动重试访问503页面和无结果页面。 59 | 60 | 61 | 2、使用多种关键数据【长度、大小、头部比特】用于自动过滤和辅助手动过滤非404的非正常页面。 62 | 63 | 64 | 3、支持多种方式代理【socks5、https】请求页面代理用于调试和绕过请求限制。 65 | 66 | 67 | 4、使用多个文件记录不同情景下过滤的URL,便于追踪产生的错误和漏报。 68 | 69 | 过程及结果文件 默认输出在当前【result-时间戳】目录下, 70 | 其中 scan_waive.txt 存放基于404、403、500状态码 过滤的URL。 (waive 放弃) 71 | 其中 scan_filter.txt 存放基于【长度、大小、头部比特】过滤的URL。(filter 过滤 72 | 其中 scan_retry.txt 存放根据请求结果自动重试的URL和对应重试次数。(retry 重试) 73 | 74 | 75 | 其中 scan_result.txt 存放状态码为200,并且不被过滤的URL,此文件为实际结果文件。 (result 结果) 76 | 其中 scan_manual.txt 存放当重试多次依然无法判断为正常请求时的URL,此文件结果需用户进行手动重试。(manual 手动) 77 | 78 | 79 | 80 | 5、通过fofa批量采集了2000站点的mapping路径加入字典文件。 81 | 82 | 83 | 84 | # 快速使用 85 | 86 | 1、将目标URL填写在springboot_target.txt,并运行 python3 springboot_scan.py 87 | 88 | 89 | # TODO: 90 | 91 | 92 | 1、对可能存在漏洞的请求URL进行提示。(极小概率) 93 | 94 | 95 | 2、对敏感的响应内容进行提示。(极小概率,建议使用HAE插件替代) 96 | 97 | 3、美化代码整体结构 【极大可能】 98 | 99 | -------------------------------------------------------------------------------- /springboot_path.txt: -------------------------------------------------------------------------------- 1 | / 2 | /#/wallboard 3 | /%20/swagger-ui.html 4 | /Swagger/ui/index 5 | /acl/article?id=66 6 | /acm 7 | /actuator 8 | /actuator/#/wallboard 9 | /actuator/acm 10 | /actuator/admin/swagger-ui.html 11 | /actuator/api-docs 12 | /actuator/api.html 13 | /actuator/api/index.html 14 | /actuator/api/swagger-ui.html 15 | /actuator/api/v2/api-docs 16 | /actuator/api/v2/swagger.json 17 | /actuator/archaius 18 | /actuator/article?id=${7*7} 19 | /actuator/article?id=66 20 | /actuator/auditLog 21 | /actuator/auditevents 22 | /actuator/auditevents/actuator/intergrationgraph 23 | /actuator/autoconfig 24 | /actuator/beans 25 | /actuator/beans/actuator/jolokia 26 | /actuator/beans1 27 | /actuator/caches 28 | /actuator/caches/actuator/refresh 29 | /actuator/caches/cache 30 | /actuator/channels 31 | /actuator/conditions 32 | /actuator/conditions/actuator/jolokia/list 33 | /actuator/conditions1 34 | /actuator/configprops 35 | /actuator/configurationMetadata 36 | /actuator/distv2/index.html 37 | /actuator/docs 38 | /actuator/druid/login.html 39 | /actuator/dubbo-provider/distv2/index.html 40 | /actuator/dump 41 | /actuator/env 42 | /actuator/env/actuator/liquibase 43 | /actuator/env/java.home 44 | /actuator/env/spring.jmx.enabled 45 | /actuator/env/system 46 | /actuator/events 47 | /actuator/exportRegisteredServices 48 | /actuator/features 49 | /actuator/features/actuator/peripheral/swagger-ui.html 50 | /actuator/flyway 51 | /actuator/gateway 52 | /gateway 53 | /actuator/h2-console 54 | /actuator/health 55 | /actuator/health/ 56 | /actuator/health/actuator/loggers 57 | /actuator/healthcheck 58 | /actuator/heapdump 59 | /actuator/httptrace 60 | /actuator/httptrace/actuator/mappings 61 | /actuator/hystrix.stream 62 | /actuator/hystrix.stream/*/actuator/swagger 63 | /actuator/info 64 | /actuator/info/actuator/metrics 65 | /actuator/integrationgraph 66 | /actuator/intergrationgraph 67 | /actuator/jolokia 68 | /actuator/jolokia/*/actuator/static/swagger.json 69 | /actuator/jolokia/list 70 | /actuator/liquibase 71 | /actuator/logfile 72 | /actuator/logfile/actuator/sw/swagger-ui.html 73 | /actuator/loggers 74 | /actuator/loggers/ 75 | 76 | /actuator/loggingConfig 77 | /actuator/management/heapdump 78 | /actuator/mappings 79 | /actuator/mappings 80 | /actuator/mappings/actuator/monitor/conditions 81 | /actuator/metrics 82 | /actuator/metrics 83 | /actuator/metrics/ 84 | /actuator/metrics/actuator/monitor/env 85 | /actuator/monitor/auditevents 86 | /actuator/monitor/conditions 87 | /actuator/monitor/env 88 | /actuator/monitor/loggers 89 | /actuator/monitor/mappings 90 | /actuator/monitor/scheduledtasks 91 | /actuator/monitor/threaddump 92 | /actuator/peripheral/swagger-ui.html 93 | /actuator/peripheral/v2/api-docs 94 | /actuator/prometheus 95 | /actuator/prometheus/actuator/swagger-dubbo/api-docs 96 | /actuator/refresh 97 | /actuator/refresh/actuator/peripheral/v2/api-docs 98 | /actuator/registeredServices 99 | /actuator/releaseAttributes 100 | /actuator/resolveAttributes 101 | /actuator/restart 102 | /actuator/scheduledtasks 103 | /actuator/scheduledtasks/actuator/monitor/mappings 104 | /actuator/sentinel 105 | /actuator/service-registry/actuator/prometheus 106 | /actuator/sessions 107 | /actuator/sessions/ 108 | /actuator/sessions/actuator/swagger-ui.html 109 | /actuator/shutdown 110 | /actuator/spring-security-oauth-resource/swagger-ui.html 111 | /actuator/spring-security-rest/api/swagger-ui.html 112 | /actuator/springWebflow 113 | /actuator/sso 114 | /actuator/ssoSessions 115 | /actuator/static/swagger.json 116 | /actuator/statistics 117 | /actuator/status 118 | /actuator/sw/swagger-ui.html 119 | /actuator/swagger 120 | /actuator/swagger-dubbo/api-docs 121 | /actuator/swagger-resourcesce 122 | /actuator/swagger-ui 123 | /actuator/swagger-ui.html 124 | /actuator/swagger-ui/index.html 125 | /actuator/swagger/codes 126 | /actuator/swagger/index.html 127 | /actuator/swagger/static/index.html 128 | /actuator/system/ 129 | /actuator/system/env 130 | /actuator/system/mappings 131 | /actuator/system/showOsInfo 132 | /actuator/system/showProperties 133 | /actuator/template/swagger-ui.html 134 | /actuator/threaddump 135 | /actuator/threaddump/actuator/monitor/scheduledtasks 136 | /actuator/tra 137 | /actuator/trace 138 | /actuator/user/swagger-ui.html 139 | /admin/swagger-ui.html 140 | /api 141 | /api-docs 142 | /api-docs/swagger.json 143 | /api.html 144 | /api/api-docs 145 | /api/apidocs 146 | /api/doc 147 | /api/index.html 148 | /api/swagger 149 | /api/swagger-resources 150 | /api/swagger-ui 151 | /api/swagger-ui.html 152 | /api/swagger-ui.json 153 | /api/swagger.json 154 | /api/swagger/ 155 | /api/swagger/ui 156 | /api/swaggerui 157 | /api/v1/ 158 | /api/v1/api-docs 159 | /api/v1/apidocs 160 | /api/v1/login 161 | /api/v1/swagger 162 | /api/v1/swagger-resources 163 | /api/v1/swagger-ui 164 | /api/v1/swagger-ui.html 165 | /api/v1/swagger-ui.json 166 | /api/v1/swagger.json 167 | /api/v1/swagger/ 168 | /api/v2 169 | /api/v2/api-docs 170 | /api/v2/apidocs 171 | /api/v2/login 172 | /api/v2/swagger 173 | /api/v2/swagger-resources 174 | /api/v2/swagger-ui 175 | /api/v2/swagger-ui.html 176 | /api/v2/swagger-ui.json 177 | /api/v2/swagger.json 178 | /api/v2/swagger/ 179 | /api/v3 180 | /apidocs 181 | /apidocs/swagger.json 182 | /article?id=${7*7} 183 | /article?id=66 184 | /auditevents 185 | /autoconfig 186 | /beans 187 | /beans1 188 | /caches 189 | /channels 190 | /clients 191 | /clients/actuator/system/showOsInfo 192 | /clients/all/actuator/tra 193 | /clients/saveOrUpdate/actuator/trace 194 | /cloudfoundryapplication 195 | /conditions 196 | /conditions1 197 | /configprops 198 | /distv2/index.html 199 | /doc.html 200 | /docs 201 | /docs/ 202 | /druid/*/actuator/swagger/codes 203 | /druid/api.html 204 | /druid/basic.json 205 | /druid/datasource.html 206 | /druid/index.html 207 | /druid/login.html 208 | /druid/spring.html 209 | /druid/sql.html 210 | /druid/wall.html 211 | /druid/webapp.html 212 | /druid/websession.html 213 | /druid/weburi.html 214 | /dubbo-provider/distv2/index.html 215 | /dump 216 | /entity/all 217 | /env 218 | /env/ 219 | /env/(name) 220 | /env/java.home 221 | /env/spring 222 | /env/spring.jmx.enabled 223 | /env/{name} 224 | /error/actuator/monitor/threaddump 225 | /eureka 226 | /eureka/*/actuator/service-registry 227 | /features 228 | /flyway 229 | /gateway/actuator 230 | /gateway/actuator/auditevents 231 | /gateway/actuator/beans 232 | /gateway/actuator/conditions 233 | /gateway/actuator/configprops 234 | /gateway/actuator/env 235 | /gateway/actuator/health 236 | /gateway/actuator/heapdump 237 | /gateway/actuator/httptrace 238 | /gateway/actuator/hystrix.stream 239 | /gateway/actuator/info 240 | /gateway/actuator/jolokia 241 | /gateway/actuator/logfile 242 | /gateway/actuator/loggers 243 | /gateway/actuator/mappings 244 | /gateway/actuator/metrics 245 | /gateway/actuator/scheduledtasks 246 | /gateway/actuator/swagger-ui.html 247 | /gateway/actuator/threaddump 248 | /gateway/actuator/trace 249 | /get 250 | /graphql 251 | /h2-console 252 | /health 253 | /health/ 254 | /heapdump 255 | /heapdump.json 256 | /httptrace 257 | /hystrix 258 | /hystrix.stream 259 | /info 260 | /intergrationgraph 261 | /jolokia 262 | /jolokia/exec/org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager/getProperty/spring.datasource.password 263 | /jolokia/exec/org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager/getProperty/spring.datasource.url 264 | /jolokia/list 265 | /lastn/actuator/sessions 266 | /libs/swaggerui 267 | /liquibase 268 | /list 269 | /log/view?filename=/etc/passwd&base=../../../../../../../../../../ 270 | /log/view?filename=/windows/win.ini&base=../../../../../../../../../../ 271 | /log/view?filename=/windows/win.ini&base=../../../../../../../../../../ 272 | /logfile 273 | /loggers 274 | /login/admin/swagger-ui.html 275 | /manage/log/view?filename=/etc/passwd&base=../../../../../../../../../../ 276 | /manage/log/view?filename=/windows/win.ini&base=../../../../../../../../../../ 277 | /manage/log/view?filename=/windows/win.ini&base=../../../../../../../../../../ 278 | /management/heapdump 279 | /mappings 280 | /metrics 281 | /metrics/ 282 | /metrics/mem 283 | /metrics/{name} 284 | /monitor 285 | /monitor/auditevents 286 | /monitor/beans 287 | /monitor/conditions 288 | /monitor/configprops 289 | /monitor/env 290 | /monitor/health 291 | /monitor/heapdump 292 | /monitor/httptrace 293 | /monitor/hystrix.stream 294 | /monitor/info 295 | /monitor/jolokia 296 | /monitor/loggers 297 | /monitor/mappings 298 | /monitor/metrics 299 | /monitor/scheduledtasks 300 | /monitor/threaddump 301 | /oauth/authorize/actuator/swagger/index.html 302 | /oauth/check_token/actuator/swagger/static/index.html 303 | /oauth/client/token/api-docs 304 | /oauth/confirm_access/actuator/system/ 305 | /oauth/error/actuator/system/env 306 | /oauth/get/token/api.html 307 | /oauth/refresh/token/api/doc 308 | /oauth/remove/token/api/index.html 309 | /oauth/token/actuator/system/mappings 310 | /oauth/token/list/api/swagger 311 | /oauth/user/token/api/swagger-resources 312 | /oauth/userinfo/api/swagger-ui.html 313 | /peripheral/swagger-ui.html 314 | /peripheral/v2/api-docs 315 | /prometheus 316 | /redis/keysSize/api/swagger/ui 317 | /redis/memoryInfo/api/swaggerui 318 | /refresh 319 | /restart 320 | /scheduledtasks 321 | /services 322 | /services/1 323 | /services/api/v2/api-docs 324 | /services/findAlls/api/v1/api-docs 325 | /services/findOnes/api/v1/login 326 | /services/granted/api/v1/swagger-resources 327 | /services/saveOrUpdate/api/v1/swagger-ui.html 328 | /sessions 329 | /shutdown 330 | /spring-security-oauth-resource/swagger-ui.html 331 | /spring-security-rest/api/swagger-ui.html 332 | /static/swagger.json 333 | /sw/swagger-ui.html 334 | /swagger 335 | /swagger-dubbo/api-docs 336 | /swagger-resources 337 | /swagger-resources/actuator/shutdown 338 | /swagger-resources/configuration/security 339 | /swagger-resources/configuration/security/actuator/spring-security-oauth-resource/swagger-ui.html 340 | /swagger-resources/configuration/ui 341 | /swagger-resources/configuration/ui/actuator/spring-security-rest/api/swagger-ui.html 342 | /swagger-ui 343 | /swagger-ui.html 344 | /swagger-ui.html# 345 | /swagger-ui.html/api/v2/swagger.json 346 | /swagger-ui.json 347 | /swagger-ui/html 348 | /swagger-ui/index.html 349 | /swagger-ui/swagger.json 350 | /swagger.json 351 | /swagger.yml 352 | /swagger/ 353 | /swagger/codes 354 | /swagger/index.html 355 | /swagger/static/index.html 356 | /swagger/swagger-ui.html 357 | /swagger/ui 358 | /swagger/ui/index 359 | /swagger/v1/swagger.json 360 | /swagger/v2/swagger.json 361 | /system/ 362 | /system/druid/index.html 363 | /system/druid/login.html 364 | /system/druid/websession.html 365 | /system/env 366 | /system/mappings 367 | /system/showOsInfo 368 | /system/showProperties 369 | /template/swagger-ui.html 370 | /threaddump 371 | /trace 372 | /trace/ 373 | /uc/env 374 | /user/swagger-ui.html 375 | /v1.1/swagger-ui.html 376 | /v1.2/swagger-ui.html 377 | /v1.3/swagger-ui.html 378 | /v1.4/swagger-ui.html 379 | /v1.5/swagger-ui.html 380 | /v1.6/swagger-ui.html 381 | /v1.7/swagger-ui.html 382 | /v1.8/swagger-ui.html 383 | /v1.9/swagger-ui.html 384 | /v1/agent/self/actuator/system/showProperties 385 | /v1/api-docs 386 | /v1/catalog/service/app 387 | /v1/catalog/services/actuator/threaddump 388 | /v1/swagger.json 389 | /v2.0/swagger-ui.html 390 | /v2.1/swagger-ui.html 391 | /v2.2/swagger-ui.html 392 | /v2.3/swagger-ui.html 393 | /v2/api-docs 394 | /v2/api-docs?group=swagger接口文档 395 | /v2/swagger.json 396 | /v3/api-docs 397 | /validata/code 398 | /version 399 | /webpage/system/druid/index.html 400 | /webpage/system/druid/login.html 401 | /webpage/system/druid/websession.html 402 | /actuator/gateway/routes 403 | /actuator/get 404 | /gateway/routes/new_route 405 | /actuator/gateway/routes/new_route 406 | /new_route 407 | /actuator/gateway/refresh 408 | /gateway/refresh 409 | /actuator/gateway/globalfilters 410 | /actuator/gateway/routefilters 411 | /actuator/gatewayroutes/1 412 | /actuator/nacos 413 | /actuator/nacos-config/actuator/swagger-resourcesce 414 | /actuator/nacos-discovery/actuator/swagger-ui 415 | /actuator/nacosconfig 416 | /actuator/nacos/v1/cs/configs 417 | /actuator/nacos/v1/cs/configs?dataId=Misplaced 418 | /actuator/nacos/v1/ns/instance 419 | /actuator/nacos/v1/ns/instance?serviceName=springboot2-nacos-discovery 420 | /actuator/nacos/v2/cs/configs 421 | /actuator/nacos/v2/cs/configs?dataId=Misplaced 422 | /actuator/nacos/v2/ns/instance 423 | /actuator/nacos/v2/ns/instance?serviceName=springboot2-nacos-discovery 424 | /actuator/nacos/v1/service/list?pageSize=123&groupname=default_group&encoding=utf-8 425 | /actuator/nacos/v2/service/list?pageSize=123&groupname=default_group&encoding=utf-8 426 | /nacos 427 | /nacos/v1/cs/configs 428 | /nacos/v1/cs/configs?dataId=Misplaced 429 | /nacos/v1/ns/instance 430 | /nacos/v1/ns/instance?serviceName=springboot2-nacos-discovery 431 | /nacos/v2/cs/configs 432 | /nacos/v2/cs/configs?dataId=Misplaced 433 | /nacos/v2/ns/instance 434 | /nacos/v2/ns/instance?serviceName=springboot2-nacos-discovery 435 | /nacos/v1/service/list?pageSize=123&groupname=default_group&encoding=utf-8 436 | /nacos/v2/service/list?pageSize=123&groupname=default_group&encoding=utf-8 437 | /v1/cs/configs 438 | /v1/cs/configs?dataId=Misplaced 439 | /v1/ns/instance 440 | /v1/ns/instance?serviceName=springboot2-nacos-discovery 441 | /v2/cs/configs 442 | /v2/cs/configs?dataId=Misplaced 443 | /v2/ns/instance 444 | /v2/ns/instance?serviceName=springboot2-nacos-discovery 445 | /v1/service/list?pageSize=123&groupname=default_group&encoding=utf-8 446 | /v2/service/list?pageSize=123&groupname=default_group&encoding=utf-8 447 | /actuator/archaius/actuator/nacosdiscovery 448 | /actuator/configprops/actuator/nacos 449 | /actuator/health/nacos 450 | /actuator/heapdump/actuator/loggers/nacos 451 | /actuator/loggers/actuator/metrics/nacos 452 | /env/nacos 453 | /get?serviceName=springboot2-nacos-discovery 454 | /metrics/nacos 455 | /webjars/**/actuator/nacosconfig 456 | /actuator/nacos/config 457 | -------------------------------------------------------------------------------- /springboot_scan.py: -------------------------------------------------------------------------------- 1 | import requests, sys, os, argparse 2 | from binascii import b2a_hex 3 | from datetime import datetime 4 | import logging 5 | import time, random 6 | from concurrent.futures import ThreadPoolExecutor, as_completed 7 | 8 | requests.packages.urllib3.disable_warnings() 9 | from collections import defaultdict 10 | 11 | method_list = ["head", "get", "post"] 12 | retry_times = len(method_list) * 3 # 每种方法最多重试3次 13 | headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'} 14 | time_out = 5 15 | allow_redirects=True 16 | 17 | # 简单的读文件到列表 18 | def read_file(file): 19 | result_list = [] 20 | with open(file) as fileopen: 21 | for line in fileopen.readlines(): 22 | if line.strip() != '': result_list.append(line.strip()) 23 | return result_list 24 | 25 | # 判断列表里的元素是否包含在字符串内 26 | def list_element_in_str(list_=[], string_=""): 27 | flag = False 28 | if len(list_) != 0: 29 | for common_error in list_: 30 | if common_error in string_: 31 | flag = True 32 | break 33 | return flag 34 | 35 | 36 | # 设置日志打印器 37 | def set_logger(logger_name, log_file, level=logging.INFO): 38 | logger = logging.getLogger(logger_name) 39 | formatter = logging.Formatter('%(message)s') 40 | fileHandler = logging.FileHandler(log_file, mode='a') 41 | fileHandler.setFormatter(formatter) 42 | logger.setLevel(level) 43 | logger.addHandler(fileHandler) 44 | return logger 45 | 46 | 47 | # 获得版本号 48 | def get_version(): 49 | return 'You Tools Version is {} !!!'.format(version) 50 | 51 | 52 | # 输出结果处理 53 | def requests_print(logger_handle, logger_source, target_url, resp_status, resp_content_length, resp_text_size,resp_bytes_head, retry): 54 | size_and_length_sum = resp_content_length + resp_text_size 55 | logger_string = '{},"{}",{},{},{},{},{},{}'.format(logger_source, target_url, resp_status, resp_content_length, 56 | resp_text_size, size_and_length_sum, resp_bytes_head, 57 | retry_times - retry,allow_redirects) 58 | print(logger_handle, logger_string,'\n',end='') 59 | if logger_handle == "log_result": 60 | log_result.info(logger_string) 61 | elif logger_handle == "log_waive": 62 | log_waive.info(logger_string) 63 | elif logger_handle == "log_manual": 64 | log_manual.info(logger_string) 65 | elif logger_handle == "log_retry": 66 | log_retry.info(logger_string) 67 | elif logger_handle == "log_filter": 68 | log_filter.info(logger_string) 69 | else: 70 | print("please input logger_handle") 71 | 72 | 73 | # 请求测试路径 74 | def requests_common(method="get", scope=None, target_url=None, cookie=None, stream=True, timeout=time_out, retry=retry_times, proxies=None, sleep=0, allow_redirects=True): 75 | resp_status = 0 76 | resp_bytes_head = "NULL" 77 | resp_content_length = 0 78 | resp_text_size = 0 79 | resp = None 80 | 81 | try: 82 | time.sleep(sleep) 83 | resp = requests.request(method=method, url=target_url, cookies=cookie, timeout=timeout, stream=stream,proxies=proxies, headers=headers, verify=False, allow_redirects=allow_redirects) 84 | try: 85 | resp_status = resp.status_code 86 | except Exception as tmp: 87 | pass 88 | # 获取三个关键匹配项目 89 | try: 90 | resp_bytes_head = b2a_hex(resp.raw.read(10)).decode() if b2a_hex( 91 | resp.raw.read(10)).decode().strip() != "" else "NULL" 92 | except Exception as tmp: 93 | pass 94 | if resp_bytes_head.strip() == "": resp_bytes_head = "NULL" 95 | try: 96 | resp_content_length = int(str(resp.headers.get('Content-Length'))) 97 | except Exception as tmp: 98 | pass 99 | try: 100 | resp_text_size = resp_content_length if resp_content_length > 1024000 * 5 else len(resp.text) 101 | except Exception as tmp: 102 | pass 103 | 104 | return [target_url, resp_status, resp_content_length, resp_text_size, resp_bytes_head] 105 | except KeyboardInterrupt: 106 | sys.exit() 107 | except Exception as e: 108 | try: 109 | resp_status = resp.status_code 110 | except Exception as tmp: 111 | pass 112 | # 获取三个关键匹配项目 113 | try: 114 | resp_bytes_head = b2a_hex(resp.raw.read(10)).decode() if b2a_hex( 115 | resp.raw.read(10)).decode().strip() != "" else "NULL" 116 | except Exception as tmp: 117 | pass 118 | if resp_bytes_head.strip() == "": resp_bytes_head = "NULL" 119 | try: 120 | resp_content_length = int(str(resp.headers.get('Content-Length'))) 121 | except Exception as tmp: 122 | pass 123 | try: 124 | resp_text_size = resp_content_length if resp_content_length > 1024000 * 5 else len(resp.text) 125 | except Exception as tmp: 126 | pass 127 | return [target_url, resp_status, resp_content_length, resp_text_size, resp_bytes_head] 128 | 129 | 130 | # 请求文件测试 131 | def requests_stream(method="get", scope=None, target_url=None, cookie=None, stream=True, timeout=time_out, retry=retry_times, proxies=None, sleep=0, allow_redirects=True): 132 | # print("Requests {}".format(target_url)) 133 | resp_status = 0 134 | resp_bytes_head = "NULL" 135 | resp_content_length = 0 136 | resp_text_size = 0 137 | resp = None 138 | if retry < 0: 139 | requests_print("log_manual", "Retry-{}-Times".format(retry_times), target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 140 | return 141 | 142 | # 包含关键字的目标必须使用post方法 143 | must_use_post_list = ['/shutdown'] 144 | if list_element_in_str(must_use_post_list, target_url): method = 'post' 145 | 146 | # 包含关键字的目标必须使用head方法 147 | must_use_head_list = ['/heapdump'] 148 | if list_element_in_str(must_use_head_list, target_url): method = 'head' 149 | 150 | try: 151 | time.sleep(sleep) 152 | resp = requests.request(method=method, url=target_url, cookies=cookie, timeout=timeout, stream=stream,proxies=proxies, headers=headers, verify=False, allow_redirects=allow_redirects) 153 | try: 154 | resp_status = resp.status_code 155 | except Exception as tmp: 156 | pass 157 | # 获取三个关键匹配项目 158 | try: 159 | resp_bytes_head = b2a_hex(resp.raw.read(10)).decode() if b2a_hex( 160 | resp.raw.read(10)).decode().strip() != "" else "NULL" 161 | except Exception as tmp: 162 | pass 163 | if resp_bytes_head.strip() == "": resp_bytes_head = "NULL" 164 | try: 165 | resp_content_length = int(str(resp.headers.get('Content-Length'))) 166 | except Exception as tmp: 167 | pass 168 | if resp_content_length >= 1024000 * 5: 169 | resp_text_size = resp_content_length 170 | else: 171 | resp_text_size = len(resp.text) 172 | 173 | # 对响应进行处理和判断 174 | if str(resp_status).startswith("404") or str(resp_status).startswith("403") or str(resp_status).startswith( 175 | "500"): 176 | # 404\403\500响应直接弃用处理 177 | requests_print("log_waive", "Normal-NotExistUrl", target_url, resp_status, resp_content_length, 178 | resp_text_size, resp_bytes_head, retry) 179 | elif str(resp_status).startswith("503"): 180 | # 503响应需要重新测试 181 | requests_print("log_retry", "Normal-ServerBad", target_url, resp_status, resp_content_length, 182 | resp_text_size, resp_bytes_head, retry) 183 | for index in range(0, len(method_list)): 184 | if retry % len(method_list) == index: 185 | requests_stream(method=method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=True, timeout=time_out * 1.5, retry=retry - 1, proxies=proxies, sleep=random.random(), allow_redirects=allow_redirects) 186 | else: 187 | # 响应状态码为20X时,暂未考虑到30X 188 | # 200响应时,如果没有判断数据就需要重试 189 | if (resp_content_length == 0) and (resp_text_size == 0) and (resp_bytes_head == "NULL"): 190 | requests_print("log_retry", "Normal-ALLKeyZero", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 191 | for index in range(0, len(method_list)): 192 | if retry % len(method_list) == index: 193 | requests_stream(method=method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=False, timeout=time_out * 2, retry=retry - 1, proxies=proxies, allow_redirects=allow_redirects) 194 | else: 195 | # 对比处理,过滤所有错误数据 196 | if target_dict[scope]["resp_bytes_head"] == resp_bytes_head: 197 | requests_print("log_filter", "Normal-By-Bytes", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 198 | elif target_dict[scope]["resp_content_length"] == resp_content_length: 199 | requests_print("log_filter", "Normal-By-Length", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 200 | elif target_dict[scope]["resp_text_size"] == resp_text_size: 201 | requests_print("log_filter", "Normal-By-Size", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 202 | else: 203 | requests_print("log_result", "Normal-Requests", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 204 | except KeyboardInterrupt: 205 | sys.exit() 206 | except Exception as e: 207 | try: 208 | resp_status = resp.status_code 209 | except Exception as tmp: 210 | pass 211 | # 获取三个关键匹配项目 212 | try: 213 | resp_bytes_head = b2a_hex(resp.raw.read(10)).decode() if b2a_hex( 214 | resp.raw.read(10)).decode().strip() != "" else "NULL" 215 | except Exception as tmp: 216 | pass 217 | if resp_bytes_head.strip() == "": resp_bytes_head = "NULL" 218 | try: 219 | resp_content_length = int(str(resp.headers.get('Content-Length'))) 220 | except Exception as tmp: 221 | pass 222 | try: 223 | resp_text_size = resp_content_length if resp_content_length > 1024000 * 5 else len(resp.text) 224 | except Exception as tmp: 225 | pass 226 | 227 | if "IncompleteRead" in str(e): 228 | # IncompleteRead #不支持stream , 可能需要尝试重新请求,响应码 000 200 404 503 229 | if str(resp_status).startswith("0"): 230 | # 没有获取到状态码选项,需要重新测试 231 | requests_print("log_retry", "Incomplete-NoStatus", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 232 | for index in range(0, len(method_list)): 233 | if retry % len(method_list) == index: 234 | requests_stream(method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=False, timeout=time_out * 2, retry=retry - 1, proxies=proxies, allow_redirects=allow_redirects) 235 | elif str(resp_status).startswith("404") or str(resp_status).startswith("403") or str(resp_status).startswith("500"): 236 | requests_print("log_waive", "Incomplete-NotExistUrl", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 237 | elif str(resp_status).startswith("503"): 238 | requests_print("log_retry", "Incomplete-ServerBad", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 239 | for index in range(0, len(method_list)): 240 | if retry % len(method_list) == index: 241 | requests_stream(method=method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=False, timeout=time_out * 1.5, retry=retry - 1, proxies=proxies, sleep=random.random(), allow_redirects=allow_redirects) 242 | else: 243 | # str(resp_status).startswith("200"): 244 | # 判断用于匹配的三个关键属性是否为空,是的话就需要重试 245 | if (resp_content_length == 0) and (resp_text_size == 0) and (resp_bytes_head == "NULL"): 246 | requests_print("log_retry", "Incomplete-ALLKeyZero", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 247 | for index in range(0, len(method_list)): 248 | if retry % len(method_list) == index: 249 | requests_stream(method=method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=False, timeout=time_out * 2, retry=retry - 1, proxies=proxies, allow_redirects=allow_redirects) 250 | else: 251 | # 对比处理,过滤所有错误数据 252 | if target_dict[scope]["resp_bytes_head"] == resp_bytes_head: 253 | requests_print("log_filter", "Incomplete-By-Bytes", target_url, resp_status,resp_content_length, resp_text_size, resp_bytes_head, retry) 254 | elif target_dict[scope]["resp_content_length"] == resp_content_length: 255 | requests_print("log_filter", "Incomplete-By-Length", target_url, resp_status,resp_content_length, resp_text_size, resp_bytes_head, retry) 256 | elif target_dict[scope]["resp_text_size"] == resp_text_size: 257 | requests_print("log_filter", "Incomplete-By-Size", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 258 | else: 259 | requests_print("log_result", "Incomplete-Requests", target_url, resp_status,resp_content_length, resp_text_size, resp_bytes_head, retry) 260 | 261 | elif "Read timed out" in str(e): 262 | # Read timed out. #访问超时 , 需要尝试重新请求 263 | requests_print("log_retry", "Timedout-NoResult", target_url, resp_status, resp_content_length, 264 | resp_text_size, resp_bytes_head, retry) 265 | for index in range(0, len(method_list)): 266 | if retry % len(method_list) == index: 267 | requests_stream(method=method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=True, timeout=time_out * 2, retry=retry - 1, proxies=proxies, allow_redirects=allow_redirects) 268 | else: 269 | # 其他结果 270 | print("存在其他异常:", str(e)) 271 | requests_print("log_retry", "Requests-NoResult", target_url, resp_status, resp_content_length,resp_text_size, resp_bytes_head, retry) 272 | for index in range(0, len(method_list)): 273 | if retry % len(method_list) == index: 274 | requests_stream(method=method_list[index], scope=scope, target_url=target_url, cookie=cookie, stream=True, timeout=time_out * 2, retry=retry - 1, proxies=proxies, allow_redirects=allow_redirects) 275 | 276 | 277 | if __name__ == '__main__': 278 | version = "0.6.3" 279 | url_list = [] 280 | path_list = [] 281 | target_dict = defaultdict(dict) 282 | 283 | parser = argparse.ArgumentParser() 284 | parser.description = "Spring Boot Unauthorized path access detection tool , supports automatic retry and automatic filtering ..." 285 | parser.add_argument("-u", "--url", help="Specifies the destination URL to be scanned", default=None) 286 | parser.add_argument("-f", "--file", help="Specifies the destination URL file to be scanned", 287 | default='springboot_target.txt') 288 | parser.add_argument("-t", "--thread", help="Specifies the number of threads at request time", type=int, default=10) 289 | parser.add_argument("-c", "--cookie", 290 | help="Specify the Cookie field at requests , format: {'parameter1':'value1','parameter2':'value2'}", 291 | default=None) 292 | parser.add_argument("-p", "--proxies", 293 | help="Specify the requests proxy address, support Socks5 and HTTP, for example: http://127.0.0.1:8080 or socks5://127.0.0.1:1080", 294 | default=None) 295 | parser.add_argument("-d", "--dictfile", help="Specifies the SpringBoot path dictionary ", 296 | default='springboot_path.txt') 297 | parser.add_argument("-o", "--output", help="Specifies the result dictionary , default is current [.\result]", 298 | default='result') 299 | parser.add_argument("-v", "--version", action="version", version=get_version(), 300 | help="Display tool version information") 301 | args = parser.parse_args() 302 | 303 | # 日志信息输出到文件 304 | # logging 输出两个日志文件 https://www.cnblogs.com/tastepy/p/13328847.html 305 | 306 | # 当前时间戳 307 | datef_now = datetime.now().strftime('%Y-%m-%d-%H-%M-%S') 308 | # 输出结果文件夹 309 | if args.output == "result": 310 | result_dir = args.output + "_" + datef_now 311 | else: 312 | result_dir = args.output 313 | 314 | # 输出结果文件名 315 | if not os.path.exists(result_dir): os.makedirs(result_dir) 316 | file_manual = result_dir + "/" + 'scan_manual.txt' # 保存需要手动重试的URL 317 | file_waive = result_dir + "/" + 'scan_waive.txt' # 保存404等直接放弃URL 318 | file_result = result_dir + "/" + 'scan_result.txt' # 保存正常的结果文件 319 | file_retry = result_dir + "/" + 'scan_retry.txt' # 保存503等需要重试的URL 320 | file_filter = result_dir + "/" + 'scan_filter.txt' # 保存根据测试URL匹配过滤掉的URL 321 | 322 | # 设置日志记录器和对应保存文件 323 | log_result = set_logger('log_result', file_result) 324 | log_manual = set_logger('log_manual', file_manual) 325 | log_waive = set_logger('log_waive', file_waive) 326 | log_retry = set_logger('log_retry', file_retry) 327 | log_filter = set_logger('log_filter', file_filter) 328 | 329 | if args.cookie is not None: args.cookie = eval(args.cookie) # 此处有命令执行风险,请勿对外提供接口 330 | if args.proxies is not None: args.proxies = {'http': args.proxies.replace('https://', 'http://'),'https': args.proxies.replace('http://', 'https://')} 331 | 332 | # 目标URL 333 | if args.url is not None: 334 | url_list.append(args.url) 335 | elif os.path.isfile(args.file): 336 | url_list.extend(read_file(args.file)) 337 | else: 338 | parser.print_help() 339 | 340 | # URL处理,添加http/https头 341 | tmp_url_list = [] 342 | for host in url_list: 343 | if host.startswith("http"): 344 | tmp_url_list.append(host) 345 | else: 346 | tmp_url_list.append("http://" + host) 347 | tmp_url_list.append("https://" + host) 348 | url_list = list(set(tmp_url_list)) 349 | 350 | # 路径字典 351 | if os.path.isfile(args.dictfile): path_list = read_file(args.dictfile) 352 | path_list = list(set(path_list)) 353 | 354 | # 生成测试URL,并生成对应的响应关键作为对比 355 | test_path_list = ["/xxx", "/xxx/yyy", "/xxx/yyy/zzz"] 356 | test_path_data = dict() 357 | 358 | for url in url_list: 359 | test_path_data[url] = dict() # 存放测试路径的返回结果 360 | target_dict[url]["target_url_list"] = [] # 存放每个目标URL和其对应的对比参数 361 | target_dict[url]["resp_bytes_head"] = "False" 362 | target_dict[url]["resp_text_size"] = "False" 363 | target_dict[url]["resp_content_length"] = "False" 364 | 365 | ##访问测试路径 366 | for test_path in test_path_list: 367 | test_url = url + test_path 368 | test_path_data[url][test_path] = requests_common(scope=url, target_url=test_url, cookie=args.cookie,proxies=args.proxies, retry=0, allow_redirects=allow_redirects) 369 | # print(test_path_data[url][test_path]) # ['https://xxxx/xxx', 200, 92, 42, 'b7e6b182e8aebfe997ae'] 370 | 371 | ##确定各个URL的对比参数 372 | if (test_path_data[url]['/xxx'][-1] != "NULL" and test_path_data[url]['/xxx'][-1] == test_path_data[url]['/xxx/yyy'][-1] and test_path_data[url]['/xxx/yyy'][-1] == test_path_data[url]['/xxx/yyy/zzz'][-1]): 373 | print("[{}] This can be compared by the response header bytes, which is [{}]".format(url,test_path_data[url]['/xxx'][-1])) 374 | target_dict[url]["resp_bytes_head"] = test_path_data[url]['/xxx'][-1] 375 | if (test_path_data[url]['/xxx'][-2] != 0 and test_path_data[url]['/xxx'][-2] == test_path_data[url]['/xxx/yyy'][-2] == test_path_data[url]['/xxx/yyy/zzz'][-2]): 376 | print("[{}] This can be compared by the response text size, which is [{}]".format(url, test_path_data[url]['/xxx'][-2])) 377 | target_dict[url]["resp_text_size"] = test_path_data[url]['/xxx'][-2] 378 | if (test_path_data[url]['/xxx'][-3] != 0 and test_path_data[url]['/xxx'][-3] == test_path_data[url]['/xxx/yyy'][-3] == test_path_data[url]['/xxx/yyy/zzz'][-3]): 379 | print("[{}] You can compare it by the response content-length, which is [{}]".format(url,test_path_data[url]['/xxx'][-3])) 380 | target_dict[url]["resp_content_length"] = test_path_data[url]['/xxx'][-3] 381 | 382 | # 根据路径字典合并最终的请求URL 383 | for url in url_list: 384 | for path in path_list: 385 | target_dict[url]["target_url_list"].append(url.strip('/') + path) 386 | 387 | # 逐URL进行多线程请求处理 388 | for url in url_list: 389 | if target_dict[url]["target_url_list"]: 390 | # 创建一个最大容纳数量为thread的线程池 391 | with ThreadPoolExecutor(max_workers=args.thread) as pool: 392 | all_task = [] 393 | for target_url in target_dict[url]["target_url_list"]: 394 | task = pool.submit(requests_stream, scope=url, target_url=target_url, cookie=args.cookie,proxies=args.proxies, allow_redirects=allow_redirects) 395 | all_task.append(task) 396 | # 输出返回的结果 397 | for future in as_completed(all_task): pass 398 | # print(future.result()) 399 | -------------------------------------------------------------------------------- /springboot_target.txt: -------------------------------------------------------------------------------- 1 | http://www.baidu.com --------------------------------------------------------------------------------