CVE-2024-23315
A read-what-where vulnerability exists in the Programming Software Connection IMM 01A1 Memory Read functionality of AutomationDirect P3-550E 1.2.10.9. A specially crafted network packet can lead to a disclosure of sensitive information. An attacker can send an unauthenticated packet 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.
AutomationDirect P3-550E 1.2.10.9
7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CWE-284 - Improper Access Control
The P3-550E is the most recent CPU module released in the Productivity3000 line of Programmable Automation Controllers from AutomationDirect. It is an affordable control CPU which communicates remotely via ethernet, serial, and USB and exposes a variety of control services, including MQTT, Modbus, ENIP and the engineering workstation protocol DirectNET.
The P3-550E exposes a “Programming Software Connection” service over UDP port 9999 that is used by the engineering workstation software to program and otherwise configure the device. The protocol is not well documented and what information we do have is pieced together from reverse engineering efforts.
Each message of the protocol is prefixed with a 12-byte header containing the requesting client’s IP address and originating port, two 16-bit fields only referred to as ‘GBS’ and ‘IMM’, and is followed by a payload which varies by type of request.
0 1
0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| GBS | Client Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Client IP |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| IMM | Undetermined |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The first byte of the IMM field acts as a function code, dictating which high-level feature set is being requested, and the second byte dictates the exact functionality expected. The protocol format for the remainder of the message is dependent on the IMM
value.
This vulnerability arises when the first byte of IMM
is 0x01
, which appears to host a subset of diagnostic features. The function responsible for implementing this feature set is located at offset 0x759d0
and we refer to it as _DISCOVERY_CALLBACK_1
. Within this function, the second byte of IMM
is used in a switch-case to identify the exact feature being requested. For this report, we are most interested in the implementation of the handler associated with IMM[1] = 0xA1
. This function appears to enable arbitrary remote memory access, but we did not find any features within the engineering workstation software that interacted with it. From reverse engineering, we can identify that this handler expects the following payload format.
0 1
0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Base Addr |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chunk Size | Chunk Count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The handler for IMM = 0x01A1
, located at offset 0x7b010
, implements a remote memory read, where chunk_count
s of chunk_size
(limited to 1, 2, and 4) are read from base_address
. A partial decompilation of this function is included below for reference.
int32_t sub_7b010(struct packet* pkt)
{
int result;
if (sub_7afa4(0xA1, pkt->data_destination) != 0) {
result = 0;
}
else {
uint16_t chunk_count = pkt->chunk_count;
uint16_t chunk_size = pkt->chunk_size;
if (chunk_count == 0) {
chunk_count = 1;
pkt->chunk_size = 1; // Relevant to note that the response packet appears to be unioned to the same variable
// so the field here is a response field, probably unrelated to `chunk_size`
}
if (((chunk_size != 1 && chunk_size != 2) && chunk_size != 4))) {
result = 0;
}
if (chunk_size == 4) {
void* address = pkt->base_addr;
void* dest = &pkt->data_destination; // A field that will exist in the response but not the original request
// Maximum bytes to be read are 0x100 * 4 = 0x400
if (chunk_count > 0x100) {
chunk_count = 0x100;
}
uint32_t bytes_txd = NULL;
uint32_t* dest_ptr = &pkt->data_dest;
if (chunk_count != 0) {
if (chunk_count > 8) {
uint32_t i = ((chunk_count - 1) / 8;
if ((chunk_count - 8) > 0) {
do {
bytes_txd += 8;
dest_ptr[0] = address[0];
dest_ptr[1] = address[1];
dest_ptr[2] = address[2];
dest_ptr[3] = address[3];
dest_ptr[4] = address[4];
dest_ptr[5] = address[5];
dest_ptr[6] = address[6];
dest_ptr[7] = address[7];
address = &address[8];
dest_ptr = &dest_ptr[8];
i--;
} while (i != 0);
}
}
// The remainder of this function is effectively just the various unrolled loops associated
// with the varying selections of `chunk_size` and `chunk_count`, then responds with the requested data
...
In the above decompilation, we confirm that this endpoint allows an unauthenticated remote endpoint to read arbitrary memory addresses from the device. An attacker who submits a series of properly formatted requests can use this feature to remotely read arbitrary memory regions on the device resulting in a complete lack of confidentiality.
A CISA advisory can be found here: https://www.cisa.gov/news-events/ics-advisories/icsa-24-144-01
2024-02-14 - Initial Vendor Contact
2024-02-15 - Vendor Disclosure
2024-05-23 - Vendor Patch Release
2024-05-28 - Public Release
Discovered by Matt Wiseman of Cisco Talos.