CVE-2024-21785
A leftover debug code vulnerability exists in the Telnet Diagnostic Interface functionality of AutomationDirect P3-550E 1.2.10.9. A specially crafted series of network requests can lead to unauthorized access. An attacker can send a sequence of 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.
AutomationDirect P3-550E 1.2.10.9
9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-489 - Leftover Debug Code
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 device exposes a telnet interface which, when “unlocked”, exposes a powerful diagnostic interface. While telnet is enabled by default, this interface is not, and we identified no mechanism within the engineering workstation software that would allow a standard user to enable it. There are, however, three undocumented methods confirmed to enable this interface. The interface is titled ‘PCMON’ and appears as a command console exposing a variety of low-level features collected into groups. Some notable features include the ability to read and modify arbitrary system memory and files, change the run status of the PLC, interact with other devices on the backplane, enable modbus sniffing, etc.
telnet 172.172.172.172
Trying 172.172.172.172...
Connected to 172.172.172.172.
Escape character is '^]'.
MACBORG
60:52:D0:00:00:00 01FEB24
*******
CLI over Telnet Active...
P3-550E PMON>?
P3-550E PAC Monitor(01020A09:20220622) Command List
AS CDM EM FI IMP MB NET PL SC SH SY TST USB // ?
P3-550E PMON>PL ?
Command Description Syntax
------- ----------- ------
BUF1 Get/Set 1st Project Memory Table<pmem> [index] <pmem> [index]
BUF2 Get/Set 2nd Project Memory Table<pmem> [index] <pmem> [index]
DBG Debug level 0=off 1=crit 2=info dbg <level>
LOAD Dispatch uploaded project files <load>
MAIL Srvr address 192.168.20.226:110 mail <srvr ip>
PMEM Get/Set Project Memory Table <pmem> [index]
R2S Simulate run to stop r2s
RTE Sim run-time edit startup <rte>
S2R Simulate stop to run s2r
SIT Dump the SIT (table)
START Sim startup of project loader <start>
? Help PrjMenu <subcommand list>
P3-550E PMON>SY ?
Command Description Syntax
------- ----------- ------
BINF Sys build info <view only>
DATE Set/Display Date&Time mm/dd/yyyy hh:mm[:ss]
RESET Reset system/application 0 0:HW, 2 1/3:Boot/Serv
DEVS Find System Devices <none>
FACTORY INTERNAL USE ONLY DO NOT USE
BATT Check Battery Voltage <view only>
RUN Display Run Sw State <none>
SRAM Test System SRAM <none>
ISVER Test if we are running <ver> <ver string>
UNLOCK Unlock serial Port <none>
I2C Test I2C <none>
DBUG Set Debug vars <op> [index]
DM Memory Display <addr><length><boundary>
I2C Reset I2C Bus <none>
MCCR Show MCCR registers
PRTST Printf status
PCI Dump PCI Config <op>
MPOOL Memory Pool status <none>
SM Set value to Memory <addr><content-4 bytes>
TASK Task/HISR information
TICK System ticks <new tick value>
UIM UIM Commands <cmd#> [opt] [string]
PCSW PACSW status <view only>
RESO Sys resource <view only>
SRM SRAM table display/setup <none> or <op>
RD Read Test I/O <none>
FIND Find a byte pattern
LOCK Lock CLI <argument list>
MTEST Test P2 memory
CACHE enable/disable cache 0=disable, 1=enable
PLOG show printf log
? Help SysMenu <subcommand list>
P3-550E PMON>MB ?
Command Description Syntax
------- ----------- ------
DMOD Display the map of MODBUS <mb dmod>
STS Disp MODBUS STATUS All <mb sts>
TBL Disp MODBUS/RTU Table <mb tbl <Tcp,Slave,Gs,Im,Verbose>>
DBG MODBUS debug flag <mb dbg (type) (code)>
SOC Dsiplay socket discriptor <mb socket>
ETBL Display EnetCtrlTbl <mb etbl>
EI Enable Serial port <mb ei (port)>
Q Q quantities mb q
QST Q use report mb qst
DTBL Device table mb dtbl
DEV Device entry mb dev
REC Reconcile tables mb rec
TEST Regression test mb test
EDRV Test for eDrive MODBUS/TCP <mb edrv (The last IP #1-63>
GS Show GS Drive table status <mb gs>
SNIF Toggle sniffer enable <on/off>
DBGC Toggle connectin debug <on/off>
EXT External port activity mb ext <?>
? Help SysMenu <subcommand list>
Notably, the SH
command indicates that it should spawn a NucleOS shell which would provide access to RTOS primitives, but we believe this particular function has been disabled over telnet.
This interface is held in a locked state by a function we refer to as lock_telnet
which is located at offset 0xd3334
. This function is an infinite loop which, normally, would receive and discard all incoming data silently. There is one command that the telnet service will respond to prior to being unlocked, MACBORG
, which will return the current date and MAC address of the device, and this is what enables the first bypass. The interface is guarded by a per-device unique daily key created by a weak key generation algorithm based on the current date and device MAC address. The generated key is always seven characters long. If this password is submitted to the device via telnet then the diagnostic interface unlocks annd places the remote user inside of the above CLI. While we did not identify the key generation algorithm in use, we were able to extract the generation functionality and emulate it to the point that it will generate keys for any combination of MAC address and date.
// [1] If MACBORG is seen...
0000d3468 if (strcmp(&client_msg, "MACBORG") == 0)
0000d3464 {
0000d3470 int32_t FLAG_LOCK_TELNET_PREV;
0000d3470 if (g_TELNET_CONNECTION_EXISTS == 0)
0000d346c {
0000d3474 FLAG_LOCK_TELNET_PREV = FLAG_LOCK_TELNET;
0000d3478 FLAG_LOCK_TELNET = 0;
0000d3478 }
// [2] Collect the device's MAC address and current time
0000d3484 char macaddr[0x6];
0000d3484 get_device_macaddr("Enet0_DP83815", &macaddr);
0000d34a8 printf("%02X:%02X:%02X:%02X:%02X:%02X ", macaddr[0], macaddr[1], macaddr[2], macaddr[3], macaddr[4], macaddr[5]);
0000d34b0 get_as_datetime(&telnet_epoch_time);
0000d34b4 int32_t year = g_CURRENT_DATETIME.year;
0000d34bc if (year >= 0x64)
0000d34b8 {
0000d34dc g_CURRENT_DATETIME.year = (year % 0x64);
0000d34d4 }
0000d34e0 int32_t month = g_CURRENT_DATETIME.month;
// [3] Select the most readable form of output, and then supply the information to the remote user
0000d34e8 if (month > 12)
0000d34e4 {
0000d3534 printf("m%d d%d y%02d\n\r", (month + 1), g_CURRENT_DATETIME.day, g_CURRENT_DATETIME.year);
0000d351c }
0000d350c else
0000d350c {
0000d350c printf("%02d%s%02d\n\r", g_CURRENT_DATETIME.day, &MONTHS_MAP[month], g_CURRENT_DATETIME.year);
0000d34f4 }
0000d353c if (g_TELNET_CONNECTION_EXISTS == 0)
0000d3538 {
0000d3544 NU_Task_Sleep(20);
0000d3548 FLAG_LOCK_TELNET = FLAG_LOCK_TELNET_PREV;
0000d3548 }
0000d3538 }
// [4] If we have received at least 7 bytes, try treating it as a password
0000d3550 if (bytes_rxd == 7)
0000d354c {
// [5] Allocate and generate the daily password
0000d3558 char diagnostic_key[0x8];
0000d3558 telnet_unlock_keygen(&diagnostic_key);
// [6] Compare the provided password with the generated one
0000d356c if (strcmp(&client_msg, &diagnostic_key) == 0)
0000d3568 {
0000d3574 if (g_TELNET_FLAG == 0)
0000d3570 {
// [7] Unlock the interface
0000d357c FLAG_LOCK_TELNET = 0;
0000d357c }
0000d3580 lock_status = 1;
0000d3584 break;
0000d3584 }
0000d3574 }
As noted above, an attacker can first submit MACBORG
(which will be handled by [1]
, [2]
, and [3]
) to retrieve the device’s MAC address and date. Then, using the key generation algorithm they can derive the password and submit it, causing the device to generate a matching key (at [5]
) which will result in the interface unlocking at [7]
.
The second method occurs over either RS232 or RS485 and requires that the device have enabled one of these interfaces, and that it is configured for Modbus RTU traffic. The PLC implements a vendor-specific modbus function, 0x6b
, which takes 8-bytes worth of a hard-coded 16-byte key. Sending two of these modbus messages in sequence with each half of the key will cause the device to unlock the diagnostic CLI for any extant telnet connections. This is done by using a global boolean to indicate to the lock_telnet
function whether the interface should be enabled, regardless of password or any other mechanism. The handling of modbus function 0x6b
is done inside a function we refer to as mb_handle_0x6b
and is found at offset 0xd3654
.
0000d3654 void mb_handle_msg_0x6b(struct mb_rtu_t* arg1, int32_t len)
// Several variables in this function are defined globally
0000d3654 {
0000d3670 if (len >= 0xa)
0000d365c {
0000d367c uint32_t timedout;
0000d367c if (MB_RTU_SECRET_IDX <= 1)
0000d3678 {
0000d3688 timedout = check_time_elapsed(g_COMM_INITIALIZATION_TIME, 1000);
0000d3684 }
0000d3690 if ((MB_RTU_SECRET_IDX > 1 || (MB_RTU_SECRET_IDX <= 1 && timedout != 0)))
0000d368c {
0000d3698 MB_RTU_SECRET_IDX = 0;
0000d3698 }
// [1] Verify the content of the modbus message matches with the current window of key being compared against
0000d36c0 if (memcmp(&arg1->content, &MB_RTU_SECRETS[MB_RTU_SECRET_IDX], 8) != 0)
0000d36bc {
// [2] If they don't match, disable the diagnostic interface and reset the counter
0000d377c DIAGNOSTIC_INTERFACE_ENABLE = 0;
0000d3780 MB_RTU_SECRET_IDX = 0;
0000d3780 }
0000d36c4 else
0000d36c4 {
// [3] If we matched and we were not the final comparison, just continue on ensuring the interface is still locked
0000d36cc if (MB_RTU_SECRET_IDX != 1)
0000d36c8 {
0000d3760 MB_RTU_SECRET_IDX++;
0000d3768 DIAGNOSTIC_INTERFACE_ENABLE = 0;
0000d3768 }
0000d36d8 else
0000d36d8 {
// [4] Otherwise, the key matches completely so...
// [5] send a response to the message
0000d36d8 response.unit_id = arg1->unit_id;
0000d36ec response.func_code = (arg1->func_code | 0x80);
0000d36f0 response.field_2 = 3;
0000d36f4 response.field_3 = 4;
0000d36f8 sub_4fcd4();
0000d3708 sub_4ee94(1, &response, 4);
0000d3710 NU_Task_Sleep(0xa);
// [6] Configure a new comm service
0000d3728 struct comm_descriptor descriptor;
0000d3728 descriptor.baudrate = 115200;
0000d373c descriptor.parity = 0;
0000d3740 descriptor.stop_bits = 1;
0000d3744 descriptor.bits = 8;
0000d3748 initialize_comm(0, TELNET, &descriptor);
0000d374c data_14cbb0 = 0;
// [7] Enable the diagnostic interface flag
0000d3750 DIAGNOSTIC_INTERFACE_ENABLE = 1;
0000d3754 MB_RTU_SECRET_IDX = 0;
0000d3754 }
0000d3770 g_COMM_INITIALIZATION_TIME = NU_Plus_Get_Time();
0000d376c }
0000d376c }
0000d365c }
We note that in the above function, at [1]
, we compare the current modbus payload against the current segment of the key and if, after two messages, both keys have matched, a successful response is sent (at [5]
), a new comm service is established (at [6]
) and finally the global flag DIAGNOSTIC_INTERFACE_ENABLE
is set to 1. Within the lock_telnet
function, at offset 0xd341c
we find the conditional responsible for monitoring this flag.
0000d341c if (DIAGNOSTIC_INTERFACE_ENABLE != 0)
0000d3418 {
0000d3424 if (g_TELNET_FLAG == 0)
0000d3420 {
0000d342c FLAG_LOCK_DIAGNOSTIC_INTERFACE = 0;
0000d342c }
0000d3430 lock_status = 1;
0000d3434 break;
0000d3434 }
This feature would allow an attacker who can communicate to the device over ModbusRTU to enable the diagnostic interface without any other knowledge of the target device, as the key is static across all devices.
The third method relies on the automatic execution of commands inside a file named STARTUP.CLI
that can be placed onto a USB drive and inserted into the PLC. Each line of the file is executed within the diagnostic shell, so an entire script can be placed into this file for headless running, or one can simply include the command SY SM 14CD04 1
which uses the “SY(stem) S(et)M(emory)” command to turn the DIAGNOSTIC_INTERFACE_ENABLE
variable (located at 0x14CD04
) on.
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.