At the tail end of last year (November 2024), we found ourselves in possession of an EV Charge Controller (CHARX SEC-3150). With some spare cycles on the calendar and a keen interest in learning more about how electric vehicle charging equipment works, we set out to hack the device. The results - we found that it was possible for an attacker with physical access to the CHARX SEC-3150, 12 hours to kill, and some hardware, to gain code execution on the CHARX SEC-3150 with root permissions.

Once we found this we worked with Phoenix Contact to disclose the issues. This resulted in the allocation of 5 CVEs - 3 of which were rated with a score of 7.8 or higher (HIGH impact). In this blog, we’ll tell the story of how we came to find these issues.

Not interested in the blog? Just want exploit details? Get those details here:

Project Goal

At ivision we help companies secure their products (software and hardware), but for this project we made it our goal to obtain unauthenticated code execution as root on the device.

We had only one requirement: It had to be in a feature, configuration, or setting likely to be used in a production environment. Root via a default password didn’t count. It had to be something more exciting.

Getting Started

With any kind of security research project, the first task is to read the manual and get acquainted with the target. Let’s talk about the CHARX SEC-3150. This device is a part of Phoenix Contact’s Electric Vehicle Modular Charging Lineup. It enables enterprise grade Electric Vehicle to be designed and created in a modular line up. In this modular environment the CHARX SEC-3150 was the brains.

Example Deployment of a CHARX

You’ll notice that there’s a lot of different pieces of equipment. For our purposes we’re really only interested in the CHARX SEC-3510 and the accompanying display (more about this later).

In terms of exposed ports and available physical attack interfaces, the CHARX SEC-3510 contains:

  1. A single USB-C (or USB-micro) port which is used to attach peripherals.
  2. Two Ethernet ports one for a WAN connection and another for a LAN connection. The LAN port is intended to allow multiple CHARX SEC-3150’s to be chained together for larger deployments.

On the software side, the CHARX SEC-3150 was running an embedded Linux distribution on an ARM32 CPU. During our testing period, we focused on version 1.6.4 of the CHARX-SEC 3150, which by default exposed:

  1. The main web interface which used a default password
  2. MQTT Server on the LAN interface

First Root Shell

Developing exploits for a piece of equipment is always significantly easier when you have a working development environment. The nice thing about embedded systems and product security is that the equipment is its own development environment. You just have to build it. So the first step, was figuring a way to get a root shell. How we did it did not matter, just that we did.

This turned out to be trivial. SSH could be enabled in the settings. Once authenticated and logged in we were given access to system as a low privileged user (user-app). Our first goal - escalate. We used CHARX-FINDING-005 and CHARX-FINDING-004 (CVE-2025-24006, CVE-2025-24005) to do this. By reviewing the sudoers file, we found the following:

user-app ALL=(ALL) NOPASSWD: /sbin/ip

This was a lucky moment, the ip command has a neat feature which allows users create, and delete network namespaces, and then execute commands within them. So by calling the following sequences of commands, we were able to enter a namespace with root permissions:

ssh user-app@192.168.1.61
Last login: Thu Jul 18 09:28:59 2024 from 192.168.1.1
ev3000:~$ id
uid=2005(user-app) gid=2000(user-app) groups=2000(user-app)
ev3000:~$ sudo ip netns add ivision
ev3000:~$ sudo ip netns exec ivision /bin/sh
ev3000:/home/user-app# id
uid=0(root) gid=0(root) groups=0(root)

Once we had our initial root privileges, it was trivial to create our own development environment. We changed the root password and enabled root logins via SSH. Then we copied over various debug binaries and setup GDB for remote access.

Focusing on unauthenticated exploits

With our initial root access obtained, it was time to start focusing on our goal: Unauthenticated code execution as root. With our goal in mind, we started by ruling out obviously authenticated paths such as the web interface. SSH was also out of the question. However, the MQTT interface piqued our interest.

The MQTT interface lacked authentication allowing all LAN based users to interact with it. MQTT as a protocol is an interesting attack path. MQTT operates on “topics” with a publish and subscribe model, so if an attacker is able to publish content, anyone subscribed to the topic would receive and take action on the received message.

With MQTT selected as our entry point, we just need to figure out which services subscribe to which MQTT topics. We can figure this out by leveraging our makeshift development environment and enabling verbose MQTT logs.

To save everyone some time, we ended up finding that the CharxEichrechtAgent service subscribed to various MQTT topics and in certain conditions was enabled by default. Specifically, CharxEichrechtAgent was enabled by default in German deployments. This topic provides support for the German Calibration Law:

The German calibration law, commonly known as “Eichrecht”, applies to all measuring devices, including electricity meters on EV charging stations. 1

Exploring the CharxEichrechtAgent

When exploring the CharxEichrechtAgent, we noticed that it subscribed to a significant number of MQTT topics and setup a callback function for each subscribed topic. Now we needed to figure out the MQTT topic message protocol being used. Again our makeshift development environment let us setup logging and we could see that JSON messages were being used. From here we setup a some fuzz tests and begin reversing each of the callbacks. Before we knew it we had another CVE. The CharxEichrechtAgent failed to properly handle the following JSON message:

