Diving into Zimbra CVE-2022-27925 vulnerability and version detection

Written on 2023/01/29

CVE-2022-27925 vulnerability impacting Zimbra Collaboration Server Network Edition is not new. But there are still quite a lot of impacted assets exposed on the Internet. In this blog post, we will demonstrate how we were able to create an innocuous, non-intrusive, remote check. We will also show how it is possible to identify exact Zimbra version remotely, without any authentication requirements.

This vulnerability is part of ANSSI’s TOP 10 most exploited vulnerabilities. They built this list after numerous forensics analysis. Don’t be the next to help building them. We have added this innocuous test to our vulnscan category, along with Zimbra version detection.

A critical vulnerability with public exploits

This vulnerability allows an unauthenticated attacker to upload a file, remotely. One of the exploit scenario is to create a JSP webshell that will be uploaded on a publicly accessible Web directory. The attacker can then use this webshell to achieve its nefarious goal, like to deploy ransomware. It is game over for the victim, that’s why you should already have patched that issue. If not, see Zimbra links for patches against versions 8.x & 9.x:

https://wiki.zimbra.com/wiki/Zimbra_Releases/8.8.15/P33

https://wiki.zimbra.com/wiki/Zimbra_Releases/9.0.0/P26

We won’t paste links to these exploits here, but they are easy to find anyway.

Two methods to perform an innocuous and reliable check

By trying different exploitation programs in our lab, we noticed that by sending the nefarious payload we received an interesting error message:

Error 401 no authtoken cookie

We modified some exploit code to not send the payload, making it just send a POST request to impacted URL. In the end, we have the following curl request:

curl -s --insecure -XPOST 'https://192.168.1.31/service/extension/backup/mboximport?account-name=admin&account-status=1&ow=cmd'
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 401 no authtoken cookie</title>
</head>
<body><h2>HTTP ERROR 401</h2>
<p>Problem accessing /service/extension/backup/mboximport. Reason:
<pre> no authtoken cookie</pre></p>
</body>
</html>

What about trying this exact same request on a patched server?

curl -s --insecure -XPOST 'https://192.168.1.32/service/extension/backup/mboximport?account-name=admin&account-status=1&ow=cmd'
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 401 Auth failed</title>
</head>
<body><h2>HTTP ERROR 401 Auth failed</h2>
<table>
<tr><th>URI:</th><td>/service/extension/backup/mboximport</td></tr>
<tr><th>STATUS:</th><td>401</td></tr>
<tr><th>MESSAGE:</th><td>Auth failed</td></tr>
<tr><th>SERVLET:</th><td>ExtensionDispatcherServlet</td></tr>
</table>

</body>
</html>

Bingo, the response HTTP body is different. Why not use that behavior to create an innocuous check? The difference is in the title part (and even body part) of the reply:


It works the same way for 8.x & 9.x versions.

That was for a POST request with an empty HTTP body. Let’s try with same kind of request but with a GET method now:

curl -s --insecure -XGET 'https://192.168.1.31/service/extension/backup/mboximport?account-name=admin&account-status=1&ow=cmd'
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 500 Server Error</title>
</head>
<body><h2>HTTP ERROR 500</h2>
<p>Problem accessing /service/extension/backup/mboximport. Reason:
<pre> Server Error</pre></p>
</body>
</html>
curl -s --insecure -XGET 'https://192.168.1.32/service/extension/backup/mboximport?account-name=admin&account-status=1&ow=cmd'
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 500 javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</title>
</head>
<body><h2>HTTP ERROR 500 javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</h2>
<table>
<tr><th>URI:</th><td>/service/extension/backup/mboximport</td></tr>
<tr><th>STATUS:</th><td>500</td></tr>
<tr><th>MESSAGE:</th><td>javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</td></tr>
<tr><th>SERVLET:</th><td>ExtensionDispatcherServlet</td></tr>
<tr><th>CAUSED BY:</th><td>javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</td></tr>
<tr><th>CAUSED BY:</th><td>javax.servlet.ServletException: HTTP GET requests are not supported</td></tr>
</table>

</body>
</html>

This time:


By playing a little bit more, we found out that there is even no need to give HTTP parameters to this GET request. In the end, the following curl command is enough to assert if a target is vulnerable or not:

curl -s --insecure -XGET 'https://192.168.1.31/service/extension/backup/mboximport' | grep -F '<title>Error 500 Server Error</title>'
<title>Error 500 Server Error</title>

You have same output when run on your own assets? They are vulnerable.

Remote version detection

What if there was another way of testing this vulnerability and all the others, past, present and future? Well, we don’t have a crystal ball for future vulnerabilities. But we found a publicly accessible JS file containing version information and release date. Perfect to assert if a vulnerability exists or not in a given software version. To make it short, here is the curl request to borrow version information:

curl -s --insecure -XGET 'https://192.168.1.31/js/zimbraMail/share/model/ZmSettings.js' |grep -E 'CLIENT_RELEASE|CLIENT_VERSION'
this.registerSetting("CLIENT_RELEASE", {type:ZmSetting.T_CONFIG, defaultValue:"20190819064612"});
this.registerSetting("CLIENT_VERSION", {type:ZmSetting.T_CONFIG, defaultValue:"8.8.12_GA_3844"});

