@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
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/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Exposed-Files Remediation Playbook
|
|
2
|
+
|
|
3
|
+
## Pattern 1 — Block dot-directories at the web server
|
|
4
|
+
|
|
5
|
+
### nginx (location blocks)
|
|
6
|
+
|
|
7
|
+
```nginx
|
|
8
|
+
# Deny all dot-files / dot-directories
|
|
9
|
+
location ~ /\. {
|
|
10
|
+
deny all;
|
|
11
|
+
access_log off;
|
|
12
|
+
log_not_found off;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Belt-and-suspenders for specific high-value paths
|
|
16
|
+
location ~ ^/(\.git|\.svn|\.hg|\.bzr|\.env|\.aws|\.ssh) {
|
|
17
|
+
deny all;
|
|
18
|
+
return 404;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Apache (vhost or .htaccess)
|
|
23
|
+
|
|
24
|
+
```apache
|
|
25
|
+
<DirectoryMatch "^\.|/\.">
|
|
26
|
+
Require all denied
|
|
27
|
+
</DirectoryMatch>
|
|
28
|
+
|
|
29
|
+
<Files ~ "^\.">
|
|
30
|
+
Require all denied
|
|
31
|
+
</Files>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Caddy
|
|
35
|
+
|
|
36
|
+
Caddy denies dot-directories by default since v2.0. To explicitly
|
|
37
|
+
assert:
|
|
38
|
+
|
|
39
|
+
```caddy
|
|
40
|
+
example.com {
|
|
41
|
+
@dot path */.git/* */.env */.aws/* */.ssh/* */.svn/*
|
|
42
|
+
respond @dot 404
|
|
43
|
+
file_server
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### AWS ALB / WAF
|
|
48
|
+
|
|
49
|
+
Add a managed-rule-group or custom rule:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"Name": "BlockDotPaths",
|
|
54
|
+
"Statement": {
|
|
55
|
+
"RegexMatchStatement": {
|
|
56
|
+
"FieldToMatch": {"UriPath": {}},
|
|
57
|
+
"RegexString": "/\\.(git|env|aws|ssh|svn|hg|bzr|idea|vscode)(\\b|/)",
|
|
58
|
+
"TextTransformations": [{"Priority": 0, "Type": "LOWERCASE"}]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"Action": {"Block": {}}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Cloudflare WAF
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
http.request.uri.path matches "/\\.(git|env|aws|ssh|svn|hg|bzr)"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Action: Block.
|
|
72
|
+
|
|
73
|
+
## Pattern 2 — Block backup / dump file extensions
|
|
74
|
+
|
|
75
|
+
### nginx
|
|
76
|
+
|
|
77
|
+
```nginx
|
|
78
|
+
location ~* \.(sql|bak|dump|swp|swo|orig|backup|tar\.gz|tar\.bz2|zip|7z|rar)$ {
|
|
79
|
+
deny all;
|
|
80
|
+
return 404;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Apache
|
|
85
|
+
|
|
86
|
+
```apache
|
|
87
|
+
<FilesMatch "\.(sql|bak|dump|swp|swo|orig|backup|tar\.gz|tar\.bz2|zip|7z|rar)$">
|
|
88
|
+
Require all denied
|
|
89
|
+
</FilesMatch>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Caddy
|
|
93
|
+
|
|
94
|
+
```caddy
|
|
95
|
+
@backups path *.sql *.bak *.dump *.swp *.tar.gz *.zip
|
|
96
|
+
respond @backups 404
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Pattern 3 — Block private key extensions
|
|
100
|
+
|
|
101
|
+
### nginx
|
|
102
|
+
|
|
103
|
+
```nginx
|
|
104
|
+
location ~* \.(pem|key|p12|pfx|jks|crt|cer|csr|kdb|kbx)$ {
|
|
105
|
+
deny all;
|
|
106
|
+
return 404;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
These extensions should never be reachable via the web; if a key needs
|
|
111
|
+
to be served (e.g., a JWKS for OAuth), serve it at an explicit path
|
|
112
|
+
with a content-type check rather than relying on the extension.
|
|
113
|
+
|
|
114
|
+
## Pattern 4 — Block development metadata
|
|
115
|
+
|
|
116
|
+
### nginx
|
|
117
|
+
|
|
118
|
+
```nginx
|
|
119
|
+
location ~* /(phpinfo|info|test)\.php$ {
|
|
120
|
+
deny all;
|
|
121
|
+
return 404;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
location ~* /(composer\.json|package\.json|Dockerfile|docker-compose\.yml|requirements\.txt|Gemfile|build\.gradle)$ {
|
|
125
|
+
deny all;
|
|
126
|
+
return 404;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`phpinfo.php` should be removed from any production environment.
|
|
131
|
+
Detection of an active `phpinfo.php` on prod is an audit-flag-worthy
|
|
132
|
+
operational gap; the fix is removal, not access control.
|
|
133
|
+
|
|
134
|
+
## Pattern 5 — Block at the build / deploy layer
|
|
135
|
+
|
|
136
|
+
The cleanest fix is preventing the files from getting deployed in
|
|
137
|
+
the first place.
|
|
138
|
+
|
|
139
|
+
### Dockerfile — multi-stage build with explicit copies
|
|
140
|
+
|
|
141
|
+
```dockerfile
|
|
142
|
+
# Build stage
|
|
143
|
+
FROM node:20 AS build
|
|
144
|
+
WORKDIR /build
|
|
145
|
+
COPY package.json package-lock.json ./
|
|
146
|
+
RUN npm ci
|
|
147
|
+
COPY src/ ./src/
|
|
148
|
+
RUN npm run build
|
|
149
|
+
|
|
150
|
+
# Runtime stage — only copy the built artifact, NOT the build context
|
|
151
|
+
FROM nginx:alpine
|
|
152
|
+
COPY --from=build /build/dist /usr/share/nginx/html
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Notice: no `COPY . .` anywhere. The build stage gets only what it
|
|
156
|
+
needs; the runtime stage gets only the `dist/` artifact. `.git/`,
|
|
157
|
+
`.env`, `node_modules/`, etc., never reach the runtime image.
|
|
158
|
+
|
|
159
|
+
### .dockerignore
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
.git
|
|
163
|
+
.env*
|
|
164
|
+
.aws
|
|
165
|
+
.ssh
|
|
166
|
+
*.pem
|
|
167
|
+
*.key
|
|
168
|
+
node_modules
|
|
169
|
+
.DS_Store
|
|
170
|
+
.idea
|
|
171
|
+
.vscode
|
|
172
|
+
backup.sql
|
|
173
|
+
*.sql
|
|
174
|
+
*.dump
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### .gitlab-ci.yml / GitHub Actions — exclude sensitive paths from artifacts
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
artifacts:
|
|
181
|
+
paths:
|
|
182
|
+
- dist/
|
|
183
|
+
exclude:
|
|
184
|
+
- "**/.git*"
|
|
185
|
+
- "**/.env*"
|
|
186
|
+
- "**/.aws/*"
|
|
187
|
+
- "**/.ssh/*"
|
|
188
|
+
- "**/*.pem"
|
|
189
|
+
- "**/*.key"
|
|
190
|
+
- "**/backup.*"
|
|
191
|
+
- "**/*.sql"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Pattern 6 — Cloud Run / Lambda artifact builds
|
|
195
|
+
|
|
196
|
+
Cloud Run and Lambda build the deployment artifact from your repo by
|
|
197
|
+
default. To exclude:
|
|
198
|
+
|
|
199
|
+
### Cloud Run via Cloud Build
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
# cloudbuild.yaml
|
|
203
|
+
steps:
|
|
204
|
+
- name: 'gcr.io/cloud-builders/docker'
|
|
205
|
+
args: ['build', '-t', 'gcr.io/$PROJECT_ID/app', '.']
|
|
206
|
+
options:
|
|
207
|
+
ignoreFile: '.dockerignore' # honored by Cloud Build
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Combined with the `.dockerignore` above, this excludes the files.
|
|
211
|
+
|
|
212
|
+
### Lambda via SAM / Serverless Framework
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
package:
|
|
216
|
+
patterns:
|
|
217
|
+
- '!.git/**'
|
|
218
|
+
- '!.env*'
|
|
219
|
+
- '!.aws/**'
|
|
220
|
+
- '!.ssh/**'
|
|
221
|
+
- '!**/*.pem'
|
|
222
|
+
- '!**/*.key'
|
|
223
|
+
- '!backup.*'
|
|
224
|
+
- '!**/*.sql'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Auditing existing deployments
|
|
228
|
+
|
|
229
|
+
Run the scanner on every production endpoint:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
for ENDPOINT in $(cat production-urls.txt); do
|
|
233
|
+
python3 plugins/security/penetration-tester/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py \
|
|
234
|
+
"$ENDPOINT" --authorized --min-severity critical \
|
|
235
|
+
--format jsonl --output /dev/stdout
|
|
236
|
+
done > exposure-audit.jsonl
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Treat any CRITICAL finding as ship-same-hour. Document the
|
|
240
|
+
remediation in the same PR as the audit (so the audit's grep target
|
|
241
|
+
gets re-verified on commit).
|
|
242
|
+
|
|
243
|
+
## After remediation — assume compromise
|
|
244
|
+
|
|
245
|
+
If `.git/` was exposed, assume:
|
|
246
|
+
|
|
247
|
+
- Every credential ever committed to the repo, including ones in past
|
|
248
|
+
commits that were later removed, is compromised. Rotate them all.
|
|
249
|
+
- The full source code is in the attacker's hands. Treat any
|
|
250
|
+
authentication / authorization logic as if it had been read.
|
|
251
|
+
|
|
252
|
+
If `.env` was exposed:
|
|
253
|
+
|
|
254
|
+
- Rotate every credential in the file
|
|
255
|
+
- Audit logs for any API call against those credentials in the
|
|
256
|
+
past window from when the deploy happened to when you rotated
|
|
257
|
+
- Notify partners whose API keys you held
|
|
258
|
+
|
|
259
|
+
If a backup `.sql` was exposed:
|
|
260
|
+
|
|
261
|
+
- Assume the database is compromised in the state it was in when
|
|
262
|
+
the backup was taken
|
|
263
|
+
- Trigger your data-breach response: regulator notification,
|
|
264
|
+
customer notification, credential rotation for anyone whose
|
|
265
|
+
data was in the dump
|
|
266
|
+
|
|
267
|
+
## Verification after remediation
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py \
|
|
271
|
+
https://example.com --authorized --min-severity info
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Expected: exit 0, zero findings of any severity.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Exposed-Files Theory
|
|
2
|
+
|
|
3
|
+
## Why this probe class is the highest-value pentest call
|
|
4
|
+
|
|
5
|
+
Most pentest findings are about chained conditions and threshold judgments.
|
|
6
|
+
This one is binary: either the `.git/HEAD` is reachable or it's not. A
|
|
7
|
+
single 200 response answers the question. The consequence of a true
|
|
8
|
+
positive is direct: full repo history (with embedded credentials in old
|
|
9
|
+
commits), live API keys (in `.env`), and direct database access (in
|
|
10
|
+
`backup.sql`). No CVE chaining, no exploitation primitive needed.
|
|
11
|
+
|
|
12
|
+
The reason these exposures keep happening is operational: web servers
|
|
13
|
+
are designed to serve files from a directory tree, and deployment
|
|
14
|
+
processes routinely deploy the whole tree, including dot-directories
|
|
15
|
+
and config files the application never intended to expose. Modern
|
|
16
|
+
frameworks ship "deny" defaults (Caddy, nginx with a security baseline,
|
|
17
|
+
Cloud Run) but legacy stacks (LAMP on a shared host, hand-rolled
|
|
18
|
+
nginx vhosts on a VM) default to "allow."
|
|
19
|
+
|
|
20
|
+
## File-by-file: why each one matters
|
|
21
|
+
|
|
22
|
+
### `.git/` — version control directory
|
|
23
|
+
|
|
24
|
+
The `.git/` subdirectory contains the entire history of the repository.
|
|
25
|
+
Exposing it allows an attacker to clone the repo without needing
|
|
26
|
+
authentication:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
git clone https://example.com/.git/ ./reconstructed-source
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
History reveals:
|
|
33
|
+
|
|
34
|
+
- API keys in old commits (before the `.env` was added to `.gitignore`)
|
|
35
|
+
- Hardcoded credentials someone removed but didn't rebase out
|
|
36
|
+
- Repo URL + branch names + commit messages → understanding the
|
|
37
|
+
deployment process
|
|
38
|
+
- Database schemas if migrations are checked in
|
|
39
|
+
- Source code (obviously) → static analysis targets
|
|
40
|
+
|
|
41
|
+
Three minimal probes that confirm exposure:
|
|
42
|
+
|
|
43
|
+
- `.git/HEAD` — should be a small text file starting with `ref:` or a
|
|
44
|
+
40-char hex SHA
|
|
45
|
+
- `.git/config` — `[remote "origin"]` block reveals the upstream
|
|
46
|
+
- `.git/index` — binary file starting with `DIRC` magic
|
|
47
|
+
|
|
48
|
+
The `.git/` exposure is so common that there are off-the-shelf tools
|
|
49
|
+
(GitDumper, git-dumper) that walk the exposed directory and reconstruct
|
|
50
|
+
the repo. Once exposed, assume the full history is compromised.
|
|
51
|
+
|
|
52
|
+
### `.env` — dotenv credentials
|
|
53
|
+
|
|
54
|
+
Most modern stacks (Node.js with dotenv, Python with python-dotenv,
|
|
55
|
+
Ruby with dotenv-rails, Laravel) load environment variables from a
|
|
56
|
+
`.env` file at startup. The file's contents are typically the most
|
|
57
|
+
sensitive thing the application has: API keys, database connection
|
|
58
|
+
strings, signing secrets, third-party credentials.
|
|
59
|
+
|
|
60
|
+
A leaked `.env`:
|
|
61
|
+
|
|
62
|
+
- Lets an attacker authenticate as the app to upstream services
|
|
63
|
+
(Stripe, Twilio, OpenAI, etc.)
|
|
64
|
+
- Reveals database credentials if `DATABASE_URL` is present
|
|
65
|
+
- Discloses signing secrets for JWT / session tokens, enabling token
|
|
66
|
+
forgery without further effort
|
|
67
|
+
|
|
68
|
+
Fingerprint: `KEY=VALUE` pattern on each line, often `[A-Z_]` keys.
|
|
69
|
+
|
|
70
|
+
### `.aws/credentials`, `.aws/config`
|
|
71
|
+
|
|
72
|
+
AWS SDK and CLI look in `~/.aws/credentials` by default. If a deploy
|
|
73
|
+
process accidentally copies the user's home directory into the web
|
|
74
|
+
root, AWS credentials are reachable.
|
|
75
|
+
|
|
76
|
+
`[default]\naws_access_key_id = AKIA...` is the fingerprint. Once
|
|
77
|
+
leaked, the credentials grant whatever permissions the IAM principal
|
|
78
|
+
had — historically that's "Administrator" on dev/test accounts
|
|
79
|
+
because the principle of least privilege is not the default.
|
|
80
|
+
|
|
81
|
+
### Private keys (`id_rsa`, `*.pem`, `*.key`)
|
|
82
|
+
|
|
83
|
+
SSH private keys, TLS private keys, or signing keys. Exposure means:
|
|
84
|
+
|
|
85
|
+
- An attacker can authenticate as the server to other systems (lateral
|
|
86
|
+
movement)
|
|
87
|
+
- An attacker can MITM TLS connections by presenting the leaked cert +
|
|
88
|
+
key combination
|
|
89
|
+
- An attacker can sign tokens / artifacts as the system
|
|
90
|
+
|
|
91
|
+
Fingerprint: any of `-----BEGIN RSA PRIVATE KEY-----`,
|
|
92
|
+
`-----BEGIN OPENSSH PRIVATE KEY-----`, `-----BEGIN PRIVATE KEY-----`,
|
|
93
|
+
`-----BEGIN EC PRIVATE KEY-----`, `-----BEGIN DSA PRIVATE KEY-----`.
|
|
94
|
+
|
|
95
|
+
### Backup files (`backup.sql`, `dump.sql`, `*.tar.gz`)
|
|
96
|
+
|
|
97
|
+
SQL dumps contain the entire database — schema and rows. A leaked
|
|
98
|
+
backup is functionally equivalent to RCE on the database server.
|
|
99
|
+
Common origins:
|
|
100
|
+
|
|
101
|
+
- Operator made a backup before a migration and left it in the web root
|
|
102
|
+
- Deploy script copies a tarball of the previous release into the web
|
|
103
|
+
root for rollback purposes
|
|
104
|
+
- Cron job dumps a backup to a path the web server happens to serve
|
|
105
|
+
|
|
106
|
+
Fingerprint: SQL dumps contain `CREATE TABLE`, `INSERT INTO`. Archive
|
|
107
|
+
files (binary) get a no-fingerprint check — the path being reachable
|
|
108
|
+
at 200 is the finding.
|
|
109
|
+
|
|
110
|
+
### `.DS_Store`
|
|
111
|
+
|
|
112
|
+
macOS Finder creates a `.DS_Store` in every directory it views,
|
|
113
|
+
recording metadata about how the directory is displayed. The binary
|
|
114
|
+
format includes the filenames of every file in the directory.
|
|
115
|
+
|
|
116
|
+
Exposure is medium severity because it doesn't directly leak credentials
|
|
117
|
+
or source code, but it enumerates the directory structure, including
|
|
118
|
+
hidden files that wouldn't otherwise be discoverable by URL probing.
|
|
119
|
+
|
|
120
|
+
Fingerprint: binary blob with `Bud1` magic at offset 4 or 0.
|
|
121
|
+
|
|
122
|
+
### `phpinfo()` output
|
|
123
|
+
|
|
124
|
+
PHP's `phpinfo()` function dumps the full PHP environment — every
|
|
125
|
+
configuration directive, every environment variable, every loaded
|
|
126
|
+
module. Common in `phpinfo.php`, `info.php`, `test.php` files left
|
|
127
|
+
behind from initial server setup.
|
|
128
|
+
|
|
129
|
+
Includes:
|
|
130
|
+
|
|
131
|
+
- Document root path (informs directory traversal)
|
|
132
|
+
- Loaded extensions (informs exploit selection)
|
|
133
|
+
- Often: `SERVER` variables including request headers and cookies
|
|
134
|
+
- Sometimes: `ENV` variables including secrets
|
|
135
|
+
|
|
136
|
+
Fingerprint: HTML body containing `PHP Version` heading.
|
|
137
|
+
|
|
138
|
+
### IDE configs (`.idea/`, `.vscode/`)
|
|
139
|
+
|
|
140
|
+
Per-project IDE settings. Low severity but information disclosure:
|
|
141
|
+
|
|
142
|
+
- Run configurations (database connection strings, env vars used during dev)
|
|
143
|
+
- Recent file lists (informs which files the dev was working on)
|
|
144
|
+
- Inspection scope (informs which directories have application code)
|
|
145
|
+
|
|
146
|
+
### Dependency manifests on production root (`package.json`, etc.)
|
|
147
|
+
|
|
148
|
+
Information disclosure: exposes exact versions of every dependency,
|
|
149
|
+
enabling targeted CVE lookup. Not a vulnerability in itself, but a
|
|
150
|
+
recon enabler.
|
|
151
|
+
|
|
152
|
+
## Why fingerprint-checking matters
|
|
153
|
+
|
|
154
|
+
SPAs (Single-Page Applications) using client-side routing return the
|
|
155
|
+
app's `index.html` for any unknown route — including `/.git/HEAD`.
|
|
156
|
+
Without fingerprint verification, every `/.git/*` probe returns 200
|
|
157
|
+
and every `/.env` probe returns 200, all of them false positives.
|
|
158
|
+
|
|
159
|
+
The fingerprint check inspects the response body. If a request for
|
|
160
|
+
`.git/HEAD` returns 200 with body `<!DOCTYPE html>`, it's the SPA
|
|
161
|
+
catching the route, not a real `.git/HEAD`. If the body starts with
|
|
162
|
+
`ref:` or matches a 40-char hex SHA, it's the real file.
|
|
163
|
+
|
|
164
|
+
The skill's `--check-only` mode skips fingerprint verification for
|
|
165
|
+
cases where the operator wants to know about every 200, including
|
|
166
|
+
the SPA false positives, and accepts noise as the cost.
|
|
167
|
+
|
|
168
|
+
## Primary sources
|
|
169
|
+
|
|
170
|
+
- [OWASP WSTG-INFO-02 — Fingerprint Web Server](https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/01-Information_Gathering/02-Fingerprint_Web_Server)
|
|
171
|
+
- [OWASP WSTG-CONF-04 — Review Old Backup and Unreferenced Files](https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/02-Configuration_and_Deployment_Management_Testing/04-Review_Old_Backup_and_Unreferenced_Files_for_Sensitive_Information)
|
|
172
|
+
- [CWE-538 — File and Directory Information Exposure](https://cwe.mitre.org/data/definitions/538.html)
|
|
173
|
+
- [CWE-200 — Information Exposure](https://cwe.mitre.org/data/definitions/200.html)
|
|
174
|
+
- [NIST SP 800-53 SC-28 — Protection of Information at Rest](https://nvd.nist.gov/800-53/Rev5/control/SC-28)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Probe for accidentally-served secret-bearing files in the web root.
|
|
3
|
+
|
|
4
|
+
Companion to skill `detecting-exposed-secrets-files`. Sends a GET for
|
|
5
|
+
each path in a curated 40+ probe set. For 200 responses, fingerprints
|
|
6
|
+
the body to distinguish a real file from an SPA index page that 200s
|
|
7
|
+
on any route.
|
|
8
|
+
|
|
9
|
+
References:
|
|
10
|
+
OWASP WSTG v4.2 § 4.2.4 Enumerate Infrastructure / Application
|
|
11
|
+
NIST SP 800-53 SC-28 Protection of Information at Rest
|
|
12
|
+
CWE-538 File and Directory Information Exposure
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
23
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
25
|
+
|
|
26
|
+
from lib.authz_check import require_authorization # noqa: E402
|
|
27
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
28
|
+
from lib.http_client import make_session, safe_get # noqa: E402
|
|
29
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
30
|
+
|
|
31
|
+
SKILL_ID = "detecting-exposed-secrets-files"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Probe set. Each entry: (path, label, severity, body_fingerprint_regex_or_None, control)
|
|
35
|
+
# fingerprint_regex applied case-insensitively against first 2 KiB of body.
|
|
36
|
+
PROBES = [
|
|
37
|
+
# Critical - direct credential / source / data exposure
|
|
38
|
+
(
|
|
39
|
+
".git/HEAD",
|
|
40
|
+
"Git repository .git/HEAD exposed",
|
|
41
|
+
Severity.CRITICAL,
|
|
42
|
+
r"^(ref:\s*refs/|[0-9a-f]{40})",
|
|
43
|
+
"NIST 800-53 SC-28",
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
".git/config",
|
|
47
|
+
"Git repository .git/config exposed (may include remote credentials)",
|
|
48
|
+
Severity.CRITICAL,
|
|
49
|
+
r"\[remote\b",
|
|
50
|
+
"NIST 800-53 SC-28",
|
|
51
|
+
),
|
|
52
|
+
(".git/index", "Git repository .git/index exposed", Severity.CRITICAL, r"^DIRC", "NIST 800-53 SC-28"),
|
|
53
|
+
(
|
|
54
|
+
".git/logs/HEAD",
|
|
55
|
+
"Git repository ref log exposed",
|
|
56
|
+
Severity.CRITICAL,
|
|
57
|
+
r"[0-9a-f]{40}\s+[0-9a-f]{40}",
|
|
58
|
+
"NIST 800-53 SC-28",
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
".env",
|
|
62
|
+
".env file exposed (likely contains API keys, DB credentials)",
|
|
63
|
+
Severity.CRITICAL,
|
|
64
|
+
r"^[A-Z_][A-Z0-9_]*\s*=",
|
|
65
|
+
"OWASP A05:2021",
|
|
66
|
+
),
|
|
67
|
+
(".env.production", ".env.production exposed", Severity.CRITICAL, r"^[A-Z_][A-Z0-9_]*\s*=", "OWASP A05:2021"),
|
|
68
|
+
(".env.local", ".env.local exposed", Severity.CRITICAL, r"^[A-Z_][A-Z0-9_]*\s*=", "OWASP A05:2021"),
|
|
69
|
+
(
|
|
70
|
+
".aws/credentials",
|
|
71
|
+
"AWS credentials file exposed",
|
|
72
|
+
Severity.CRITICAL,
|
|
73
|
+
r"\[default\]|aws_access_key_id",
|
|
74
|
+
"CWE-200",
|
|
75
|
+
),
|
|
76
|
+
(".aws/config", "AWS config file exposed", Severity.CRITICAL, r"\[default\]|region\s*=", "CWE-200"),
|
|
77
|
+
("id_rsa", "SSH private key exposed", Severity.CRITICAL, r"BEGIN\s+(RSA|OPENSSH|EC|DSA)?\s*PRIVATE KEY", "CWE-321"),
|
|
78
|
+
("id_ed25519", "SSH ed25519 private key exposed", Severity.CRITICAL, r"BEGIN\s+OPENSSH\s+PRIVATE KEY", "CWE-321"),
|
|
79
|
+
("server.pem", "Server PEM key exposed", Severity.CRITICAL, r"BEGIN\s+(RSA\s+)?PRIVATE KEY", "CWE-321"),
|
|
80
|
+
("backup.sql", "SQL backup exposed", Severity.CRITICAL, r"CREATE\s+TABLE|INSERT\s+INTO", "CWE-538"),
|
|
81
|
+
("dump.sql", "SQL dump exposed", Severity.CRITICAL, r"CREATE\s+TABLE|INSERT\s+INTO", "CWE-538"),
|
|
82
|
+
("database.sql", "Database SQL exposed", Severity.CRITICAL, r"CREATE\s+TABLE|INSERT\s+INTO", "CWE-538"),
|
|
83
|
+
("backup.zip", "Archive backup.zip exposed", Severity.CRITICAL, None, "CWE-538"),
|
|
84
|
+
("backup.tar.gz", "Archive backup.tar.gz exposed", Severity.CRITICAL, None, "CWE-538"),
|
|
85
|
+
("dump.tar.gz", "Archive dump.tar.gz exposed", Severity.CRITICAL, None, "CWE-538"),
|
|
86
|
+
# High - VCS metadata (less direct than .git but still source-of-truth-leaking)
|
|
87
|
+
(".svn/entries", "Subversion .svn/entries exposed", Severity.HIGH, r"^\d+\s|^svn:", "NIST 800-53 SC-28"),
|
|
88
|
+
(".svn/wc.db", "Subversion working copy DB exposed", Severity.HIGH, r"^SQLite", "NIST 800-53 SC-28"),
|
|
89
|
+
(".hg/store/00manifest.i", "Mercurial repo manifest exposed", Severity.HIGH, None, "NIST 800-53 SC-28"),
|
|
90
|
+
(".bzr/branch-format", "Bazaar branch format exposed", Severity.HIGH, r"Bazaar", "NIST 800-53 SC-28"),
|
|
91
|
+
# Medium - useful enumeration for attacker
|
|
92
|
+
(
|
|
93
|
+
".DS_Store",
|
|
94
|
+
"macOS .DS_Store exposed (reveals directory structure)",
|
|
95
|
+
Severity.MEDIUM,
|
|
96
|
+
r"^Bud1|^\x00\x00\x00\x01Bud1",
|
|
97
|
+
"CWE-538",
|
|
98
|
+
),
|
|
99
|
+
("Thumbs.db", "Windows Thumbs.db exposed", Severity.MEDIUM, None, "CWE-538"),
|
|
100
|
+
("phpinfo.php", "phpinfo() output exposed", Severity.MEDIUM, r"PHP Version|phpinfo\(\)", "CWE-200"),
|
|
101
|
+
("info.php", "PHP info exposed", Severity.MEDIUM, r"PHP Version|phpinfo\(\)", "CWE-200"),
|
|
102
|
+
("test.php", "Test PHP file exposed", Severity.MEDIUM, r"PHP Version|phpinfo\(\)", "CWE-200"),
|
|
103
|
+
# Low - infrastructure metadata
|
|
104
|
+
(".idea/workspace.xml", "JetBrains IDE config exposed", Severity.LOW, r"<project", "CWE-200"),
|
|
105
|
+
(".vscode/settings.json", "VS Code config exposed", Severity.LOW, r"^\s*\{", "CWE-200"),
|
|
106
|
+
(".gitlab-ci.yml", "GitLab CI config exposed", Severity.LOW, r"stages:|script:", "CWE-200"),
|
|
107
|
+
(".github/workflows/", "GitHub Actions workflows dir exposed", Severity.LOW, None, "CWE-200"),
|
|
108
|
+
("Dockerfile", "Dockerfile exposed", Severity.LOW, r"^FROM\s+", "CWE-200"),
|
|
109
|
+
("docker-compose.yml", "docker-compose.yml exposed", Severity.LOW, r"^version:|services:", "CWE-200"),
|
|
110
|
+
("composer.json", "PHP composer.json exposed on web root", Severity.LOW, r'"name":\s*"', "CWE-200"),
|
|
111
|
+
("package.json", "Node package.json exposed on web root", Severity.LOW, r'"name":\s*"', "CWE-200"),
|
|
112
|
+
("requirements.txt", "Python requirements.txt exposed", Severity.LOW, r"^[a-zA-Z][a-zA-Z0-9_.-]*[=<>]", "CWE-200"),
|
|
113
|
+
("Gemfile", "Ruby Gemfile exposed", Severity.LOW, r"^source\s+['\"]https", "CWE-200"),
|
|
114
|
+
("config.yml", "Generic config.yml exposed", Severity.LOW, None, "CWE-200"),
|
|
115
|
+
("config.json", "Generic config.json exposed", Severity.LOW, r"^\s*\{", "CWE-200"),
|
|
116
|
+
("README.md", "README.md exposed on production web root", Severity.LOW, r"^#\s+", "CWE-200"),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _verify_fingerprint(body_text: str, fingerprint_re: str | None, content_type: str) -> bool:
|
|
121
|
+
"""Return True if response body looks like the expected file type."""
|
|
122
|
+
if fingerprint_re is None:
|
|
123
|
+
# No fingerprint check requested (e.g. binary archives) — trust the 200
|
|
124
|
+
return True
|
|
125
|
+
# Inspect first 2 KiB
|
|
126
|
+
sample = body_text[:2048]
|
|
127
|
+
if re.search(fingerprint_re, sample, re.MULTILINE | re.IGNORECASE):
|
|
128
|
+
return True
|
|
129
|
+
# If server claimed it's HTML / SPA, treat as false positive
|
|
130
|
+
if "text/html" in content_type.lower():
|
|
131
|
+
return False
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: list[str] | None = None) -> int:
|
|
136
|
+
parser = argparse.ArgumentParser(description="Probe for exposed secrets files")
|
|
137
|
+
parser.add_argument("url")
|
|
138
|
+
parser.add_argument("--authorized", action="store_true")
|
|
139
|
+
parser.add_argument("--output", default=None)
|
|
140
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
141
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
142
|
+
parser.add_argument("--timeout", type=float, default=10.0)
|
|
143
|
+
parser.add_argument("--paths-file", default=None, help="Custom probe set (one path per line); replaces default")
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--check-only", action="store_true", help="Skip body fingerprint check (treat any 200 as a finding)"
|
|
146
|
+
)
|
|
147
|
+
args = parser.parse_args(argv)
|
|
148
|
+
|
|
149
|
+
require_authorization(args.url, args.authorized)
|
|
150
|
+
|
|
151
|
+
base = args.url.rstrip("/") + "/"
|
|
152
|
+
sess = make_session(timeout=args.timeout)
|
|
153
|
+
findings: list[Finding] = []
|
|
154
|
+
|
|
155
|
+
if args.paths_file:
|
|
156
|
+
paths = Path(args.paths_file).read_text().splitlines()
|
|
157
|
+
probe_set = [
|
|
158
|
+
(p.strip(), f"Custom path exposed: {p.strip()}", Severity.MEDIUM, None, "custom")
|
|
159
|
+
for p in paths
|
|
160
|
+
if p.strip()
|
|
161
|
+
]
|
|
162
|
+
else:
|
|
163
|
+
probe_set = PROBES
|
|
164
|
+
|
|
165
|
+
for path, title, sev, fingerprint, control in probe_set:
|
|
166
|
+
url = base + path.lstrip("/")
|
|
167
|
+
resp = safe_get(sess, url, timeout=args.timeout, allow_redirects=False)
|
|
168
|
+
if resp is None or resp.status_code != 200:
|
|
169
|
+
continue
|
|
170
|
+
body = resp.text or ""
|
|
171
|
+
ctype = resp.headers.get("Content-Type", "")
|
|
172
|
+
if not args.check_only and not _verify_fingerprint(body, fingerprint, ctype):
|
|
173
|
+
continue
|
|
174
|
+
evidence = (("status_code", 200), ("content_length", len(resp.content or b"")), ("content_type", ctype))
|
|
175
|
+
findings.append(
|
|
176
|
+
Finding(
|
|
177
|
+
skill_id=SKILL_ID,
|
|
178
|
+
title=title,
|
|
179
|
+
severity=sev,
|
|
180
|
+
target=url,
|
|
181
|
+
detail=(
|
|
182
|
+
f"GET {url} returned 200 with content matching the expected "
|
|
183
|
+
f"signature of {path!r}. The file is publicly reachable "
|
|
184
|
+
"and likely contains sensitive data."
|
|
185
|
+
),
|
|
186
|
+
remediation=(
|
|
187
|
+
f"Configure the web server to deny requests to {path!r} and "
|
|
188
|
+
"the directory it lives in. See references/PLAYBOOK.md for "
|
|
189
|
+
"nginx / Apache / Caddy / ALB snippets per category."
|
|
190
|
+
),
|
|
191
|
+
cwe_id=None,
|
|
192
|
+
affected_control=control,
|
|
193
|
+
evidence=evidence,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Severity floor
|
|
198
|
+
floor = Severity(args.min_severity)
|
|
199
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
200
|
+
|
|
201
|
+
target_display = args.url
|
|
202
|
+
emit(findings, args.output, args.format, target_display)
|
|
203
|
+
return exit_code(findings)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
sys.exit(main())
|