Monday, March 27, 2017

Hacking Xfinity's Home Security Cameras, part 3

So we discontinued our Xfinity Home subscription (which isn't a statement against the service; we moved out of the Xfinity service area and into a Cox service area and opted not to install their home security package) several months ago.  Of course I took down the Xfinity cameras before we moved, and they've been in storage since.  Last night, I finally dug them out and set about getting them working -- without the touchscreen and security router that Xfinity had them set up for.  This blog post is mostly so I will remember how I did it, because all the information is out there, but it's not all correct (for my cameras, at least).

The problem, of course, is that when the cameras were paired to the Xfinity touchscreen/router/system, passwords were set to view the feeds, and to administer the cameras.  It's possible to get the cameras onto another Wifi by pressing the reset button for a few seconds (putting it into a WPS pairing mode) and then pressing the WPS button on your router.  But this is of no use if you don't know the passwords to get into the camera.

(First thing's first: use the power+ethernet dongle that you cleverly had the Xfinity tech leave you when he installed the cameras so that you can put them on your hardwired LAN.  Do that now.)

So, for the iCamera2 cameras, the information I found online says that the default username is "administrator" and the default password is a blank password.  This, obviously, didn't work with my camera because it was paired with the Xfinity system.  Additionally, the firmware that was loaded onto the camera doesn't have a web UI by default.  (But, if you have the password, you can enable it with a request to /adm/enable_ui.cgi.)

As an aside, I did try updating the firmware, thinking that might reset the password. I found a list of available firmware images in a comment on this blog post, which was a welcome find. I'll repeat his list here for convenience sake:

iCamera1000 Firmware Files

http://edge.xfinity-home.top.comcast.net/firmware/DYW9HZ-105-1002R08.bin
http://edge.xfinity-home.top.comcast.net/firmware/DYW9HZ-308-1002R17.bin
http://edge.xfinity-home.top.comcast.net/firmware/DYW9HZ-308-1002R19.bin
http://edge.xfinity-home.top.comcast.net/firmware/DYW9HZ-308-1002R22.bin

iCamera2 Firmware Files

http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-104-300114.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-105-300124.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-105-300126.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-106-300128.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-106-300129.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-108-300132.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-110-300229.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-110-300230.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-111-300235.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-111-300238.bin
http://edge.xfinity-home.top.comcast.net/firmware/DAXNHZ-111-300239.bin

The online information also suggested that one should hold the reset button while applying power to the camera.  This appears to put the camera into some kind of recovery mode where the power LED flashes rapidly.  I was not able to see the camera perform any sort of initialization or DHCP request or anything, but eventually figured out that it had just assumed a 192.168.0.99 IP address.  I was able to communicate with it on that address, but the only thing available seemed to be a firmware update page, from which I could upload new firmware images (and have them installed) but that was it.

What I eventually worked out as the solution was to let the camera power on normally, then press and hold the reset button for some 30 seconds.  I did not see any indications that this did anything, but it looks like this resets the camera into a setup mode where the touchscreen can pair it.  After performing this step, I was able to access the camera using curl from my linux box:

# curl --basic --user administrator: 'http://10.0.1.133/adm/set_group.cgi?group=NETWORK&dns_server=10.0.0.1&dns_type=1'
OK
# curl --basic --user administrator: 'http://10.0.1.133/adm/set_group.cgi?group=HTTP&https_mode=1&http_mode=1'
OK
# curl --basic --user administrator: 'http://10.0.1.133/adm/set_group.cgi?group=USER&admin_name=root&admin_password=&login_check=1&user1=%2C'
OK

Of course, at this point I went to the web UI to verify that the password I'd just set was working. And it was. Thinking about it now, I'm wondering if once reset this way, the web UI is not disabled anymore. If not (that is, if the web UI is disabled and browsing to the camera yields a 404 error), it can be re-enabled with:

# curl --basic --user : 'http:///adm/enable_ui.cgi'


This was enough to get me into the camera's configuration UI and able to set up everything there. I will be repeating this with additional cameras later on, so I'll update this further if anything else becomes apparent. But, in case it helps anyone else, these were the steps I took and what I ended up with.

Tuesday, October 27, 2015

Hacking Xfinity's Home Security Cameras, part 2

(Note to the evil among us: I have, of course, changed the authorization and authentication details below. So don't bother. Or do, waste your time, see if I care. :) )

We recently moved about a mile down the road, which meant we needed a new Xfinity setup, and of course they've revamped their hardware. This time, we got a new Xfinity Gateway with 802.11ac support.  In fact, the latest gateway we just got apparently has something called beamforming, which is totally worth reading about because it's a really awesome hack.

Unfortunately, we discovered that Xfinity Home Security no longer uses a discrete "security router" for the wireless touchscreen and cameras. They appear to have integrated this functionality directly into the Xfinity internet gateway, so my camera recording solution couldn't be set up with the new installation.  While this is no doubt cheaper and better for them, I wasn't happy losing access to the cameras for no good reason.

Thanks to Chad Sturgill, however, I'd been thinking about sniffing the packets of a camera provisioning to determine the hidden wifi network and other details.  Now I had a reason.  So after the Xfinity tech left, I went to work.  Chad was right, and this is probably not for the faint of heart, and it took me a few tries to get something useful.  I set up my Linux firewall with 3 NICs to treat two of them in a bridged configuration, connected one to the camera, one to port 4 of the Xfinity gateway, and then ran tshark to capture the traffic.  I did have to run dhclient on the bridged interface to get it online, and then ran tshark thusly:

# ifdown eth0
# brctl addbr br0
# brctl addif br0 eth0
# brctl addif br0 eth1
# dhclient br0
# tshark -i br0 -w capture.tcp src net 172.16.12.0/24 and dst net 172.16.12.0/24

(My firewall was getting traffic on non-bridged interfaces and trying to route it out the bridged interface, so I used the filter expression above to limit the captured traffic to only the "security" network.)
And voila, I see packets:
GET /device.xml HTTP/1.1
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130:6789
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
CONTENT-TYPE: text/xml
CONTENT-LENGTH: 998
LAST_MODIFIED: Tue, 27 Oct 2015 10:04:34 GMT
DATE: Tue, 27 Oct 2015 10:04:38 GMT
SERVER: Linux/2.6.18_SC-DM365 UPNP/1.0 Intel UPnP
CONNECTION: close

<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
    <major>1</major>
    <minor>0</minor>
</specVersion>
<URLBase>http://172.16.12.130:6789</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Wireless Network Camera:1</deviceType>
<friendlyName>iCamera-iCameraDFCD9E</friendlyName>
<manufacturer>iControl</manufacturer>
<manufacturerURL>http://www.icontrol.com</manufacturerURL>
<modelDescription>Wireless Network Camera </modelDescription>
<modelName>Wireless Network Camera</modelName>
<modelNumber>iCamera</modelNumber>
<UDN>uuid:4d751d74-694d-6564-6961-000e8fdfcd9e</UDN>
<serviceList><service>    <serviceType>urn:upnp:service:BasicService:1</serviceType>    <serviceId>urn:upnp:serviceId:BasicServiceId</serviceId>        <controlURL>/upnp/control/BasicServiceId</controlURL>        <eventSubURL>/upnp/control/BasicServiceId</eventSubURL>        <SCPDURL>/scpd_basic.xml</SCPDURL>    </service>
</serviceList>
<presentationURL>http://172.16.12.130</presentationURL></device>
</root>
There is some SSL traffic that I can't decrypt, but then things switch back to HTTP:
GET /adm/sysinfo.cgi HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Pragma: no-cache
Expires: Mon, 06 Jan 1990 00:00:01 GMT
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:04:38 GMT
Server: ip-camera

Firmware Version: V1.0.02R19
Serial Number:1307ZZ9063892
Note that the Authorization header here decodes to a username of "administrator" with an empty password. This seems like it's just getting the firmware version of the camera, checking to see if a firmware upgrade is required.
GET /adm/set_group.cgi?group=NETWORK&dns_server1=172.16.12.1&dns_type=1 HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:04:43 GMT
Server: ip-camera

OK
Using the same Authorization header, this appears to set the DNS (probably so that the next step can use DNS).
GET /adm/auto_upgrade.cgi?new_fw_url=http://edge.xfinity-home.top.comcast.net/firmware/DYW9HZ-308-1002R22.bin HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Status: 200
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:04:43 GMT
Server: ip-camera

OK
So it looks like this camera had firmware version 1.0.02R19, and the touchscreen wants it to get version 1.0.02R22. The request to perform the upgrade was sent 10 seconds in, and the touchscreen looks like it just waits 2 minutes. Then, at 129 seconds in, it tries to connect to the camera again. This fails, and the touchscreen starts periodically ARPing the camera.

At 383 seconds in, the camera appears to have finished whatever it was doing as it requests a DHCP from the Gateway router, does a bunch of IGD/UPNP negotiation with the Gateway router. Finally, at 402 seconds in, the touchscreen reconnects to the camera.
GET /adm/sysinfo.cgi HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Pragma: no-cache
Expires: Mon, 06 Jan 1990 00:00:01 GMT
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:11:17 GMT
Server: ip-camera

Firmware Version: V1.0.02R22
Serial Number:1307ZZ9063892
And it appears the upgrade succeeded, since the camera reports v1.0.02R22 now. The touchscreen then makes the same request again. That's weird.
GET /adm/sysinfo.cgi HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Pragma: no-cache
Expires: Mon, 06 Jan 1990 00:00:01 GMT
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:11:17 GMT
Server: ip-camera

Firmware Version: V1.0.02R22
Serial Number:1307ZZ9063892
And just for good measure, it checks a third time ("How many times did you do it?" "Three, you told me to always do it three times.")
GET /adm/sysinfo.cgi HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Pragma: no-cache
Expires: Mon, 06 Jan 1990 00:00:01 GMT
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:11:17 GMT
Server: ip-camera

Firmware Version: V1.0.02R22
Serial Number:1307ZZ9063892
And then things get interesting. Note that I'm breaking these requests up for clarity but they are actually being sent over a single HTTP connection in some cases. The first thing that happens is that the touchscreen tries to set the http and https mode. This fails because now authentication is required (is the username/password in the firmware that was just updated? or does it come from somewhere else?).
GET /adm/set_group.cgi?group=HTTP&https_mode=1&http_mode=1 HTTP/1.1
Host: 172.16.12.130
Connection: Keep-Alive

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Authorization"
Content-Type: text/html
Content-Length: 351
Date: Tue, 27 Oct 2015 10:11:17 GMT
Server: ip-camera

<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>401 - Unauthorized</title>
 </head>
 <body>
  <h1>401 - Unauthorized</h1>
 </body>
</html>
Denied! But then the touchscreen immediately tries again with username administrator and an empty password:
GET /adm/set_group.cgi?group=HTTP&https_mode=1&http_mode=1 HTTP/1.1
Host: 172.16.12.130
Connection: Keep-Alive
Authorization: Basic YWRtaW5pc3RyYXRvcjo=

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 10:11:21 GMT
Server: ip-camera

OK
And this succeeds. I'm not sure whether this enables both an http and https server, or sets some other kind of mode. The touchscreen proceeds to get the firmware version again (but I'm not going to show that yet again). And then, at last, things get interesting!
GET /adm/set_group.cgi?group=USER&admin_name=root&admin_password=dvRxPrwv&login_check=1&user1=icn0Pnl2%2CV53xEm4 HTTP/1.1
Authorization: Basic YWRtaW5pc3RyYXRvcjo=
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:11:32 GMT
Server: ip-camera

OK
Looks like it wants to set a username of root and a password of dvRxPrwv, and a set of "viewer" credentials of username icn0Pnl2 and a password of tV53xEm4 (the %2C there is of course the URL-encoded value for the colon ":", which is used in the HTTP authentication scheme to separate the username from the password).
GET /adm/set_group.cgi?group=SYSTEM&led_mode=1&daylight_saving=1&pir_mode=0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 10:11:32 GMT
Server: ip-camera

OK
This is probably not really important, blinking lights and time zones, etc.
GET /adm/set_group.cgi?group=WIRELESS&wlan_type=1&wlan_essid=XHS-8C1EB62D&wlan_security=2&wlan_domain=12&wpa_ascii=D765494564A6621854 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 10:11:36 GMT
Server: ip-camera

OK
More meat! The wireless network settings we should use! The rest, frankly, seems like administrivia.
GET /adm/set_group.cgi?group=AUDIO&audio_in=0&in_audio_type=1 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 11:11:41 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=HTTP_EVENT&http_post_url=&http_event_en=1 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 11:11:43 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=VIDEO&contrast=4&sharpness=2&exposure=4&text_overlay=0&default_channel=1&saturation=4&color=0&hue=4&dn_sch=0&time_stamp=0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 11:11:49 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=MOTION&md_mode=0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 11:11:49 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=UPNP&upnp_mode=0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 11:11:50 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=MPEG4&gov_length=20&quality_type=0&mode3=0&quality_level=3&resolution=3&mode2=0&frame_rate=15&bit_rate=6&mode=1 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 11:11:54 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=H264&mode3=0&quality_level2=3&gov_length2=10&resolution=3&resolution2=3&mode=1&gov_length=15&quality_type=0&quality_type2=0&bit_rate2=512&frame_rate2=5&mode2=1&frame_rate=15&bit_rate=768&profile=77 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 11:11:57 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=JPEG&frame_rate3=10&mode2=0&resolution3=3&mode3=1&quality_level3=5&mode=0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 11:12:02 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=EVENT&event_trigger=1&event_interval=0&event_attach=mp4%2C1%2C5%2C10&event_httpc=email%3A0%3Bftpu%3A0%3Bhttpn%3A0%3Bhttppost%3A1&event_pir=email%3A0%3Bftpu%3A0%3Bhttpn%3A0%3Bhttppost%3A0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Date: Tue, 27 Oct 2015 11:12:06 GMT
Server: ip-camera

OK
GET /adm/set_group.cgi?group=NETWORK&dns_type=1&dhcp=0&ip_addr=172.16.12.163&dns_server1=172.16.12.1&gateway=172.16.12.1 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.130
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 11:12:09 GMT
Server: ip-camera

OK
But note this -- interestingly, it's moving the camera to a new IP address. Not sure why but okay. There's a little bit of ARPing before the next connection can be made, since the camera switches IP addresses immediately.
GET /adm/set_group.cgi?group=JPEG&frame_rate3=15&mode2=0&resolution3=2&mode3=1&quality_level3=3&mode=0 HTTP/1.1
Authorization: Basic cm9vdDpkdkR4cnJ3dg==
User-Agent: Dalvik/1.4.0 (Linux; U; Android 2.3.7; TCA203 Build/R10.3.3-r1)
Host: 172.16.12.163
Connection: Keep-Alive
Accept-Encoding: gzip

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 4
Date: Tue, 27 Oct 2015 11:13:04 GMT
Server: ip-camera

OK
And that's it, all done.

Apparently, not news to anyone, this camera is an OEMed camera made by an overseas company called Sercomm, and many models exist (branded for each vendor). But they all (mostly) share (mostly) the same API. I found a pretty good reference at github, but there are others. IMHO the one at github does the best job listing all of the options available over the API.

A few interesting notes, thanks to the others who have investigated this:
  • The camera has a microphone, and if you turn it on you can hear it on the RTSP stream, at least. But it's a very bad microphone and you can barely hear it.
  • The camera might have a speaker. I can't tell about mine, but apparently some of them do. Maybe I'll try to do something with this at some point.
  • There is some kind of built-in motion detection that can be used to issue alerts to an HTTP server, and possibly other functionality. I haven't tried this either, but it seems like maybe having each camera notify a recording system of events rather than having the recorder streaming video from all of the cameras all of the time could be a much more efficient system.
  • There is a dynamic DNS update client. Not that these aren't a dime a dozen, and as the API reference notes that many of the DDNS providers are no longer working, this isn't a big deal, but it could be useful.
  • There is a Windows software package for managing the cameras. I haven't found it yet, but I haven't really looked either.
A few other notes:
  • The admin credentials for this camera are NOT the same as for the other cameras on my network, so there are apparently different credentials for each camera. Good Job, Comcast.
  • There are functions in the camera that seems like they could be abused -- the ability to do a wifi site survey and retrieve information about all of the wireless networks that the camera sees. I'm just not sure what purpose this could serve, and I worry (just a little bit) about that information leaking on such a massive scale since these cameras are quite popular.
  • In the same vein, you can do an SMB survey of the local network (which given it's an isolated hidden wifi network won't include your own machines). This could also potentially be abused, but I think the risk is mitigated by the network topology that Comcast enforces.


In conclusion, my thanks to Chad for pioneering this, and hopefully the protocol captures I've shown above might help someone figure out something about their camera.

Sunday, March 2, 2014

Hacking Xfinity's Home Security Cameras

I have a series of webcams running, one of which is monitored by Motion, a motion detection system. When Motion sees activity on the camera, it starts recording it to a flash video file with a snapshot, and I have hacked together a dirty webpage that allows navigating the recorded archives. A cron job handles cleanup by deleting everything older than 3 days.

Recently, we got an Xfinity Home Security system installed. I've done some research and determined that the sensors work on the ZigBee protocol (which is pretty common for home security sensors) and a second (locked down) Wifi network. The Wifi network appears to be used to connect the touchscreen (Android based, pretty sure it's a rebranded iControl device) to the wireless cameras (iCamera-1000), and to allow remote access to the touchscreen for settings, monitoring, and alerts sent by the touchscreen.

So my wife wants to put one of the cameras pointing at our front door, and record people who come to the door. The Xfinity system would actually support this, but only in a limited sense and only by combining the camera with a motion sensor -- the system can be configured to record a video clip when the motion sensor triggers. But I don't want to waste a motion sensor (nor am I sure that the IR sensor would function properly outdoors in the cold). Given that I already have a system capable of detecting motion and recording it, I wanted to integrate the new camera(s) into that system.

But Xfinity doesn't want me in that "security router". The don't give you the admin credentials to access the router, nor do they even give you the WPA2 key for the Wifi network. But it turns out that I can add a NIC to my firewall/router running Linux and just plug that into the back of the security router. It happily obtains a DHCP address from the security router, and is able to communicate with the cameras! I needed to run tcpdump and access the cameras from the remote Xfinity app to sniff the HTTP (Basic Authentication) username and password, but was able to access the camera from Motion after that.

Based on the web page that the camera serves up, it's capable of streaming H.264 and MJPEG, but Motion only supports MJPEG, I think. I might experiment with H.264 at some point to see if I can get it working, but for now I'm happy with what I've got.

I'm hoping that I'll be able to eventually set up some sort of monitoring for sensor status and/or alarm status. One of the detractions of the Xfinity system is that the touchscreen is the only device with "speakers" and if it's situated in a remote part of the house (ours is usually upstairs in the master bedroom) it can't be heard throughout the house. Our is barely audible in the living room but impossible to hear from the basement (where I spend most of my time). I'd like to be able to have my PC "chime" when the sensors get tripped so I'll know if one of the kids is opening doors. But so far, I haven't been able to see any network activity between the control box and the world at large...

Tuesday, October 15, 2013

Squirrelly Javascript

Lately, I've been maintaining our (very old) Joomla website, which is running a (very old) version of RSForm!Pro. To be honest, RSForm!Pro seems like a pretty decent product and even the old version we're running has lots of hooks were I've been able to cobble together things. But I'm getting ahead of myself...

Build me a form...

So the powers that be wanted a form added to the website that would send an e-mail when it was submitted. This is exactly the sort of thing that RSForm is designed to do, so it was the obvious choice. If I had known then what I know now, I might have approached this differently, but anyway.

One of the requirements that became apparent was that we only wanted some fields to be visible when other fields were selected. This is reasonably easy to do, and I was able to quickly implement this with the built-in functionality of RSForm. Where it gets tricky is that it's hard to validate the input on the hidden fields, because there's no way to say that a value is required but only if the field is visible.

Custom validation rules

The way I accomplished this was to add to the "Scripts" section of the form a "called on form display" script that set up some custom javascript code:
$jscript = "<script type='text/javascript'>

function customValidation() {
    var ret = true;
    
    // Two radio buttons, at least one must be a non-default value
    if ( document.getElementById('radio0-0').checked &&
         document.getElementById('radio1-0').checked )
    {
        document.getElementById('component359').className='formError';
        ret = false;
    }
    else
    {
        document.getElementById('component359').className='formNoError';
    }

    return ret;
}
</script>";

$formLayout = $jscript . $formLayout;

This leverages RSForm's built in naming (component359 is the error div for the radio button, and the classes are already defined to display or not). Our Javascript code is "prefixed" onto the formLayout variable which already contains the HTML for the form. In this way, we get our Javascript function into the HTML page. Then, all we have to do is to get the submit button to call this code when pressed, by using the "Additional Attributes" that RSForm lets us define.
onclick="javascript:return customValidation();"


You can do -- MAGICK!

Another requirement from management was that the form accept an e-mail address for the submitter, but that it also CC two optional addresses in addition. RSForm, unfortunately, has an issue with this. While it's easy to create the fields, and easy to add them to a "To" or "CC" line, since they're optional, the commas that would normally separate them need to be present (or not) depending on whether the fields have values (or not).

So we added a hidden field (ccs) to the form and added to our "Additional Attributes" on the submit button:
document.getElementById('ccs').value=
 [document.getElementById('cc1').value,
  document.getElementById('cc2').value].filter(function(e)
  {
   return(e!=='');
  }).join(',');

This means that the CCs field is now suitable for use in the CC field of the e-mail message. Nothing to it.

Subject of controversy

I use gmail to read my work mail. I started using it years ago and it's just stuck. Being able to search nearly instantly, as well as having the unique threaded model, has worked out well for me. But it makes it hard to track these form submissions as unique events because they all have the same subject. So I needed a way to add some sort of unique identifier to the submissions. I ended up using a timestamp since it would have a reasonable value and might be useful in other circumstances. I used the same trick of adding a hidden field, having my custom form validation javascript code set the value of the field when "Submit" is pressed, and then including the field value in the Subject.
if (!String.prototype.format) {
  String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) { 
      return typeof args[number] != 'undefined'
        ? args[number]
        : match
      ;
    });
  };
}

