← Back to Writeups
HTBN/ANetwork

proxyport

XESXOR8/23/20266 min read
#network#htb#n/a

proxyport

Platform: D3C2026 | Category: Network | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-25 | Status: Solved Techniques: tcp_half_close_fingerprinting, protocol_automation, parallel_pow

Summary

Task: A TLS guide service asks for a SHA-256 proof of work, then requires distinguishing GOST from FRP across 20 forwarding rounds. Solution: Send one HTTP request, half-close the TLS write side, and classify whether the Caddy response survives.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: d3c2026 | ID: 20260725_d3c2026_proxyport
  • Tags: tls, gost, tcp_proxy, frp, caddy, proof_of_work
  • Indicators: 26-leading-zero-bit SHA-256 proof of work, proxy implementation must be identified through a shared TLS forwarding endpoint, response behavior changes after TLS CloseWrite
  • Source: 20260725_d3c2026_proxyport.md

Foothold

Vulnerability / Misconfiguration

  1. Tcp_half_close_fingerprinting
  2. Protocol_automation
  3. Parallel_pow
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • tcp_half_close_fingerprinting
  • protocol_automation
  • parallel_pow
  • Tags: tls, gost, tcp_proxy, frp, caddy, proof_of_work

Original Writeup

<details><summary>Click to expand original content</summary>

Description

follow guide in nc port to complete the challenge

The challenge exposes two TLS services. The interactive guide presents a proof of work and then 20 rounds, while the forwarding service changes between GOST and FRP. In each round, the goal is to identify the active forwarding implementation using no more than five probes and submit either gost or frp.

Analysis

Protocol and constraints

The interactive service first requests a suffix such that:

SHA256(prefix || suffix)

has 26 leading zero bits. After accepting the proof of work, it starts 20 timed rounds. A separate TLS endpoint forwards traffic to a Caddy HTTP server, and each round asks which proxy implementation is active.

An ordinary HTTP request was not enough. Both implementations returned the same 136-byte Caddy response, and their approximately one-second response times overlapped. Therefore, response content and timing under normal connection shutdown did not provide a classifier.

Half-close differential

The decisive difference appears when the client closes only its sending direction:

  1. Open a TLS connection to the forwarding endpoint.
  2. Send a complete HTTP/1.0 request.
  3. Call tls.Conn.CloseWrite() immediately.
  4. Keep reading from the connection.

The outcomes form an exact binary split:

Result after CloseWrite()Classification
136-byte HTTP response containing body caddyGOST
Zero-byte clean EOFFRP

This behavior follows from the proxies' stream-copy semantics. GOST propagates the client EOF as a write half-close toward the destination while preserving the reverse path, so Caddy can still send its response. FRP uses github.com/fatedier/golib/io.Join; when one copy direction reaches EOF, its cleanup closes both streams and tears down the reverse path before the response returns.

Only one probe is needed per round, comfortably below the five-probe limit. It also avoids the timeout encountered when trying several sequential probes.

Alternative theories were unnecessary. Source-port reuse is not observable through the TLS frontend and fixed HTTP response, while FRP tcpMux head-of-line blocking remained speculative after the deterministic half-close classifier succeeded.

Solution

The solver performs the following steps:

  1. Connect to the interactive TLS service and parse the PoW prefix.
  2. Search in parallel for a suffix satisfying the 26-zero-bit SHA-256 condition.
  3. Submit the PoW solution.
  4. For every ready message, make one half-close probe against the forwarding endpoint.
  5. Answer gost if the returned data contains caddy; otherwise answer frp.
package main

