package main import ( "flag" "log" "net" "strings" ) // TS3 Proxy to capture clientinit parameters func main() { var localAddr string var serverAddr string flag.StringVar(&localAddr, "listen", ":9988", "Local listen address") flag.StringVar(&serverAddr, "server", "localhost:9987", "Real TeamSpeak server address") flag.Parse() // Resolve addresses localUDP, err := net.ResolveUDPAddr("udp", localAddr) if err != nil { log.Fatalf("Invalid local address: %v", err) } serverUDP, err := net.ResolveUDPAddr("udp", serverAddr) if err != nil { log.Fatalf("Invalid server address: %v", err) } // Listen on local port conn, err := net.ListenUDP("udp", localUDP) if err != nil { log.Fatalf("Failed to listen: %v", err) } defer conn.Close() log.Printf("Proxy listening on %s, forwarding to %s", localAddr, serverAddr) log.Println("Connect your Real TeamSpeak Client to 'localhost:9988' to capture credentials.") // Map client addresses to server connections (for multiple clients support, simplifying for 1) // Simple forwarder: We only expect one client for this task. var clientAddr *net.UDPAddr buf := make([]byte, 2048) for { n, addr, err := conn.ReadFromUDP(buf) if err != nil { log.Printf("Read error: %v", err) continue } // Check if packet is from client or server if clientAddr == nil || addr.String() != clientAddr.String() { // New client or client packet // If it's from our known server (unlikely as we don't bind to serverAddr), it's a client. // But wait, we are listening on localUDP. // We need to dial the server to forward. // We'll use a separate socket to talk to the server to keep track of sessions? // Simplest UDP proxy: // 1. Receive from Client -> Send to Server (using a dialer) // 2. Receive from Server -> Send to Client // Let's reset for new clients to keep it simple clientAddr = addr handleClientPacket(conn, serverUDP, buf[:n], addr) } else { // Known client handleClientPacket(conn, serverUDP, buf[:n], addr) } } } // We need a persistent connection to the server to receive responses // In a simple loop, we can't easily run two blocking reads (from client and from server) without goroutines. // Let's structure: // Main loop reads from Client (ListenUDP). // On first packet, start a Goroutine that Dials the Server. // That goroutine reads from Server and writes to Client. // Main loop writes to Server using that connection. var serverConn *net.UDPConn func handleClientPacket(clientConn *net.UDPConn, serverAddr *net.UDPAddr, data []byte, clientAddr *net.UDPAddr) { // Inspect Packet for clientinit strings (Naive text search) // clientinit is encrypted in standard flow? // WAIT. If we proxy, the client and server negotiate encryption. // We CANNOT decrypt the traffic unless we perform a MitM attack on the Diffie-Hellman handshake. // TS3 uses ECDH. We cannot derive the shared secret just by passively observing. // We would need to: // 1. Intercept `initivexpand2` (Step 2/3/4?) // 2. Replace the Server's Public Key with OUR Public Key. // 3. Negotiate SharedSecret1 with Client. // 4. Negotiate SharedSecret2 with Server (using our Private Key). // 5. Decrypt Client packet -> Re-encrypt for Server. // This is significantly complex. // BUT, `clientinit` might be sent *plaintext* in some legacy modes? // No, the user's issue is specifically about the encrypted handshake. // However, `client_version` is sometimes sent in cleartext in the INIT1 packet? // No, INIT1 is just 4 timestamps + random bytes. // WAIT. The very first packet is valid? // `clientinit` is sent AFTER the handshake (Step 6). It is definitely encrypted. // If the user connects with a real client, the client calculates the SharedSecret using the REAL server's key. // We verify the packets pass through. // WE CANNOT READ THE PAYLOAD without a full MitM (replacing keys). // IS THERE ANOTHER WAY? // Does the client send version in clear text anywhere? // Maybe in the UserAgent? (UDP doesn't have headers like HTTP) // Proposal: Implement Full MitM. // We already have the ECDH logic implemented in Go! // We can act as the "Server" to the Client, and "Client" to the Server. // 1. Client connects to Proxy. Proxy acts as Server. // 2. Proxy sends its OWN Identity/Key to Client. // 3. Proxy connects to Real Server. // 4. Proxy completes handshake with Real Server. // 5. Client completes handshake with Proxy. // 6. Proxy decrypts Client's `clientinit`, LOGS IT, then re-encrypts for Server. // This is the chosen path. It reuses our `handshake.go` and `license.go` logic. inspectPacket(data) // Forwarding logic placeholder (Passive won't work for decryption) } func inspectPacket(data []byte) { // Check for "clientinit" ASCII pattern s := string(data) if strings.Contains(s, "clientinit") { log.Printf("[!] FOUND POSSIBLE CLIENTINIT (Plaintext?): %s", s) } }