Easy as knowing the correct URL.

Binding it all together

We decided to publish our checker, a program used to help cyber defense in detecting their vulnerable assets. This checker fetches exact Zimbra version, tries to detect the vulnerability with both GET and POST requests. This checker is available on our GitHub here:

https://github.com/onyphe/material/blob/master/vulnscan-checker/check-zimbra-cve-2022-27925.pl

Some example runs against our lab assets, first towards an unpatched target:

perl bin/check-zimbra-cve-2022-27925.pl https://192.168.1.31
Using URL: [https://192.168.1.31]
Using account: [admin@192.168.1.31]

Sending version request: [GET https://192.168.1.31/js/zimbraMail/share/model/ZmSettings.js]
Version[8.8.12_GA_3844] - Release[20190819064612]
Sending check request: [GET https://192.168.1.31/service/extension/backup/mboximport?account-name=admin@192.168.1.31&account-status=1&ow=cmd]

*** Target IS vulnerable to CVE-2022-27925

Received content:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 500 Server Error</title>
</head>
<body><h2>HTTP ERROR 500</h2>
<p>Problem accessing /service/extension/backup/mboximport. Reason:
<pre> Server Error</pre></p>
</body>
</html>

Sending check request: [POST https://192.168.1.31/service/extension/backup/mboximport?account-name=admin@192.168.1.31&account-status=1&ow=cmd]

*** Target IS vulnerable to CVE-2022-27925

Received content:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 401 no authtoken cookie</title>
</head>
<body><h2>HTTP ERROR 401</h2>
<p>Problem accessing /service/extension/backup/mboximport. Reason:
<pre> no authtoken cookie</pre></p>
</body>
</html>

Then towards a patched target:

perl bin/check-zimbra-cve-2022-27925.pl https://192.168.1.32
Using URL: [https://192.168.1.32]
Using account: [admin@192.168.1.32]

Sending version request: [GET https://192.168.1.32/js/zimbraMail/share/model/ZmSettings.js]
Version[8.8.15_GA_4481] - Release[20221116081024]
Sending check request: [GET https://192.168.1.32/service/extension/backup/mboximport?account-name=admin@192.168.1.32&account-status=1&ow=cmd]

Target is not vulnerable to CVE-2022-27925

Received content:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 500 javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</title>
</head>
<body><h2>HTTP ERROR 500 javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</h2>
<table>
<tr><th>URI:</th><td>/service/extension/backup/mboximport</td></tr>
<tr><th>STATUS:</th><td>500</td></tr>
<tr><th>MESSAGE:</th><td>javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</td></tr>
<tr><th>SERVLET:</th><td>ExtensionDispatcherServlet</td></tr>
<tr><th>CAUSED BY:</th><td>javax.servlet.ServletException: javax.servlet.ServletException: HTTP GET requests are not supported</td></tr>
<tr><th>CAUSED BY:</th><td>javax.servlet.ServletException: HTTP GET requests are not supported</td></tr>
</table>

</body>
</html>

Sending check request: [POST https://192.168.1.32/service/extension/backup/mboximport?account-name=admin@192.168.1.32&account-status=1&ow=cmd]

Target is not vulnerable to CVE-2022-27925

Received content:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/>
<title>Error 401 Auth failed</title>
</head>
<body><h2>HTTP ERROR 401 Auth failed</h2>
<table>
<tr><th>URI:</th><td>/service/extension/backup/mboximport</td></tr>
<tr><th>STATUS:</th><td>401</td></tr>
<tr><th>MESSAGE:</th><td>Auth failed</td></tr>
<tr><th>SERVLET:</th><td>ExtensionDispatcherServlet</td></tr>
</table>

</body>
</html>

Exposed Zimbra assets vs exposed & vulnerable assets

First, let’s fetch some exposed Zimbra servers with help from our Export API. We use a triple as a de-duplication key: IP, port & forward fields. The forward field is basically the HTTP Host header containing a full-qualified domain name (FQDN). Thus, multiple FQDNs may be hosted on the same IP address and all may be vulnerable.

onyphe -export 'category:datascan app.http.component.productvendor:Zimbra app.http.component.product:"Collaboration Server" | fields ip,port,forward default=""'

Exposed unique count: 69,007

Now, do the same for vulnerable assets:

onyphe -export 'category:vulnscan app.http.component.productvendor:Zimbra app.http.component.product:"Collaboration Server" cve:CVE-2022-27925 | fields ip,port,forward default=""'

Vulnerable unique count: 1,217

Less than 2% remain vulnerable or applied a workaround. Good job sysadmins 🙂

Conclusion

We showcased two new additions to ONYPHE: a check for CVE-2022-27925 readily available in vulnscan category so you can detect your own vulnerable assets:

category:vulnscan cve:CVE-2022-27925 domain:example.com

And a new version detection technique also available in vulnscan category:

category:vulnscan app.http.component.productvendor:“Zimbra” app.http.component.productversion:“8.8.15_GA_3895” app.http.component.productversionpatch:“20200102004124”

For a complete list of scanned versions:

category:vulnscan source:version::zimbracs

With these two additions, we hope to make the Internet a more secure place by letting our customers hunt and patch their exposed devices.