Talos Vulnerability Report

TALOS-2024-2005

OpenPLC OpenPLC_v3 OpenPLC Runtime EtherNet/IP parser stack-based buffer overflow vulnerability

September 18, 2024
CVE Number

CVE-2024-34026

SUMMARY

A stack-based buffer overflow vulnerability exists in the OpenPLC Runtime EtherNet/IP parser functionality of OpenPLC _v3 b4702061dc14d1024856f71b4543298d77007b88. A specially crafted EtherNet/IP request can lead to remote code execution. An attacker can send a series of EtherNet/IP requests to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

OpenPLC _v3 b4702061dc14d1024856f71b4543298d77007b88

PRODUCT URLS

OpenPLC_v3 - https://github.com/thiagoralves/OpenPLC_v3

CVSSv3 SCORE

9.0 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-121 - Stack-based Buffer Overflow

DETAILS

OpenPLC is an open-source programmable logic controller (PLC) designed to provide a low cost option for automation. The platform consists of two parts: the Runtime and the Editor. The Runtime can be deployed on a variety of platforms including Windows, Linux, and various microcontrollers. Common uses for OpenPLC include home automation and industrial security research. OpenPLC supports communication across a variety of protocols, including Modbus and EtherNet/IP. The runtime additionally provides limited support for PCCC transported across EtherNet/IP.

When a request is sent to the OpenPLC EtherNet/IP server it is processed in a variety of ways depending on what message is provided. As long as a rough outline of a known ENIP message can be identified the data is passed on for further processing. Common ENIP commands such as SendRRData and SendUnitData have dedicated parsing functions that attempt to verify the structure of their respective requests. ENIP command codes that are either invalid or unimplemented end up getting funneled to an else block that is intended to log the message and move on.

To log any message, OpenPLC allocates a stack buffer of 1000 bytes to hold the message text.

int processEnipMessage(unsigned char *buffer, int buffer_size)
{	
    // initialize logging system
    char log_msg[1000];
    char *p = log_msg;
    ...

When an invalid or unimplemented ENIP message is encountered, this log_msg buffer is filled with a short error message followed by the bytes of the message itself in two-digit hex bytes separated by a space character.

int processEnipMessage(unsigned char *buffer, int buffer_size)
{	
...
    if (header.command[0] == 0x6f)	// Send RR Data
    {
        ...
    }
    else
    {
        p += sprintf(p, "Unknown EtherNet/IP request: ");
        for (int i = 0; i < buffer_size; i++)
        {
            p += sprintf(p, "%02x ", (unsigned char)buffer[i]);
        }
        p += sprintf(p, "\n");
        log(log_msg);

        return -1;
    }

The buffer and buffer_size variables are passed along from the underlying socket calls. Within the boundaries of what is considered by the OpenPLC runtime to be an ENIP message, both variables are able to be controlled by the user.

Thread 23 "openplc" hit Breakpoint 1, 0x0000aaaad887fe94 in processEnipMessage(unsigned char*, int) ()                                       
(gdb) i r
x0             0xffff9a4bc0e8      281473270399208   // *buffer
x1             0x203a              8250              // bufferSize
...
sp             0xffff9a4bbc80      0xffff9a4bbc80
pc             0xaaaad887fe94      0xaaaad887fe94 <processEnipMessage(...
cpsr           0x60001000          [ EL=0 BTYPE=0 SSBS C Z ]
fpsr           0x0                 [ ]
fpcr           0x0                 [ RMode=0 ]
pauth_dmask    0x7f000000000000    35747322042253312
pauth_cmask    0x7f000000000000    35747322042253312

At no point is any verification performed to ensure that the bytes in the buffer, once formatted into two digits and a space per byte, will take up less than the allocated 1000 bytes.

By sending an ENIP request with an unsupported command code, a valid encapsulation header, and at least 500 total bytes, it is possible to write past the boundary of the allocated log_msg buffer and corrupt the stack. Depending on the security precautions enabled on the host in question, further exploitation could be possible.

Crash Information

Thread 8 "openplc" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xffff98a5f0e0 (LWP 800300)]
0x0000ffff9ab17394 in __GI__IO_default_xsputn (n=<optimized out>, data=<optimized out>, f=<optimized out>) at ./libio/genops.c:394
394     ./libio/genops.c: No such file or directory.
(gdb) bt
#0  0x0000ffff9ab17394 in __GI__IO_default_xsputn (n=<optimized out>, 
    data=<optimized out>, f=<optimized out>) at ./libio/genops.c:394
#1  __GI__IO_default_xsputn (f=0xffff98a5b0a8, data=<optimized out>, n=2)
    at ./libio/genops.c:370
#2  0x0000ffff9ab011d0 in outstring_func (done=0, length=<optimized out>, 
    string=<optimized out>, s=0xffff98a5b0a8) at ../libio/libioP.h:947
#3  __vfprintf_internal (s=s@entry=0xffff98a5b0a8, 
    format=format@entry=0xaaaac7fb4458 "%02x ", ap=..., mode_flags=mode_flags@entry=0)
    at ./stdio-common/vfprintf-internal.c:1516
#4  0x0000ffff9ab0ba30 in __vsprintf_internal (string=0xffff98a5ffff "3", 
    maxlen=maxlen@entry=18446744073709551615, format=0xaaaac7fb4458 "%02x ", args=..., 
    mode_flags=mode_flags@entry=0) at ./libio/iovsprintf.c:95
#5  0x0000ffff9aaf0bcc in __sprintf (s=<optimized out>, format=<optimized out>)
    at ./stdio-common/sprintf.c:30
#6  0x0000aaaac7f90084 in processEnipMessage(unsigned char*, int) ()
#7  0x0000aaaac7fa20e4 in processMessage(unsigned char*, int, int, int) ()
#8  0x6265206566206666 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb) 
TIMELINE

2024-06-10 - Initial Vendor Contact
2024-06-10 - Vendor Disclosure
2024-09-17 - Vendor Patch Release
2024-09-18 - Public Release

Credit

Discovered by Jared Rittle of Cisco Talos.