import (
	"bufio"
	"crypto/sha256"
	"crypto/tls"
	"fmt"
	"io"
	"runtime"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

const (
	guideHost   = "rqzjeuee3jgpucmo6e6lj3wlxae.cloud.d3c.tf:443"
	forwardHost = "r7rpppeo2by2izhw6nnuf5sprvy.cloud.d3c.tf:443"
)

func dialTLS(address string) (*tls.Conn, error) {
	host := strings.Split(address, ":")[0]
	return tls.Dial("tcp", address, &tls.Config{ServerName: host})
}

func solvePoW(prefix string) string {
	var found atomic.Bool
	answer := make(chan string, 1)
	workers := runtime.NumCPU()
	var wg sync.WaitGroup

	for worker := 0; worker < workers; worker++ {
		wg.Add(1)
		go func(start int) {
			defer wg.Done()
			for i := uint64(start); !found.Load(); i += uint64(workers) {
				suffix := fmt.Sprintf("%x", i)
				h := sha256.Sum256([]byte(prefix + suffix))
				// Three zero bytes plus the top two zero bits = 26 bits.
				if h[0] == 0 && h[1] == 0 && h[2] == 0 && h[3] < 0x40 {
					if found.CompareAndSwap(false, true) {
						answer <- suffix
					}
					return
				}
			}
		}(worker)
	}

	suffix := <-answer
	wg.Wait()
	return suffix
}

func classify() (string, int, error) {
	c, err := dialTLS(forwardHost)
	if err != nil {
		return "", 0, err
	}
	defer c.Close()

	request := "GET / HTTP/1.0\r\nHost: x\r\n\r\n"
	if _, err := c.Write([]byte(request)); err != nil {
		return "", 0, err
	}
	if err := c.CloseWrite(); err != nil {
		return "", 0, err
	}

	_ = c.SetReadDeadline(time.Now().Add(2500 * time.Millisecond))
	response, readErr := io.ReadAll(c)
	if strings.Contains(string(response), "caddy") {
		return "gost", len(response), nil
	}
	// The observed FRP case is a clean EOF with zero response bytes. A read
	// error is retained so unexpected network failures do not pass silently.
	if readErr != nil {
		return "", len(response), readErr
	}
	return "frp", len(response), nil
}

func main() {
	c, err := dialTLS(guideHost)
	if err != nil {
		panic(err)
	}
	defer c.Close()

	r := bufio.NewReader(c)
	var prefix string

	// Read the PoW banner.
	for {
		line, err := r.ReadString('\n')
		if err != nil {
			panic(err)
		}
		fmt.Print(line)
		if strings.HasPrefix(line, "prefix: ") {
			prefix = strings.TrimSpace(strings.TrimPrefix(line, "prefix: "))
		}
		if strings.HasPrefix(line, "submit:") {
			break
		}
	}

	suffix := solvePoW(prefix)
	fmt.Fprintf(c, "pow %s\n", suffix)

	var pending string
	for {
		line, err := r.ReadString('\n')
		if len(line) != 0 {
			fmt.Print(line)
		}
		if strings.Contains(line, "ready") {
			pending, _, err = classify()
			if err != nil {
				panic(err)
			}
		}
		if pending != "" && strings.Contains(line, "submit `answer") {
			fmt.Fprintf(c, "answer %s\n", pending)
			pending = ""
		}
		if err != nil {
			return
		}
	}
}

Run it with:

go run probe.go

Verification

The accepted run repeatedly showed the deterministic split and ended successfully:

round 03/20 ready
[+] halfclose=0 ok=false total=1.03419575s n=0 err=<nil>
[+] classifier successes=0 answer=frp
recorded
round 04/20 ready
[+] halfclose=0 ok=true total=930.934625ms n=136 err=<nil>
[+] classifier successes=1 answer=gost
recorded
...
round 20/20 ready
[+] halfclose=0 ok=true total=936.130917ms n=136 err=<nil>
[+] classifier successes=1 answer=gost
recorded
All answers are correct.
<flag line redacted>

All 20 classifications were accepted.

Failure Analysis

  • Normal HTTP timing: Eliminated because both proxies produced the same response and their latency distributions overlapped.
  • Several sequential probes: Unnecessary and harmful because the extra probes risked exceeding the round deadline.
  • Source-port reuse: Not observable through the TLS-terminating frontend and fixed backend response.
  • FRP tcpMux or head-of-line behavior: Plausible but speculative; no concurrency-based classifier was needed once half-close semantics gave an exact split.
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR