Instructions
Luciafer started the hack of the Lytton Labs victim by performing a port scan.
Which TCP ports are open on the victim’s machine? Enter the flag as the open ports, separated by commas, no spaces, in numerical order. Disregard port numbers >= 16384.
Example: flag{80,110,111,143,443,2049}
Use the PCAP from LYTTON LABS 01 - Monstrum ex Machina.
Prerequisites
Python requirements.txt
:
pyshark
Golang packages:
gopacket
Solution
Find the hosts
The first challenge is to identify the attacker IP and target for the port scan. I did this by determining the frequency of unique destination port hits per source and destination IP. We can safely ignore non-tcp packets and packets with destination ports > 16384 per the instructions. Our wireshark filter is also looking for tcp packets with only the SYN flag set so that we only have to look through the initial packets in the handshake.
def identify_targets():
filter = "tcp.flags eq 0x02 and tcp.dstport <= 16384"
# SYN=1 and everything else 0
cap = pyshark.FileCapture(pcap_file, keep_packets=False, display_filter=filter)
unique_data_flows = {}
for pkt in cap:
dstport = int(pkt.tcp.dstport)
key = f"{pkt.ip.src}->{pkt.ip.dst}"
if key in unique_data_flows:
unique_data_flows[key].add(dstport)
else:
unique_data_flows[key] = {dstport}
for k, v in unique_data_flows.items():
print(f"{k}: {len(v)}")
The results of the analysis is that there is only one set of source and destination IPs with more than 2 unique destination ports:
192.168.100.106->192.168.100.103: 15779
Python performance for building the set of unique flows and destination ports was poor. I rewrote an implementation in Golang using gopacket and bpf instead of Python and pyshark.
func findAttacker(pcapFile string) gopacket.Flow {
var (
handle *pcap.Handle
err error
)
// Open file instead of device
handle, err = pcap.OpenOffline(pcapFile)
if err != nil {
log.Fatal(err)
}
defer handle.Close()
// Set filter
var filter string = "tcp[tcpflags] == 0x02 and tcp portrange 1-16384"
err = handle.SetBPFFilter(filter)
if err != nil {
log.Fatal(err)
}
// Create storage for unique flows
type EndpointSet map[gopacket.Endpoint]struct{}
uniqueFlows := make(map[gopacket.Flow]EndpointSet)
// Loop through packets
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
var netFlow gopacket.Flow
var dstPort gopacket.Endpoint
// IP
if net := packet.NetworkLayer(); net != nil {
netFlow = net.NetworkFlow()
}
// TCP
// We filtered to only tcp in transport in the bpf filters
if tcp := packet.TransportLayer(); tcp != nil {
dstPort = tcp.TransportFlow().Dst()
}
// Add flow if it doesn't exist
if _, exists := uniqueFlows[netFlow]; !exists {
uniqueFlows[netFlow] = make(EndpointSet)
}
// Add dst port to set if not already present
if _, exists := uniqueFlows[netFlow][dstPort]; !exists {
uniqueFlows[netFlow][dstPort] = struct{}{}
}
}
// Print results and record flow with the largest # of unique dst ports
var largestFlow gopacket.Flow
var largestDstPorts int = 0
for net, uniquePorts := range uniqueFlows {
if len(uniquePorts) > largestDstPorts {
largestFlow = net
largestDstPorts = len(uniquePorts)
}
}
fmt.Println(largestFlow)
return largestFlow
}
This provided much improved performance. (~1 min in the Python version vs ~0.2 seconds in the Go implementation)
Find the open ports
Now that we have the attacker and victim we can search for the valid responses from the port scan. We just need to filter for SYN ACK responses from the victim. The replies for the dst port of the attacker -> victim flow will originate from the same tcp src port on the victim -> attacker flow.
func findReplies(pcapFile string, flow gopacket.Flow) {
var (
handle *pcap.Handle
err error
)
// Open file instead of device
handle, err = pcap.OpenOffline(pcapFile)
if err != nil {
log.Fatal(err)
}
defer handle.Close()
// Set filter
// tcpflags 0x12 is only SYN and ACK flags set
filter := fmt.Sprintf("tcp[tcpflags] == 0x12 and tcp portrange 1-16384 and src host %v and dst host %v", flow.Dst(), flow.Src())
err = handle.SetBPFFilter(filter)
if err != nil {
log.Fatal(err)
}
uniquePorts := make(map[gopacket.Endpoint]struct{})
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
// TCP
// We filtered to only tcp in transport in the bpf filters
if tcp := packet.TransportLayer(); tcp != nil {
uniquePorts[tcp.TransportFlow().Src()] = struct{}{}
}
}
// Convert uniqueEndpoints from set to slice so we can sort it
ports := make([]gopacket.Endpoint, 0, len(uniquePorts))
for port, _ := range uniquePorts {
ports = append(ports, port)
}
// Sort to ensure proper order
sort.Slice(ports, func(i, j int) bool { return ports[i].LessThan(ports[j]) })
// Print flag format
fmt.Printf("Discovered ports: ")
for index, port := range ports {
fmt.Printf("%v", port)
if index+1 != len(ports) {
fmt.Printf(",")
}
}
fmt.Println()
}
Discovered ports: 21,135,139,445,3389