diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c24781c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:alpine + +COPY . /app + +WORKDIR /app + +RUN apk add --no-cache make build-base libpcap-dev openssh-client tcpdump + +RUN go mod download +RUN go build ./cmd/pcap-broker + +ENTRYPOINT ["./pcap-broker"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7eae809 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Fox-IT B.V. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5afdaf6 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# pcap-broker + +`pcap-broker` is a tool to capture network traffic and make this available to one or more clients via PCAP-over-IP. + +PCAP-over-IP can be useful in situations where low latency is a priority, for example during Attack and Defend CTFs. +More information on PCAP-over-IP can be found here: + + * https://www.netresec.com/?page=Blog&month=2022-08&post=What-is-PCAP-over-IP + +`pcap-broker` supports the following features: + + * Distributing packet data to one or more PCAP-over-IP listeners + * Execute a command to capture traffic, usually `tcpdump` (expects stdout to be pcap data) + * `pcap-broker` will exit if the capture command exits + +## Building + +To build `pcap-broker`: + +```shell +$ go build ./cmd/pcap-broker +$ ./pcap-broker --help +``` + +Or you can build the Docker container: + +```shell +$ docker build -t pcap-broker . +$ docker run -it pcap-broker --help +``` + +## Running + +```shell +$ ./pcap-broker --help +Usage of ./pcap-broker: + -cmd string + command to execute for pcap data (eg: tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -) + -listen string + listen address for pcap-over-ip (eg: localhost:4242) + -n disable reverse lookup of connecting PCAP-over-IP client IP address +``` + +Arguments can be passed via commandline: + +```shell +$ ./pcap-broker -cmd "sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -" +``` + +Or alternatively via environment variables: + +```shell +LISTEN_ADDRESS=:4242 PCAP_COMMAND='sudo tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -' ./pcap-broker +``` + +Using environment variables is useful when you are using `pcap-broker` in a Docker setup. + +Now you can connect to it via TCP and stream PCAP data using `nc` and `tcpdump`: + +```shell +$ nc -v localhost 4242 | tcpdump -nr - +``` + +Or use a tool that natively supports PCAP-over-IP, for example `tshark`: + +```shell +$ tshark -i TCP@localhost:4242 +``` + +# Acquiring PCAP data over SSH + +One use case is to acquire PCAP from a remote machine over SSH and make this available via PCAP-over-IP. +Such a use case, including an example SSH command to bootstrap this, has been documented in the `docker-compose.yml.example` file: + +```yaml +version: "3.2" + +services: + pcap-broker-remote-host: + image: pcap-broker:latest + restart: always + volumes: + # mount local user's SSH key into container + - ~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro + ports: + # make the PCAP-over-IP port also available on the host on port 4200 + - 4200:4242 + environment: + # Command to SSH into remote-host and execute tcpdump and filter out it's own SSH client traffic + PCAP_COMMAND: ssh root@remote-host -o StrictHostKeyChecking=no 'IFACE=$$(ip route show to default | grep -Po1 "dev \K\w+") && BPF=$$(echo $$SSH_CLIENT | awk "{printf \"not (host %s and port %s and %s)\", \$$1, \$$2, \$$3;}") && tcpdump -U --immediate-mode -ni $$IFACE $$BPF -s 65535 -w -' + LISTEN_ADDRESS: "0.0.0.0:4242" +``` + +## Background + +This tool was initially written for Attack & Defend CTF purposes but can be useful in other situations where low latency is preferred, or whenever a no-nonsense PCAP-over-IP server is needed. During the CTF that Fox-IT participated in, `pcap-broker` allowed the Blue Team to capture network data once and disseminate this to other tools that natively support PCAP-over-IP, such as: + +* [Arkime](https://arkime.com/) +* [Tulip](https://github.com/OpenAttackDefenseTools/tulip) (after we did some custom patches) +* WireShark's dumpcap and tshark diff --git a/cmd/pcap-broker/main.go b/cmd/pcap-broker/main.go new file mode 100644 index 0000000..4f1a741 --- /dev/null +++ b/cmd/pcap-broker/main.go @@ -0,0 +1,174 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net" + "os" + "os/exec" + "time" + + "github.com/google/shlex" + + "github.com/google/gopacket" + "github.com/google/gopacket/pcap" + "github.com/google/gopacket/pcapgo" +) + +type PcapClient struct { + writer *pcapgo.Writer + totalPackets uint64 + totalBytes uint64 +} + +func lookupHostnameWithTimeout(addr net.Addr, timeout time.Duration) (string, string, error) { + // Extract the IP address and port from the Addr object + tcpAddr, ok := addr.(*net.TCPAddr) + if !ok { + return "", "", fmt.Errorf("unsupported address type: %T", addr) + } + ip := tcpAddr.IP.String() + port := fmt.Sprintf("%d", tcpAddr.Port) + + // Create a new context with the given timeout + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Create a new Resolver and perform the IP lookup with the given context + resolver := net.Resolver{} + names, err := resolver.LookupAddr(ctx, ip) + if err != nil { + return "", "", err + } + if len(names) == 0 { + return "", "", fmt.Errorf("no hostnames found for %s", ip) + } + + // Return the first IP address found and the original port + return names[0], port, nil +} + +func main() { + + pcapCommand := flag.String("cmd", "", "command to execute for pcap data (eg: tcpdump -i eth0 -n --immediate-mode -s 65535 -U -w -)") + listenAddress := flag.String("listen", "", "listen address for pcap-over-ip (eg: localhost:4242)") + noReverseLookup := flag.Bool("n", false, "disable reverse lookup of connecting PCAP-over-IP client IP address") + flag.Parse() + + if *pcapCommand == "" { + *pcapCommand = os.Getenv("PCAP_COMMAND") + if *pcapCommand == "" { + log.Fatalf("Error: PCAP_COMMAND or -cmd not set, see --help for usage") + } + } + + if *listenAddress == "" { + *listenAddress = os.Getenv("LISTEN_ADDRESS") + if *listenAddress == "" { + *listenAddress = "localhost:4242" + } + } + + log.Printf("config PCAP_COMMAND = %q", *pcapCommand) + log.Printf("config LISTEN_ADDRESS = %q", *listenAddress) + + // Create connections to PcapClient map + var connMap = map[net.Conn]PcapClient{} + + // Create a pipe for the command to write to, will be read by pcap.OpenOfflineFile + rStdout, wStdout, err := os.Pipe() + if err != nil { + log.Fatal(err) + } + + // Important or these will eventually be garbage collected and the pipe will close + defer rStdout.Close() + defer wStdout.Close() + + // Acquire pcap data + args, err := shlex.Split(*pcapCommand) + if err != nil { + log.Fatal(err) + } + cmd := exec.Command(args[0], args[1:]...) + log.Printf("cmd = %v", cmd.Args) + cmd.Stdout = wStdout + cmd.Stderr = os.Stderr + + err = cmd.Start() + if err != nil { + log.Fatal(err) + } + + log.Printf("PID %v", cmd.Process.Pid) + go func() { + err := cmd.Wait() + if err != nil { + log.Fatal("Process exited with error: ", err) + } + log.Printf("process exited") + os.Exit(0) + }() + + // Read from process stdout pipe + handle, err := pcap.OpenOfflineFile(rStdout) + if err != nil { + log.Fatal(err) + } + + packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) + packetSource.Lazy = true + packetSource.NoCopy = true + go processPackets(packetSource, connMap) + + log.Printf("PCAP-over-IP server listening on %v", *listenAddress) + l, err := net.Listen("tcp", *listenAddress) + if err != nil { + log.Fatal(err) + } + + for { + conn, err := l.Accept() + if err != nil { + log.Fatal(err) + } + + if *noReverseLookup { + log.Printf("PCAP-over-IP connection from %v", conn.RemoteAddr()) + } else { + ip, port, err := lookupHostnameWithTimeout(conn.RemoteAddr(), 100*time.Millisecond) + if err != nil { + log.Printf("PCAP-over-IP connection from %v", conn.RemoteAddr()) + } else { + log.Printf("PCAP-over-IP connection from %s:%s", ip, port) + } + } + + writer := pcapgo.NewWriter(conn) + + // Write pcap header + writer.WriteFileHeader(65535, handle.LinkType()) + + // add connection to map + connMap[conn] = PcapClient{writer: writer} + } +} + +func processPackets(packetSource *gopacket.PacketSource, connMap map[net.Conn]PcapClient) { + for packet := range packetSource.Packets() { + for conn, stats := range connMap { + ci := packet.Metadata().CaptureInfo + err := stats.writer.WritePacket(ci, packet.Data()) + if err != nil { + log.Println(err) + delete(connMap, conn) + conn.Close() + continue + } + stats.totalPackets += 1 + stats.totalBytes += uint64(ci.CaptureLength) + } + } +} diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..825a281 --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,16 @@ +version: "3.2" + +services: + pcap-broker-remote-host: + image: pcap-broker:latest + restart: always + volumes: + # mount local user's SSH key into container + - ~/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro + ports: + # make the PCAP-over-IP port also available on the host on port 4200 + - 4200:4242 + environment: + # Command to SSH into remote-host and execute tcpdump and filter out it's own SSH client traffic + PCAP_COMMAND: ssh root@remote-host -o StrictHostKeyChecking=no 'IFACE=$$(ip route show to default | grep -Po1 "dev \K\w+") && BPF=$$(echo $$SSH_CLIENT | awk "{printf \"not (host %s and port %s and %s)\", \$$1, \$$2, \$$3;}") && tcpdump -U --immediate-mode -ni $$IFACE $$BPF -s 65535 -w -' + LISTEN_ADDRESS: "0.0.0.0:4242" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2d6d6fd --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module pcap-broker + +go 1.20 + +require ( + github.com/google/gopacket v1.1.19 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b7cadaf --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=