if (!String.prototype.pad) {
  String.prototype.pad = function(width, z) {
    z = z || '0';
    return this.length >= width ? this : new Array(width - this.length + 1).join(z) + this;
  };
}

function validateGoFields() {
    var ret = true;

    var now=new Date(new Date().getTime() - 420 * 60000);
    document.getElementById('timestamp').value = 
        new String('{0}{1}{2}-{3}{4}{5}').format(
            now.getUTCFullYear().toString().pad(4),
            now.getUTCMonth().toString().pad(2),
            now.getUTCDate().toString().pad(2),
            now.getUTCHours().toString().pad(2),
            now.getUTCMinutes().toString().pad(2),
            now.getUTCSeconds().toString().pad(2)
        );
I, by the way, am not really that smart. A lot of that was stolen from StackExchange and massaged to work the way I wanted it to.

(It's worth pointing out that the 420 nonsense is not a social commentary, it's -7 hours * 60 min/hr to give our timestamp in [hopefully] local time for the managers in California.)

One day I plan to learn Javascript so I can do this better.

Thursday, January 10, 2013

404 error handler

A long time ago, we reorganized our website in such a way that all the old URLs that had been indexed in search engines and immortalized in e-mails and blog posts were suddenly nonfunctional. I considered using something like mod_rewrite to redirect the old traffic to the new URLs, but this would be problematic for some URLs (like the URLs into our forum).

Now, this isn't rocket science, but I came up with a way to have my cake and eat it too. I set up an ErrorDocument handler called 404.php that was a script that can redirect people based on their incoming URLs, Referrers, etc:
<?php                                                                          
# Useful _SERVER variables:
# REDIRECT_URL - original URL of request
# REQUEST_URI  - original URI of request?

$map = array(
    "/old/url/path" => "http://www.domain.com/new/path/"
);
if ( $map[$_SERVER['REQUEST_URI']] ) # if the request is in the map
{
    header("HTTP/1.0 302 Moved");
    header("Location: ".$map[$_SERVER['REQUEST_URI']]);
    exit();
}
if ( substr($_SERVER['REQUEST_URI'], 0, 9) == "/download" )                    
{
    header("HTTP/1.0 302 Moved");
    header("Location: http://www.domain.com/new/download/path/");
    exit();
}
?>

I checked our access logs for 404 errors a few times a week and added into the map the most common hits.
Well, I thought it was clever...

Wednesday, October 3, 2012

How to avoid hard-coding Id values

So I have seen some tricks for avoiding hard-coding Id values in your Apex code, which I think is a good practice to follow, but they've always been sort of kludgey and inelegant. Today I saw one that I didn't realize you could use, and it deserves a place here:
    Id theId = [SELECT Id FROM <table> WHERE <criteria>].Id;
This is simple and elegant and exactly what I was looking for.

For loops and SOQL queries

One of my favorite loops in Apex is the for(collection) loop. It's very useful for iterating through a collection. It allows you to do something like:
    // Fix Asset records with null Product2Id fields.
    List<Asset> assets = [SELECT Id, Product_Family__c, Product2Id
                          FROM Asset
                          WHERE Product2Id = null
                         ];
    for(Asset asset : assets)
    {
        // fix asset.Product2Id field here
    }
    update assets;
As useful as this is, it turns out that there is a better way. If you use the for([SOQL]) loop instead, Apex will actually run the SOQL query and automatically querymore each time the loop recycles. This means that you don't have to generate a complete collection and use up the heap space to store the entire query result set. This version looks like:
    // Fix Asset records with null Product2Id fields.
    List<Asset> updatedAssets = new List<Asset>();
    for(Asset asset : [SELECT Id, Product_Family__c,Product2Id
                       FROM Asset
                       WHERE Product2Id = null
                      ])
    {
        // fix asset.Product2Id here
        updatedAssets.add(asset);
    }
    update updatedAssets;
I'm not sure how to bulkify the update without maintaining a collection of records, which sort of negates the benefits (in cases like this were all records get updated anyway) of using this form of loop. But in a case where not all records would be updated this form could save considerable heap space. I didn't try it but it appears that there is another way to perform this operation:
    // Fix Asset records with null Product2Id fields.
    for(Asset[] assets : [SELECT Id, Product_Family__c,Product2Id
                          FROM Asset
                          WHERE Product2Id = null
                         ])
    {
        for(Asset asset : assets)
        {
            // fix asset.Product2Id here
        }
        update assets;
    }
This reduces the heap used to store the entire DML update at the expense of the number of DML operations performed. It will update records 200-at-a-time but only 200 record will be put onto the heap at a time. The choice of which form to use is probably situationally dependent, but knowing the options obviously is the biggest part of the battle.

Search This Blog