{"data":{}}

The result? The entire process crashed. It would render devices inoperable for a few second before the system watchdog rebooted the process. As we’ll see later on, this ended up being a very useful thing. The crashing behavior could probably also be weaponized into a denial of service, rendering the Electric Vehicle Chargers in a location inoperable, but we didn’t test that (CHARX-FINDING-001, CVE-2025-24002).

Our manual reverse engineering of each topic’s callbacks resulted in us finding an out-of-bounds write (CHARX-FINDING-002, CVE-2025-24003). Specifically, the eichrecht_status topic failed to limit the size of the status field when reading it from a JSON message. This status field was then copied to static global variable as shown in:

void eichrecht_status_callback(void* topic_json_data, int controller_index){

...

dest = (char *)(&charge_controller_information + controller_index * 0x4e5); 
...
status_json_field = (char *)extract_json_field(&topic_json_data,"status");

status_length = strlen(status_json_field);
memcpy(dest,status_field,status_length);

Due to the lack of bounds checking, we could overwrite past the static variable’s bounds. Through testing we found that we could write up to 0x78C bytes into memory. This was a significant step towards our goal of unauthenticated code execution.

From write to execute

Ghidra is great! Through cross referencing the memory address we could now overwrite, we were able to find various functions making use of the our controllable data. We eventually found one interesting function which used a format string to fill a stack buffer with data we controlled (CHARX-FINDING-003,CVE-2025-24004):

memset(buffer,0x0,0x800);
if (charge_controller_count < 0xd) {
switch((int)charge_controller_count) {
case 0x2:
  <REMOVED FOR BREVITY>
  sprintf(buffer,"{\"TT\":\"StatusCS\",\"Sx\":[\"%s\",\"%s\"],\"Ax\":[\"%s\",\"%s\"]}",puVar3,
          puVar2,&DAT_004e706e,&overflowed_bss);

The function filled buffer of size 0x800 with our string, which contained 0x78C bytes. Fortunately for us, multiple instances of controlled string were read, resulting in well over 0x800 bytes being copied into the buffer. Leading to a buffer overflow!

How to call the function

So we’ve smashed the stack! That’s a win right? Well… no. We’ve found a vulnerable function, but actually calling the function is tricky. It’s not callable over MQTT. It’s only called by the USB-C connected display. This meant we needed to figure out how to impersonate the display. A review of the system’s udev rules told us that:

  1. The system was waiting for a USB device with the idVendor 0x0483
  2. Once detected - an ACM (SERIAL) connection was created for the CharxEichrechtAgent to use to communicate with the display.

So if we wanted to trigger our vulnerable function, we would need to create custom USB device with the idVendor set to 0x0483 and support for serial. We’ve created similar custom USB devices in the past using a rooted android device (checkout out Command Injection With USB Peripherals), but for this project we used gadget mode on a RPI. It’s fairly easy on a RPI, we just:

  1. Loaded the g_serial kernel module, which created a new serial interface
  2. Created a python service to read and write from the serial interface

When connected to the CHARX SEC-3150, the CHARX SEC-3150 would assume our RPI was a display and start sending data to it over serial. By responding correctly to the CHARX SEC-3150’s serial messages we were able to put the device into a state that triggered the vulnerable function.

Time for an exploit

We could smash the stack, we could trigger the smash, but we weren’t ready for an exploit. The problem - ASLR and PIE. The binary was hardened and we didn’t have a memory leak that we could use. This would typically kill our dreams of exploitation. However, we had a bit of luck on our side. ASLR is meant to make it impossible to guess the memory offset of usable functions in a binary. However, we were trying to exploit an ARM32 device. ARM32 only has 8 bits of entropy. Meaning there’s only 256 different possible memory offsets.

256 is rather small and can be searched in a realistic amount of time, especially if we if we can control the process’ life time – remember we found a way to cause a crash? We had all that was needed to brute force the ASLR memory offset and make our dreams a reality.

With this we had everything we needed.

  1. We wrote our MQTT exploitation code and loaded it up on an RPI 4.
  2. We also wrote our spoofed display code and load it up on an RPI 2 W.
  3. Setup the RPI4 and RPI 2 W appropriately and waited.

~12 hours later we had a root shell!

Our Setup

Want to try it for yourself - see exploit code and the report we sent to Phoenix Contact here:

Discourse Timeline

  • 2025-01-16 - Disclosed to Phoenix Contact
  • 2025-01-16 - Phoenix Contact Confirmed Receipt
  • 2025-01-16 - Phoenix Contact Notifies CERT@VDE
  • 2025-07-08 - Phoenix Contact Publishes Advisory
  • 2025-09-23 - ivision publishes blog

CVE assignments:

  1. https://driivz.com/glossary/eichrecht/