CVE-2024-34026
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.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
OpenPLC _v3 b4702061dc14d1024856f71b4543298d77007b88
OpenPLC_v3 - https://github.com/thiagoralves/OpenPLC_v3
9.0 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-121 - Stack-based Buffer Overflow
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.
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)
2024-06-10 - Initial Vendor Contact
2024-06-10 - Vendor Disclosure
2024-09-17 - Vendor Patch Release
2024-09-18 - Public Release
Discovered by Jared Rittle of Cisco Talos.