Talos Vulnerability Report

TALOS-2024-1942

AutomationDirect P3-550E Telnet Diagnostic Interface leftover debug code vulnerability

May 28, 2024
CVE Number

CVE-2024-21785

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

P3-550E - https://www.automationdirect.com/adc/shopping/catalog/programmable_controllers/productivity3000plcs(modular)/cpus/p3-550e

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-489 - Leftover Debug Code

DETAILS

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.

VENDOR RESPONSE

A CISA advisory can be found here: https://www.cisa.gov/news-events/ics-advisories/icsa-24-144-01

TIMELINE

2024-02-14 - Initial Vendor Contact
2024-02-15 - Vendor Disclosure
2024-05-23 - Vendor Patch Release
2024-05-28 - Public Release

Credit

Discovered by Matt Wiseman of Cisco Talos.