create-dwy 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1 -1
- package/package.json +2 -2
- package/src/create.js +61 -37
- package/src/sync.js +212 -61
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-commit-convention.md +0 -106
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CVSS 评分映射 + 严重性分级工具。"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Severity(str, Enum):
|
|
8
|
+
CRITICAL = "Critical"
|
|
9
|
+
HIGH = "High"
|
|
10
|
+
MEDIUM = "Medium"
|
|
11
|
+
LOW = "Low"
|
|
12
|
+
INFO = "Info"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Finding:
|
|
17
|
+
"""统一的漏洞发现记录。"""
|
|
18
|
+
|
|
19
|
+
title: str
|
|
20
|
+
severity: Severity
|
|
21
|
+
cvss: float
|
|
22
|
+
cve_id: str = ""
|
|
23
|
+
description: str = ""
|
|
24
|
+
evidence: str = ""
|
|
25
|
+
affected_service: str = ""
|
|
26
|
+
discovery_tool: str = ""
|
|
27
|
+
remediation: str = ""
|
|
28
|
+
references: list[str] | None = None
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"title": self.title,
|
|
33
|
+
"severity": self.severity.value,
|
|
34
|
+
"cvss": self.cvss,
|
|
35
|
+
"cve_id": self.cve_id,
|
|
36
|
+
"description": self.description,
|
|
37
|
+
"evidence": self.evidence,
|
|
38
|
+
"affected_service": self.affected_service,
|
|
39
|
+
"discovery_tool": self.discovery_tool,
|
|
40
|
+
"remediation": self.remediation,
|
|
41
|
+
"references": self.references or [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cvss_to_severity(score: float) -> Severity:
|
|
46
|
+
"""CVSS 3.1 评分 → 严重性等级。"""
|
|
47
|
+
if score >= 9.0:
|
|
48
|
+
return Severity.CRITICAL
|
|
49
|
+
if score >= 7.0:
|
|
50
|
+
return Severity.HIGH
|
|
51
|
+
if score >= 4.0:
|
|
52
|
+
return Severity.MEDIUM
|
|
53
|
+
if score >= 0.1:
|
|
54
|
+
return Severity.LOW
|
|
55
|
+
return Severity.INFO
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def nuclei_severity_to_cvss(severity: str) -> float:
|
|
59
|
+
"""Nuclei 严重性字符串 → 估算 CVSS 分数。"""
|
|
60
|
+
mapping = {
|
|
61
|
+
"critical": 9.5,
|
|
62
|
+
"high": 7.5,
|
|
63
|
+
"medium": 5.5,
|
|
64
|
+
"low": 2.5,
|
|
65
|
+
"info": 0.0,
|
|
66
|
+
}
|
|
67
|
+
return mapping.get(severity.lower(), 0.0)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def nmap_vuln_to_finding(script_id: str, output: str) -> Finding:
|
|
71
|
+
"""将 nmap NSE 漏洞脚本输出转为 Finding。"""
|
|
72
|
+
# 常见 NSE 漏洞脚本的严重性映射
|
|
73
|
+
critical_scripts = {"ssl-heartbleed", "smb-vuln-ms17-010", "http-shellshock"}
|
|
74
|
+
high_scripts = {"smb-vuln-ms08-067", "ssl-poodle", "http-vuln-cve2017-5638"}
|
|
75
|
+
medium_scripts = {"ssl-dh-params", "http-csrf", "http-sql-injection"}
|
|
76
|
+
|
|
77
|
+
if script_id in critical_scripts:
|
|
78
|
+
severity = Severity.CRITICAL
|
|
79
|
+
cvss = 9.5
|
|
80
|
+
elif script_id in high_scripts:
|
|
81
|
+
severity = Severity.HIGH
|
|
82
|
+
cvss = 7.5
|
|
83
|
+
elif script_id in medium_scripts:
|
|
84
|
+
severity = Severity.MEDIUM
|
|
85
|
+
cvss = 5.5
|
|
86
|
+
else:
|
|
87
|
+
severity = Severity.MEDIUM
|
|
88
|
+
cvss = 5.0
|
|
89
|
+
|
|
90
|
+
return Finding(
|
|
91
|
+
title=script_id.replace("-", " ").replace("_", " ").title(),
|
|
92
|
+
severity=severity,
|
|
93
|
+
cvss=cvss,
|
|
94
|
+
description=output[:500],
|
|
95
|
+
discovery_tool="nmap-nse",
|
|
96
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""目标验证:IP/域名格式校验、私网地址检测。"""
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Target:
|
|
11
|
+
"""解析后的目标信息。"""
|
|
12
|
+
|
|
13
|
+
raw: str
|
|
14
|
+
host: str # 纯 IP 或域名
|
|
15
|
+
port: int | None = None
|
|
16
|
+
scheme: str | None = None # http/https
|
|
17
|
+
path: str = "/"
|
|
18
|
+
is_ip: bool = False
|
|
19
|
+
is_private: bool = False
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def base_url(self) -> str:
|
|
23
|
+
if self.scheme:
|
|
24
|
+
port_part = f":{self.port}" if self.port and self.port not in (80, 443) else ""
|
|
25
|
+
return f"{self.scheme}://{self.host}{port_part}"
|
|
26
|
+
return self.host
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def web_url(self) -> str | None:
|
|
30
|
+
"""如果有 scheme 返回完整 URL,否则 None。"""
|
|
31
|
+
if self.scheme:
|
|
32
|
+
port_part = f":{self.port}" if self.port and self.port not in (80, 443) else ""
|
|
33
|
+
return f"{self.scheme}://{self.host}{port_part}{self.path}"
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_DOMAIN_RE = re.compile(
|
|
38
|
+
r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,}$"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_target(raw: str) -> Target:
|
|
43
|
+
"""解析用户输入的目标字符串。
|
|
44
|
+
|
|
45
|
+
支持格式:
|
|
46
|
+
- IP: 192.168.1.1, 10.0.0.1
|
|
47
|
+
- 域名: example.com, sub.example.com
|
|
48
|
+
- URL: http://example.com, https://example.com:8443/path
|
|
49
|
+
- IP:端口: 192.168.1.1:8080
|
|
50
|
+
"""
|
|
51
|
+
raw = raw.strip()
|
|
52
|
+
scheme = None
|
|
53
|
+
host = raw
|
|
54
|
+
port = None
|
|
55
|
+
path = "/"
|
|
56
|
+
|
|
57
|
+
# 尝试解析为 URL
|
|
58
|
+
if "://" in raw:
|
|
59
|
+
parsed = urlparse(raw)
|
|
60
|
+
scheme = parsed.scheme or None
|
|
61
|
+
host = parsed.hostname or raw
|
|
62
|
+
port = parsed.port
|
|
63
|
+
path = parsed.path or "/"
|
|
64
|
+
elif ":" in raw and not raw.startswith("["):
|
|
65
|
+
# IP:port 或 host:port
|
|
66
|
+
parts = raw.rsplit(":", 1)
|
|
67
|
+
if parts[1].isdigit():
|
|
68
|
+
host = parts[0]
|
|
69
|
+
port = int(parts[1])
|
|
70
|
+
|
|
71
|
+
# 判断是否为 IP
|
|
72
|
+
is_ip = False
|
|
73
|
+
is_private = False
|
|
74
|
+
try:
|
|
75
|
+
addr = ipaddress.ip_address(host)
|
|
76
|
+
is_ip = True
|
|
77
|
+
is_private = addr.is_private or addr.is_loopback or addr.is_reserved
|
|
78
|
+
except ValueError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return Target(
|
|
82
|
+
raw=raw,
|
|
83
|
+
host=host,
|
|
84
|
+
port=port,
|
|
85
|
+
scheme=scheme,
|
|
86
|
+
path=path,
|
|
87
|
+
is_ip=is_ip,
|
|
88
|
+
is_private=is_private,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_target(raw: str) -> tuple[Target | None, str]:
|
|
93
|
+
"""校验目标合法性。
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
(target, warning) — target=None 表示无效,warning 为警告信息(私网等)。
|
|
97
|
+
"""
|
|
98
|
+
target = parse_target(raw)
|
|
99
|
+
|
|
100
|
+
# 校验 host 格式
|
|
101
|
+
if not target.is_ip and not _DOMAIN_RE.match(target.host):
|
|
102
|
+
return None, f"无效的目标地址: '{target.host}'(不是有效的 IP 或域名)"
|
|
103
|
+
|
|
104
|
+
# 端口范围
|
|
105
|
+
if target.port is not None and not (1 <= target.port <= 65535):
|
|
106
|
+
return None, f"无效的端口号: {target.port}"
|
|
107
|
+
|
|
108
|
+
# 私网地址警告
|
|
109
|
+
warning = ""
|
|
110
|
+
if target.is_private:
|
|
111
|
+
warning = f"目标 {target.host} 是私网/本地地址,请确认这是你的测试环境"
|
|
112
|
+
|
|
113
|
+
return target, warning
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""阶段 6:认证与凭据测试 — 弱密码检测、默认凭据探测(多服务并发)。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
uv run pentest_auth.py --target <host> --output-dir <dir> [--services ssh,ftp,http]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from lib.config import DEFAULT_CREDENTIALS, MAX_AUTH_ATTEMPTS, WEAK_PASSWORDS
|
|
19
|
+
from lib.executor import AuditLog, run_command, save_json_result
|
|
20
|
+
from lib.validator import validate_target
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
LOGIN_PATHS = ["/login", "/admin", "/admin/login", "/user/login", "/auth/login", "/wp-login.php"]
|
|
25
|
+
LOGIN_KEYWORDS = ["password", "passwd", "login", "sign in"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_ssh(host: str, port: int, audit: AuditLog) -> list[dict]:
|
|
29
|
+
"""SSH 弱密码检测。"""
|
|
30
|
+
findings = []
|
|
31
|
+
console.print(f"[bold]SSH 弱密码测试: {host}:{port}[/bold]")
|
|
32
|
+
|
|
33
|
+
usernames = ["root", "admin", "ubuntu", "ec2-user"]
|
|
34
|
+
max_passwords = MAX_AUTH_ATTEMPTS // len(usernames)
|
|
35
|
+
passwords = WEAK_PASSWORDS[:max_passwords]
|
|
36
|
+
|
|
37
|
+
pass_file = Path("/tmp/dwy_pentest_passwords.txt")
|
|
38
|
+
pass_file.write_text("\n".join(passwords))
|
|
39
|
+
user_file = Path("/tmp/dwy_pentest_users.txt")
|
|
40
|
+
user_file.write_text("\n".join(usernames))
|
|
41
|
+
|
|
42
|
+
cmd = [
|
|
43
|
+
"hydra",
|
|
44
|
+
"-L", str(user_file),
|
|
45
|
+
"-P", str(pass_file),
|
|
46
|
+
"-t", "4",
|
|
47
|
+
"-f",
|
|
48
|
+
"-o", "-",
|
|
49
|
+
"-s", str(port),
|
|
50
|
+
host,
|
|
51
|
+
"ssh",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
result = run_command(cmd, timeout=120, audit=audit)
|
|
55
|
+
|
|
56
|
+
pass_file.unlink(missing_ok=True)
|
|
57
|
+
user_file.unlink(missing_ok=True)
|
|
58
|
+
|
|
59
|
+
if result.returncode == -3:
|
|
60
|
+
console.print("[yellow]hydra 未安装,跳过 SSH 弱密码测试[/yellow]")
|
|
61
|
+
return findings
|
|
62
|
+
|
|
63
|
+
if result.blocked:
|
|
64
|
+
console.print(f"[red]hydra 被安全策略拦截: {result.block_reason}[/red]")
|
|
65
|
+
return findings
|
|
66
|
+
|
|
67
|
+
for line in result.stdout.split("\n"):
|
|
68
|
+
if "host:" in line.lower() and ("login:" in line.lower() or "password:" in line.lower()):
|
|
69
|
+
findings.append({
|
|
70
|
+
"type": "Weak SSH Credential",
|
|
71
|
+
"detail": line.strip(),
|
|
72
|
+
"service": f"ssh:{port}",
|
|
73
|
+
"severity": "Critical",
|
|
74
|
+
"cvss": 9.8,
|
|
75
|
+
})
|
|
76
|
+
console.print(f" [red bold][CRITICAL] 发现弱密码! {line.strip()}[/red bold]")
|
|
77
|
+
|
|
78
|
+
if not findings:
|
|
79
|
+
console.print(f" [green]SSH 弱密码测试通过(测试了 {len(usernames) * len(passwords)} 组凭据)[/green]")
|
|
80
|
+
|
|
81
|
+
return findings
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_ftp(host: str, port: int, audit: AuditLog) -> list[dict]:
|
|
85
|
+
"""FTP 匿名登录 + 弱密码检测(匿名探测与 hydra 弱密码并发)。"""
|
|
86
|
+
findings = []
|
|
87
|
+
console.print(f"[bold]FTP 测试: {host}:{port}[/bold]")
|
|
88
|
+
|
|
89
|
+
creds = DEFAULT_CREDENTIALS[:10]
|
|
90
|
+
user_file = Path("/tmp/dwy_pentest_ftp_users.txt")
|
|
91
|
+
pass_file = Path("/tmp/dwy_pentest_ftp_pass.txt")
|
|
92
|
+
user_file.write_text("\n".join(set(u for u, _ in creds)))
|
|
93
|
+
pass_file.write_text("\n".join(set(p for _, p in creds)))
|
|
94
|
+
|
|
95
|
+
anon_cmd = ["nmap", "-p", str(port), "--script", "ftp-anon", host]
|
|
96
|
+
hydra_cmd = [
|
|
97
|
+
"hydra",
|
|
98
|
+
"-L", str(user_file),
|
|
99
|
+
"-P", str(pass_file),
|
|
100
|
+
"-t", "4",
|
|
101
|
+
"-f",
|
|
102
|
+
"-s", str(port),
|
|
103
|
+
host,
|
|
104
|
+
"ftp",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
# 匿名探测 + hydra 弱密码 并发
|
|
108
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
109
|
+
anon_future = pool.submit(run_command, anon_cmd, 30, audit)
|
|
110
|
+
hydra_future = pool.submit(run_command, hydra_cmd, 60, audit)
|
|
111
|
+
anon_result = anon_future.result()
|
|
112
|
+
hydra_result = hydra_future.result()
|
|
113
|
+
|
|
114
|
+
user_file.unlink(missing_ok=True)
|
|
115
|
+
pass_file.unlink(missing_ok=True)
|
|
116
|
+
|
|
117
|
+
if "Anonymous FTP login allowed" in anon_result.stdout:
|
|
118
|
+
findings.append({
|
|
119
|
+
"type": "FTP Anonymous Login",
|
|
120
|
+
"detail": "FTP 服务允许匿名登录",
|
|
121
|
+
"service": f"ftp:{port}",
|
|
122
|
+
"severity": "High",
|
|
123
|
+
"cvss": 7.5,
|
|
124
|
+
})
|
|
125
|
+
console.print(" [red][HIGH] FTP 匿名登录已开启![/red]")
|
|
126
|
+
|
|
127
|
+
for line in hydra_result.stdout.split("\n"):
|
|
128
|
+
if "host:" in line.lower() and "login:" in line.lower():
|
|
129
|
+
findings.append({
|
|
130
|
+
"type": "Weak FTP Credential",
|
|
131
|
+
"detail": line.strip(),
|
|
132
|
+
"service": f"ftp:{port}",
|
|
133
|
+
"severity": "Critical",
|
|
134
|
+
"cvss": 9.8,
|
|
135
|
+
})
|
|
136
|
+
console.print(f" [red bold][CRITICAL] {line.strip()}[/red bold]")
|
|
137
|
+
|
|
138
|
+
if not findings:
|
|
139
|
+
console.print(" [green]FTP 安全测试通过[/green]")
|
|
140
|
+
|
|
141
|
+
return findings
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def _async_probe_login_path(client: httpx.AsyncClient, url: str, path: str) -> str | None:
|
|
145
|
+
try:
|
|
146
|
+
resp = await client.get(f"{url}{path}")
|
|
147
|
+
if resp.status_code == 200 and any(kw in resp.text.lower() for kw in LOGIN_KEYWORDS):
|
|
148
|
+
return f"{url}{path}"
|
|
149
|
+
except Exception:
|
|
150
|
+
return None
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _async_try_credential(
|
|
155
|
+
client: httpx.AsyncClient, login_url: str, username: str, password: str
|
|
156
|
+
) -> dict | None:
|
|
157
|
+
try:
|
|
158
|
+
resp = await client.post(
|
|
159
|
+
login_url,
|
|
160
|
+
data={"username": username, "password": password},
|
|
161
|
+
)
|
|
162
|
+
if resp.status_code in (302, 303) and "login" not in resp.headers.get("location", "").lower():
|
|
163
|
+
return {
|
|
164
|
+
"type": "Default HTTP Credential",
|
|
165
|
+
"detail": f"登录成功: {username}:{password}",
|
|
166
|
+
"service": login_url,
|
|
167
|
+
"severity": "Critical",
|
|
168
|
+
"cvss": 9.8,
|
|
169
|
+
}
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def _async_test_http_login(url: str) -> tuple[list[dict], int]:
|
|
176
|
+
findings: list[dict] = []
|
|
177
|
+
tested = 0
|
|
178
|
+
|
|
179
|
+
# 并发探测所有可能的登录页面
|
|
180
|
+
async with httpx.AsyncClient(timeout=10, verify=False, follow_redirects=True) as client:
|
|
181
|
+
path_tasks = [_async_probe_login_path(client, url, p) for p in LOGIN_PATHS]
|
|
182
|
+
path_results = await asyncio.gather(*path_tasks, return_exceptions=True)
|
|
183
|
+
|
|
184
|
+
found_login = next((r for r in path_results if isinstance(r, str)), None)
|
|
185
|
+
if not found_login:
|
|
186
|
+
return findings, tested
|
|
187
|
+
|
|
188
|
+
console.print(f" 发现登录页面: {found_login}")
|
|
189
|
+
|
|
190
|
+
# 并发尝试默认凭据(受 MAX_AUTH_ATTEMPTS 限制)
|
|
191
|
+
creds_to_try = DEFAULT_CREDENTIALS[: min(10, MAX_AUTH_ATTEMPTS)]
|
|
192
|
+
tested = len(creds_to_try)
|
|
193
|
+
|
|
194
|
+
async with httpx.AsyncClient(timeout=10, verify=False, follow_redirects=False) as client:
|
|
195
|
+
cred_tasks = [_async_try_credential(client, found_login, u, p) for u, p in creds_to_try]
|
|
196
|
+
cred_results = await asyncio.gather(*cred_tasks, return_exceptions=True)
|
|
197
|
+
|
|
198
|
+
for r in cred_results:
|
|
199
|
+
if isinstance(r, dict):
|
|
200
|
+
console.print(f" [red bold][CRITICAL] 默认凭据有效: {r['detail']}[/red bold]")
|
|
201
|
+
findings.append(r)
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
return findings, tested
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_http_login(url: str, audit: AuditLog) -> list[dict]:
|
|
208
|
+
"""HTTP 登录表单默认凭据检测(路径探测 + 凭据尝试并发)。"""
|
|
209
|
+
console.print(f"[bold]HTTP 登录测试: {url}[/bold]")
|
|
210
|
+
findings, tested = asyncio.run(_async_test_http_login(url))
|
|
211
|
+
|
|
212
|
+
if not findings and tested == 0:
|
|
213
|
+
console.print(" [green]未发现标准登录页面[/green]")
|
|
214
|
+
elif not findings:
|
|
215
|
+
console.print(f" [green]HTTP 默认凭据测试通过(测试了 {tested} 组)[/green]")
|
|
216
|
+
|
|
217
|
+
return findings
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main():
|
|
221
|
+
parser = argparse.ArgumentParser(description="渗透测试 - 认证测试(多服务并发)")
|
|
222
|
+
parser.add_argument("--target", required=True, help="目标 IP/域名/URL")
|
|
223
|
+
parser.add_argument("--output-dir", required=True, help="输出目录")
|
|
224
|
+
parser.add_argument("--services", default="ssh,http", help="测试服务列表,逗号分隔(ssh,ftp,http)")
|
|
225
|
+
parser.add_argument("--ssh-port", type=int, default=22, help="SSH 端口")
|
|
226
|
+
parser.add_argument("--ftp-port", type=int, default=21, help="FTP 端口")
|
|
227
|
+
args = parser.parse_args()
|
|
228
|
+
|
|
229
|
+
target, warning = validate_target(args.target)
|
|
230
|
+
if target is None:
|
|
231
|
+
console.print(f"[red]{warning}[/red]")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
if warning:
|
|
234
|
+
console.print(f"[yellow]{warning}[/yellow]")
|
|
235
|
+
|
|
236
|
+
output_dir = Path(args.output_dir)
|
|
237
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
audit = AuditLog(output_dir / "audit.log")
|
|
239
|
+
|
|
240
|
+
services = [s.strip() for s in args.services.split(",")]
|
|
241
|
+
|
|
242
|
+
# 多服务并发测试
|
|
243
|
+
console.print(f"[bold cyan]并发测试服务: {', '.join(services)}[/bold cyan]")
|
|
244
|
+
tasks: dict = {}
|
|
245
|
+
with ThreadPoolExecutor(max_workers=len(services) or 1) as pool:
|
|
246
|
+
if "ssh" in services:
|
|
247
|
+
tasks["ssh"] = pool.submit(test_ssh, target.host, args.ssh_port, audit)
|
|
248
|
+
if "ftp" in services:
|
|
249
|
+
tasks["ftp"] = pool.submit(test_ftp, target.host, args.ftp_port, audit)
|
|
250
|
+
if "http" in services:
|
|
251
|
+
url = target.web_url or f"http://{target.host}"
|
|
252
|
+
tasks["http"] = pool.submit(test_http_login, url, audit)
|
|
253
|
+
|
|
254
|
+
all_findings: list[dict] = []
|
|
255
|
+
for name, fut in tasks.items():
|
|
256
|
+
try:
|
|
257
|
+
all_findings.extend(fut.result())
|
|
258
|
+
except Exception as e:
|
|
259
|
+
console.print(f"[red]{name} 测试异常: {e}[/red]")
|
|
260
|
+
|
|
261
|
+
result = {
|
|
262
|
+
"target": target.host,
|
|
263
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
264
|
+
"services_tested": services,
|
|
265
|
+
"max_attempts_per_service": MAX_AUTH_ATTEMPTS,
|
|
266
|
+
"findings": all_findings,
|
|
267
|
+
"total": len(all_findings),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
output_file = output_dir / "auth_test.json"
|
|
271
|
+
save_json_result(result, output_file)
|
|
272
|
+
console.print(f"\n[green]结果已保存: {output_file}[/green]")
|
|
273
|
+
|
|
274
|
+
if all_findings:
|
|
275
|
+
table = Table(title="认证测试发现")
|
|
276
|
+
table.add_column("类型")
|
|
277
|
+
table.add_column("严重性")
|
|
278
|
+
table.add_column("详情")
|
|
279
|
+
for f in all_findings:
|
|
280
|
+
table.add_row(f["type"], f["severity"], f["detail"][:60])
|
|
281
|
+
console.print(table)
|
|
282
|
+
|
|
283
|
+
console.print(f"\n[red bold]!! 发现 {len(all_findings)} 个凭据安全问题 !![/red bold]")
|
|
284
|
+
else:
|
|
285
|
+
console.print(f"\n[green]所有认证测试通过[/green]")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
main()
|