├── debian ├── compat ├── dirs ├── docs ├── source │ └── format ├── kali-ci.yml ├── watch ├── gbp.conf ├── rules ├── changelog ├── control └── copyright ├── renovate.json ├── docs ├── changelog ├── ENUM_HOSTS └── readme.html ├── README.md └── miranda.py /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/bin 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | docs/* 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/kali-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - https://gitlab.com/kalilinux/tools/kali-ci-pipeline/raw/master/recipes/kali.yml 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=3 2 | 3 | https://code.google.com/p/mirandaupnptool/downloads/list .*/miranda-(\d[\d.]*)\.(?:tgz|tbz2|txz|tar\.gz|tar\.bz2|tar\.xz) 4 | -------------------------------------------------------------------------------- /debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | debian-branch = kali/master 3 | debian-tag = kali/%(version)s 4 | pristine-tar = True 5 | 6 | [pq] 7 | patch-numbers = False 8 | 9 | [dch] 10 | multimaint-merge = True 11 | -------------------------------------------------------------------------------- /docs/changelog: -------------------------------------------------------------------------------- 1 | 2009-02-19 Craig Heffner 2 | 3 | * Miranda v1.1 released. 4 | 5 | * Bugfix in receive loop, thanks to Johannes Veser. 6 | 7 | 2008-11-03 Craig Heffner 8 | 9 | *Initial release of Miranda v1.0. 10 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ 14 | 15 | override_dh_auto_install: 16 | dh_installdirs 17 | install miranda.py $(CURDIR)/debian/miranda/usr/bin/miranda 18 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | miranda (1.0-1kali3) kali-dev; urgency=medium 2 | 3 | * Drop useless dependency on libreadline6 4 | 5 | -- Sophie Brun Mon, 23 Jan 2017 14:25:03 +0100 6 | 7 | miranda (1.0-1kali2) kali-dev; urgency=medium 8 | 9 | * Drop dependency on python-support (obsolete package) 10 | * Use debhelper 9 11 | 12 | -- Sophie Brun Thu, 28 Jan 2016 14:35:16 +0100 13 | 14 | miranda (1.0-1kali1) kali; urgency=low 15 | 16 | * Updated watch file 17 | 18 | -- Mati Aharoni Sun, 12 Jan 2014 19:18:51 -0500 19 | 20 | miranda (1.0-1kali0) kali; urgency=low 21 | 22 | * Initial release 23 | 24 | -- Devon Kearns Wed, 09 Jan 2013 09:06:52 -0700 25 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: miranda 2 | Section: net 3 | Priority: extra 4 | Maintainer: Kali Developers 5 | Build-Depends: debhelper (>= 9) 6 | Standards-Version: 3.9.3 7 | Homepage: http://code.google.com/p/mirandaupnptool/ 8 | Vcs-Git: https://gitlab.com/kalilinux/packages/miranda.git 9 | Vcs-Browser: https://gitlab.com/kalilinux/packages/miranda 10 | 11 | Package: miranda 12 | Architecture: all 13 | Depends: ${misc:Depends}, python, python-jsonpickle 14 | Description: UPNP administration tool 15 | Miranda is a Python-based Universal Plug-N-Play client 16 | application designed to discover, query and interact with 17 | UPNP devices, particularly Internet Gateway Devices (aka, 18 | routers). It can be used to audit UPNP-enabled devices on a 19 | network for possible vulnerabilities. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SourceSec Security Research Group's Miranda UPNP Administration Tool 2 | 3 | Miranda is a Python-based Universal Plug-N-Play client application designed to discover, query and interact with UPNP devices, particularly Internet Gateway Devices (aka, routers). It can be used to audit UPNP-enabled devices on a network for possible vulnerabilities. Some of its features include: 4 | 5 | Interactive shell with tab completion and command history 6 | Passive and active discovery of UPNP devices 7 | Customizable MSEARCH queries (query for specific devices/services) 8 | Full control over application settings such as IP addresses, ports and headers 9 | Simple enumeration of UPNP devices, services, actions and variables 10 | Correlation of input/output state variables with service actions 11 | Ability to send actions to UPNP services/devices 12 | Ability to save data to file for later analysis and collaboration 13 | Command logging 14 | Miranda was built on and for a Linux system and has been tested on a Linux 5.4 kernel with Python 3.8. However, since it is written in Python, most functionality should be available for any Python-supported platform. Miranda has been tested against IGDs from various vendors, including Linksys, D-Link, Belkin and ActionTec. All Python modules came installed by default on a Linux Mint 5 (Ubuntu 8.04) test system. 15 | 16 | For more information about UPNP, visit the UPNP Forum. 17 | 18 | For information regarding UPNP vulnerabilities, see UPNP Hacks and GNUCitizen. 19 | 20 | Check out the Plug-N-Play Network Hacking article at the Ethical Hacker Network site. This is great use-case for the tool as well as the basics of UPNP. 21 | -------------------------------------------------------------------------------- /docs/ENUM_HOSTS: -------------------------------------------------------------------------------- 1 | #Sample ENUM_HOSTS structure (truncated...). This structure can be enumerated using the 'host info' command. 2 | 3 | self.ENUM_HOSTS[0] = { 4 | 'name' : '192.168.0.1:5678', 5 | 'dataComplete' : False, 6 | 'proto' : 'http://', 7 | 'xmlFile' : 'http://192.168.0.1:5678/igd.xml', 8 | 'deviceList' : { 9 | 'InternetGatewayDevice' : { 10 | 'fullName' : 'urn:schemas-upnp-org:device:InternetGatewayDevice:1', 11 | 'manufacturerURL' : 'http://www.linksys.com/', 12 | 'modelURL' : 'http://www.linksys.com/', 13 | 'presentationURL' : None, 14 | 'modelName' : 'WRT54G', 15 | 'modelNumber' : 'v3.03.9', 16 | 'serverType' : 'LINUX/2.4 UPnP/1.0 BRCM400/1.0', 17 | 'deviceType' : 'urn:schemas-upnp-org:device:InternetGatewayDevice:1', 18 | 'friendlyName' : 'Residential Gateway Device', 19 | 'modelDescription' : 'Internet Access Server', 20 | 'UDN' : 'uuid:0014-bf39-23280000fedc', 21 | 'UPC' : None, 22 | 'manufacturer' : 'Linksys Inc.', 23 | 'services' : { 24 | 'WANIPConnection' : { 25 | 'fullName' : 'urn:schemas-upnp-org:service:WANIPConnection:1', 26 | 'controlURL' : '/uuid:0014-bf39-23280200fedc/WANIPConnection:1', 27 | 'eventSubURL' : '/uuid:0014-bf39-23280200fedc/WANIPConnection:1', 28 | 'serviceId' : 'urn:upnp-org:serviceId:WANIPConn1', 29 | 'SCPDURL' : '/dynsvc/WANIPConnection:1.xml' 30 | 'actions' : { 31 | 'setDefaultConnectionService' : { 32 | 'arguments' : { 33 | 'NewDefaultConnectionService' : { 34 | 'direction' : 'in', 35 | 'relatedStateVariable' : 'DefaultConnectionService' 36 | } 37 | } 38 | } 39 | } 40 | 'serviceStateVariables' : { 41 | 'DefaultConnectionService' : { 42 | 'dataType' : 'string' 43 | 'sendEvents' : None, 44 | 'allowedValueList' : ['TCP','UDP'] 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: miranda 3 | Source: http://code.google.com/p/mirandaupnptool/ 4 | 5 | Files: * 6 | Copyright: Craig Heffner 7 | License: MIT 8 | Permission is hereby granted, free of charge, to any person 9 | obtaining a copy of this software and associated 10 | documentation files (the "Software"), to deal in the 11 | Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, 13 | sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, 15 | subject to the following conditions: 16 | . 17 | The above copyright notice and this permission notice shall 18 | be included in all copies or substantial portions of the 19 | Software. 20 | . 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 22 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 23 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 24 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 25 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 26 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 27 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 28 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | Files: debian/* 31 | Copyright: 2013 Devon Kearns 32 | License: GPL-2+ 33 | This package is free software; you can redistribute it and/or modify 34 | it under the terms of the GNU General Public License as published by 35 | the Free Software Foundation; either version 2 of the License, or 36 | (at your option) any later version. 37 | . 38 | This package is distributed in the hope that it will be useful, 39 | but WITHOUT ANY WARRANTY; without even the implied warranty of 40 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 41 | GNU General Public License for more details. 42 | . 43 | You should have received a copy of the GNU General Public License 44 | along with this program. If not, see 45 | . 46 | On Debian systems, the complete text of the GNU General 47 | Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". 48 | -------------------------------------------------------------------------------- /docs/readme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48 | 49 | 50 |
51 | 52 |

Description

53 |

54 | Despite the wide spread use of the Universal Plug-N-Play protocol in applications, operating systems and embedded devices, few tools exist that allow 55 | simple discovery and interaction with UPNP-enabled devices. Further, of the tools that do exist, most or all are closed-source Windows binaries. 56 | Miranda is a Python-based UPNP client application designed to discover, query and interact with UPNP devices, particularly Internet 57 | Gateway Devices (aka, routers). 58 |

59 | 60 |

Features

61 |

62 |

    63 |
  • Interactive shell with tab completion and command history
  • 64 |
  • Passive and active discovery of UPNP devices
  • 65 |
  • Customizable MSEARCH queries (query for specific devices/services)
  • 66 |
  • Full control over application settings such as IP addresses, ports and headers
  • 67 |
  • Simple enumeration of UPNP devices, services, actions and variables
  • 68 |
  • Correlation of input/output state variables with service actions
  • 69 |
  • Ability to send actions to UPNP services/devices
  • 70 |
  • Ability to save data to file for later analysis and collaberation
  • 71 |
  • Command logging
  • 72 |
73 |

74 | 75 |

System Requirements

76 |

77 | Miranda was built on and for a Linux system and has been tested on a Linux 2.6 kernel with Python 2.5. 78 | However, since it is written in Python, most functionality *should* be available for any Python-supported 79 | platform. 80 |

81 |

82 | Miranda has been tested against IGDs from various vendors, including Linksys, D-Link, and ActionTec. 83 | All Python modules came installed by default on a Linux Mint 5 (Ubuntu 8.04) test system. 84 |

85 | 86 |

CLI Usage

87 |

88 |

 89 | 	./miranda.py [OPTIONS]
 90 | 
 91 | 	        -s <struct file>        Load previous host data from struct file
 92 | 	        -l <log file>           Log user-supplied commands to log file
 93 | 		-i <interface>		Specify the name of the interface to use (Linux only, requires root)
 94 | 	        -u                      Disable show-uniq-hosts-only option
 95 | 		-v			Enable verbose mode
 96 | 	        -d                      Enable debug mode
 97 | 	        -h                      Show command line help
 98 | 
 99 | 	If run with no options, you will be dropped into the interactive shell with the default settings.
100 | 
101 |

102 | 103 |

Shell Usage

104 |

105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 |
msearchActively locate UPNP hosts
pcapPassively listen for UPNP hosts
hostView host list and host information
saveSave current host data to file
loadRestore previous host data from file
logLogs user-supplied commands to a log file
headShow/define HTTP headers
setiShow/define application settings
helpShow program help
quitExit the shell
exitExit the shell
141 |

142 |

143 | Many of the shell commands support various sub-commands. Miranda is designed to be as self-documenting 144 | as possible, so use '<command> help' for specific command usage, descriptions and examples. 145 |

146 | 147 |

Usage Tutorial

148 | 149 |

Introduction

150 |

151 | While this tutorial will not cover every command and option available in Miranda, it will walk you through the basic 152 | usage and demonstrate the tool's major capabilities. 153 |

154 | 155 |

Discovering UPNP Hosts

156 |

157 | Upon running Miranda, you will be greeted with a 'upnp>' prompt. You will likely wish to discover all UPNP hosts on your 158 | network first; this can be done with the msearch or pcap commands. The difference is that pcap will passively listen 159 | for SSDP notification messages sent out by UPNP hosts, while msearch will actively query the network for UPNP hosts. In 160 | this example, we will use the msearch command: 161 |

162 |
163 | 			upnp> msearch 
164 | 
165 | 			Entering dicovery mode for 'upnp:rootdevice', Ctl+C to stop...
166 | 
167 | 			****************************************************************
168 | 			SSDP reply message from 192.168.1.1:2869
169 | 			XML file is located at http://192.168.1.1:2869/IGatewayDeviceDescDoc
170 | 			Device is running VxWorks/5.4.2 UPnP/1.0 iGateway/1.1
171 | 			****************************************************************
172 | 
173 | 			Discover mode halted...
174 | 
175 |

176 | Here you can see that we found one host on the network (in this case, the network's Linksys router). When run without any 177 | arguments, the msearch command will query the network for all UPNP root devices. However, if we had only been interested 178 | in UPNP hosts that are of a certian device type, or that offer a particular service, we could have queried the network 179 | for only hosts matching our criteria. For example, to search only for WANDevice UPNP devices, we could have run: 180 |

181 |
182 | 			upnp> msearch device WANDevice
183 | 
184 |

185 | Likewise, if we only wanted to find hosts that support the WANIPConnection service, we could have run: 186 |

187 |
		
188 | 			upnp> msearch service WANIPConnection
189 | 
190 | 191 |

Listing UPNP Hosts

192 |

193 | The 'host list' command will display all discovered hosts along with their host index number: 194 |

195 |
196 | 			upnp> host list 
197 | 
198 | 			[0] 192.168.1.1:2869
199 | 
200 |

201 | Since the Linksys router was the first (and in this case, only) host discovered, it has a host index number of 0. This index 202 | number will be used to reference this particular host in subsequent commands. 203 |

204 | 205 |

Viewing Host Info, Part 1

206 |

207 | Before moving on, let's look at a few other host commands that we can run. At this point it is important to note that all 208 | of the 'host' commands feature full tab completion; if you're unsure of what options are available to you, or what values 209 | are in a particular piece of the host data structure, pressing TAB twice will show you. 210 |

211 |

212 | The first command we will look at is 'host summary'; this command will 213 | display a summary of the host, along with the host's device type(s) and device info. Since we haven't enumerated any of the 214 | device types and services supported by the Linksys router, this command will only display a couple lines of information that 215 | identify the host and the location of the host's main UPNP XML file: 216 |

217 |
218 | 			upnp> host summary 0
219 | 
220 | 			Host: 192.168.1.1:2869
221 | 			XML File: http://192.168.1.1:2869/IGatewayDeviceDescDoc
222 | 
223 |

224 | Next, there is the 'host info' command that lets you walk through the entire data structure that holds information about 225 | the hosts that we've discovered. Running 'host info 0' shows the following: 226 |

227 |
228 | 			upnp> host info 0
229 | 
230 | 			xmlFile : http://192.168.1.1:2869/IGatewayDeviceDescDoc
231 | 			name : 192.168.1.1:2869
232 | 			proto : http://
233 | 			serverType : None
234 | 			upnpServer : VxWorks/5.4.2 UPnP/1.0 iGateway/1.1
235 | 			dataComplete : False
236 | 			deviceList : {}
237 | 
238 |

239 | You can see that the dataComplete field is set to false, indicating that we have not enumerated any detailed information about 240 | this host. However, we do know a little bit about the host just from the results of running the msearch command, including the 241 | HTTP Server header that it is using, as indicated by the upnpServer field. Note thate the value of the deviceList field is '{}'. 242 | Any field with this value indicates that it contains data sub-sets which can be further displayed with the 'host info' command 243 | like so: 244 |

245 |
246 | 			upnp> host info 0 deviceList
247 | 
248 |

249 | Because we have not discovered what type of UPNP device the Linksys router is, this command will return no data at this time. 250 |

251 |

252 | There is also the 'host details' command that will display all devices, services, actions, arguments, etc, related to a 253 | particular host. Again, we have not discovered this information yet, and the 'host details' command tells us so: 254 |

255 |
256 | 			upnp> host details 0
257 | 
258 | 			Can't show host info because I don't have it. Please run 'host get 0'
259 | 
260 | 261 |

Getting Host Info

262 |

263 | We'll take the 'host details' suggestion and run the 'host get' command. This command will request and parse all device and service 264 | XML files that are advertised by the host, and place the extracted data into the host data structure so that we can view it using the previously mentioned host commands: 265 |

266 |
267 | 			upnp> host get 0
268 | 
269 | 			Requesting device and service info for 192.168.1.1:2869 (this could take a few seconds)...
270 | 
271 | 			Host data enumeration complete!
272 | 
273 | 274 |

Viewing Host Info, Part 2

275 |

276 | Now, let's try running the 'host summary' command again and see what it reports: 277 |

278 |
279 | 			upnp> host summary 0
280 | 
281 | 			Host: 192.168.1.1:2869
282 | 			XML File: http://192.168.1.1:2869/IGatewayDeviceDescDoc
283 | 			WANConnectionDevice
284 | 				manufacturerURL: http://www.linksys.com/
285 | 				modelName: WTR54AG
286 | 				UPC: IGateway-01
287 | 				modelNumber: WTR54AG-01
288 | 				presentationURL: None
289 | 				fullName: urn:schemas-upnp-org:device:WANConnectionDevice:1
290 | 				friendlyName: WANConnectionDevice1
291 | 				modelURL: http://www.linksys.com/
292 | 				modelDescription: WTR54AG
293 | 				UDN: uuid:34bc065f-e59a-1612-9be5-c67e816b4bfb
294 | 				manufacturer: Linksys
295 | 			WANDevice
296 | 				manufacturerURL: http://www.linksys.com/
297 | 				modelName: WRT54G
298 | 				UPC: IGateway-01
299 | 				modelNumber: WRT54G-01
300 | 				presentationURL: None
301 | 				fullName: urn:schemas-upnp-org:device:WANDevice:1
302 | 				friendlyName: WANDevice
303 | 				modelURL: http://www.linksys.com/
304 | 				modelDescription: WRT54G
305 | 				UDN: uuid:28f8f50a-e59a-1612-9be4-c67e816b4bfb
306 | 				manufacturer: Linksys
307 | 			InternetGatewayDevice
308 | 				manufacturerURL: http://www.linksys.com/
309 | 				modelName: WRT54G
310 | 				UPC: IGateway-01
311 | 				modelNumber: WRT54G-01
312 | 				presentationURL: http://192.168.1.1:80/
313 | 				fullName : urn:schemas-upnp-org:device:InternetGatewayDevice:1
314 | 				friendlyName: WRT54G
315 | 				modelURL: http://www.linksys.com/
316 | 				modelDescription: WRT54G
317 | 				UDN: uuid:13814000-4ff1-11f2-9be3-c67e816b4bfb
318 | 				manufacturer: Linksys
319 | 
320 |

321 | If we hadn't known that this was a Linksys device before, we do now! The router is actually advertising itself as three 322 | UPNP devices: a WANConnectionDevice, a WANDevice, and an InternetGatewayDevice. 323 |

324 | 325 |

Saving Your Data

326 |

327 | You can also try re-running the 'host details 0' command; for clarity and brevity, the output will not be shown here 328 | as this command will spit out everything it knows about the host and its devices/services, which at this point is quite 329 | a bit. You will probably want to save this output to disk in order to view it more easily; this can be done with the 330 | 'save info' command: 331 |

332 |
333 | 			upnp> save info 0 wrt54g
334 | 
335 | 			Host info for '192.168.1.1:2869' saved to 'info_wrt54g.mir'
336 | 
337 |

338 | The 'wrt54g' file name is an optional argument; if it had not been supplied, then the host index number would have been used ('info_0.mir'). 339 |

340 | 341 |

342 | If you wish to save your data to share with others or to view at a later date, you can 343 | use the 'save data' command. This will save the entire host structure that contains all the information about all of the UPNP hosts 344 | that you have discovered and enumerated during your session: 345 |

346 |
347 | 			upnp> save data wrt54g
348 | 
349 | 			Host data saved to 'struct_wrt54g.mir'
350 | 
351 |

352 | This data can later be imported back into Miranda using the 'load' command: 353 |

354 |
355 | 			upnp> load struct_wrt54g.mir 
356 | 
357 | 			Host data restored:
358 | 
359 | 				[0] 192.168.1.1:2869
360 | 
361 |

362 | Because this data structure is saved using Python's pickle module, any other Python script can load the file for analysis using pickle. 363 |

364 | 365 |

Analyzing Host Information

366 |

367 | Let's now see if we can view the deviceList values with the 'host info' command that we tried earlier: 368 |

369 |
370 | 			upnp> host info 0 deviceList
371 | 
372 | 			WANConnectionDevice : {}
373 | 			WANDevice : {}
374 | 			InternetGatewayDevice : {}
375 | 
376 |

377 | The three device types are listed here, and they have additional information that can be enumerated. You can explore the various 378 | fields and options as you like, but for brevity, we will examine only a couple of the most interesting; the first of these 379 | is the 'services' field which exists for each device listed in the deviceList. Taking a look at the services field for the WANConnectionDevice 380 | shows that it offers two services, WANIPConnection and WANEthernetLinkConfig: 381 |

382 |
383 | 			upnp> host info 0 deviceList WANConnectionDevice services
384 | 
385 | 			WANIPConnection : {}
386 | 			WANEthernetLinkConfig : {}
387 | 
388 |

389 | Each service also contains several sub-fields, but the one that we are most concerned with is the 'actions' field which shows 390 | the actions that each service supports (if this command looks too long to type, don't worry; use the tab completion!): 391 |

392 |
393 | 			upnp> host info 0 deviceList WANConnectionDevice services WANIPConnection actions
394 | 
395 | 			AddPortMapping : {}
396 | 			GetWarnDisconnectDelay : {}
397 | 			GetGenericPortMappingEntry : {}
398 | 			GetSpecificPortMappingEntry : {}
399 | 			RequestTermination : {}
400 | 			ForceTermination : {}
401 | 			GetExternalIPAddress : {}
402 | 			GetConnectionTypeInfo : {}
403 | 			GetIdleDisconnectTime : {}
404 | 			GetStatusInfo : {}
405 | 			SetConnectionType : {}
406 | 			DeletePortMapping : {}
407 | 			GetAutoDisconnectTime : {}
408 | 			RequestConnection : {}
409 | 			GetNATRSIPStatus : {}
410 | 
411 | 412 |

Sending UPNP Commands

413 |

414 | Now that we know what devices, services, and actions exist, we can start sending UPNP commands to the Linksys router. We will try 415 | running the GetExternalIPAddress action that is supported by the WANIPConnection service offered by the WANConnectionDevice device. 416 | To send commands to a UPNP host, use the 'host send' command; you must specify the host index number, the device name, the service 417 | name, and the action name, in that order. If the action requires any input values, you will be prompted for them automatically, as 418 | well as being informed of those value's type, allowed use, and default values/ranges, if any. The GetExternalIPAddress does not 419 | require any input, so it runs immediately: 420 |

421 |
422 | 			upnp> host send 0 WANConnectionDevice WANIPConnection GetExternalIPAddress
423 | 
424 | 			NewExternalIPAddress : 69.123.45.678
425 | 
426 |

427 | The NewExternalIPAddress is the name of the output service state variable associated with the GetExternalIPAddress (some actions have 428 | several variables associated with them, but in this case there is only one), and 69.123.45.678 is the value that the UPNP host returned for that variable, which in this case is the IP address of the WAN interface. 429 |

430 | 431 |

432 | Now let's look at a more complex request; we will attempt to forward data from port 8080 of the external WAN interface to port 80 of 433 | the router via the AddPortMapping action, essentially enabling remote administration for the router: 434 |

435 |
436 | 			upnp> host send 0 WANConnectionDevice WANIPConnection AddPortMapping 
437 | 
438 | 			Required argument:
439 | 				Argument Name:  NewPortMappingDescription
440 | 				Data Type:      string
441 | 				Allowed Values: []
442 | 				Set NewPortMappingDescription value to: Test Description
443 | 
444 | 			Required argument:
445 | 				Argument Name:  NewLeaseDuration
446 | 				Data Type:      ui4
447 | 				Allowed Values: []
448 | 				Set NewLeaseDuration value to: 0
449 | 
450 | 			Required argument:
451 | 				Argument Name:  NewInternalClient
452 | 				Data Type:      string
453 | 				Allowed Values: []
454 | 				Set NewInternalClient value to: 192.168.1.1
455 | 
456 | 			Required argument:
457 | 				Argument Name:  NewEnabled
458 | 				Data Type:      boolean
459 | 				Allowed Values: []
460 | 				Set NewEnabled value to: 1
461 | 
462 | 			Required argument:
463 | 				Argument Name:  NewExternalPort
464 | 				Data Type:      ui2
465 | 				Allowed Values: []
466 | 				Set NewExternalPort value to: 8080
467 | 
468 | 			Required argument:
469 | 				Argument Name:  NewRemoteHost
470 | 				Data Type:      string
471 | 				Allowed Values: []
472 | 				Set NewRemoteHost value to: 
473 | 
474 | 			Required argument:
475 | 				Argument Name:  NewProtocol
476 | 				Data Type:      string
477 | 				Allowed Values: ['TCP', 'UDP']
478 | 				Set NewProtocol value to: TCP
479 | 
480 | 			Required argument:
481 | 				Argument Name:  NewInternalPort
482 | 				Data Type:      ui2
483 | 				Allowed Values: []
484 | 				Set NewInternalPort value to: 80
485 | 
486 | 
487 | 488 |

489 | Note that several values were required to run this action, and that we were prompted for each one. Note that boolean values are 490 | either '1' (true) or '0' (false). By leaving the NewRemoteHost value blank, we allow any remote host to use this port mapping. 491 | Since this action does not return any values, there is no output (no news is good news). 492 |

493 |

494 | We can verify that the port mapping was successful by invoking the GetSpecificPortMappingEntry action; this action requires that 495 | we input the external port number, external host, and protocol type of the port mapping entry we are interested in: 496 |

497 |
498 | 			upnp> host send 0 WANConnectionDevice WANIPConnection GetSpecificPortMappingEntry 
499 | 
500 | 			Required argument:
501 | 				Argument Name:  NewExternalPort
502 | 				Data Type:      ui2
503 | 				Allowed Values: []
504 | 				Set NewExternalPort value to: 8080
505 | 
506 | 			Required argument:
507 | 				Argument Name:  NewRemoteHost
508 | 				Data Type:      string
509 | 				Allowed Values: []
510 | 				Set NewRemoteHost value to: 
511 | 
512 | 			Required argument:
513 | 				Argument Name:  NewProtocol
514 | 				Data Type:      string
515 | 				Allowed Values: ['TCP', 'UDP']
516 | 				Set NewProtocol value to: TCP
517 | 
518 | 			NewPortMappingDescription : Test Description
519 | 			NewLeaseDuration : 0
520 | 			NewInternalClient : 192.168.1.1
521 | 			NewEnabled : 1
522 | 			NewInternalPort : 80
523 | 
524 |

525 | Finally, we can delete this port mapping entry using the DeletePortMapping action, which requires the same input parameters as the 526 | GetSpecificPortMappingEntry action did: 527 |

528 |
529 | 			upnp> host send 0 WANConnectionDevice WANIPConnection DeletePortMapping 
530 | 
531 | 			Required argument:
532 | 				Argument Name:  NewProtocol
533 | 				Data Type:      string
534 | 				Allowed Values: ['TCP', 'UDP']
535 | 				Set NewProtocol value to: TCP
536 | 
537 | 			Required argument:
538 | 				Argument Name:  NewExternalPort
539 | 				Data Type:      ui2
540 | 				Allowed Values: []
541 | 				Set NewExternalPort value to: 8080
542 | 
543 | 			Required argument:
544 | 				Argument Name:  NewRemoteHost
545 | 				Data Type:      string
546 | 				Allowed Values: []
547 | 				Set NewRemoteHost value to: 
548 | 
549 | 
550 |

551 | Again, no news is good news, and if we try to run GetSpecificPortMappingEntry after deleting the port mapping, we get 552 | an error indicating that the port mapping no longer exists: 553 |

554 |
555 | 			upnp> host send 0 WANConnectionDevice WANIPConnection GetSpecificPortMappingEntry 
556 | 
557 | 			Required argument:
558 | 				Argument Name:  NewExternalPort
559 | 				Data Type:      ui2
560 | 				Allowed Values: []
561 | 				Set NewExternalPort value to: 8080
562 | 
563 | 			Required argument:
564 | 				Argument Name:  NewRemoteHost
565 | 				Data Type:      string
566 | 				Allowed Values: []
567 | 				Set NewRemoteHost value to: 
568 | 
569 | 			Required argument:
570 | 				Argument Name:  NewProtocol
571 | 				Data Type:      string
572 | 				Allowed Values: ['TCP', 'UDP']
573 | 				Set NewProtocol value to: TCP
574 | 
575 | 			Request for 'http://192.168.1.1:2869/WANIPConnCtrlUrl' failed with error code: 500
576 | 			SOAP error message: NoSuchEntryInArray
577 | 
578 | 
579 | 580 |

Conclusion

581 |

582 | Miranda has many other features, and is designed to be self-documenting; all of the shell commands have their own help information 583 | that detail usage and sub-commands, and provide descriptions and examples. However, the above command set comprises 99% of what you 584 | will probably want to use Miranda for, and details the steps to discover and interact with UPNP devices on your network. 585 |

586 | 587 |

General Notes

588 |

589 |

    590 |
  • Base64 Data Types 591 |
      592 |
    • If an input value's data type is bin.base64, you may enter the data in plain text; Miranda will base64 encode the string before sending it to the UPNP host.
    • 593 |
    • If an output value's data type is bin.base64, Miranda will base64 decode the data before displaying it to you.
    • 594 |
    595 |
  • 596 |
  • Debug Mode 597 |
      598 |
    • By default the debug mode is disabled; it can be enabled by issuing the 'seti debug' command from the Miranda shell, or by specifying the -d option on the command line.
    • 599 |
    • In debug mode, the debug command is enabled; this command will eval() whatever you pass to it, which makes it useful for viewing the contents of data structures and the like.
    • 600 |
    601 |
  • 602 |
  • Duplicate Host Entries 603 |
      604 |
    • If you run the pcap/msearch commands long enough, they will see the same UPNP hosts re-broadcasting themselves on the network. By default, a host is only reported once, and duplicate discoveries of that host are ignored.
    • 605 |
    • If you wish for duplicate discoveries to be reported, disable the unique host option using the 'seti uniq' command from the Miranda shell, or by specifying the -u option on the command line.
    • 606 |
    607 |
  • 608 |
609 |

610 | 611 |

Contact

612 |

613 | Send all comments/suggestions/bugs/etc to Craig Heffner, dev [at] sourcesec.com. 614 |

615 | 616 | 619 | 620 | 621 | -------------------------------------------------------------------------------- /miranda.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ################################ 3 | # Interactive UPNP application # 4 | # Craig Heffner # 5 | # www.sourcesec.com # 6 | # 07/16/2008 # 7 | ################################ 8 | 9 | try: 10 | import sys,os 11 | from socket import * 12 | from urllib2 import URLError, HTTPError 13 | from platform import system as thisSystem 14 | import xml.dom.minidom as minidom 15 | import IN,urllib,urllib2 16 | import readline,time 17 | import pickle 18 | import struct 19 | import base64 20 | import re 21 | import getopt 22 | except Exception,e: 23 | print 'Unmet dependency:',e 24 | sys.exit(1) 25 | 26 | #Most of the cmdCompleter class was originally written by John Kenyan 27 | #It serves to tab-complete commands inside the program's shell 28 | class cmdCompleter: 29 | def __init__(self,commands): 30 | self.commands = commands 31 | 32 | #Traverses the list of available commands 33 | def traverse(self,tokens,tree): 34 | retVal = [] 35 | 36 | #If there are no commands, or no user input, return null 37 | if tree is None or len(tokens) == 0: 38 | return [] 39 | #If there is only one word, only auto-complete the primary commands 40 | elif len(tokens) == 1: 41 | retVal = [x+' ' for x in tree if x.startswith(tokens[0])] 42 | #Else auto-complete for the sub-commands 43 | elif tokens[0] in tree.keys(): 44 | retVal = self.traverse(tokens[1:],tree[tokens[0]]) 45 | return retVal 46 | 47 | #Returns a list of possible commands that match the partial command that the user has entered 48 | def complete(self,text,state): 49 | try: 50 | tokens = readline.get_line_buffer().split() 51 | if not tokens or readline.get_line_buffer()[-1] == ' ': 52 | tokens.append('') 53 | results = self.traverse(tokens,self.commands) + [None] 54 | return results[state] 55 | except: 56 | return 57 | 58 | #UPNP class for getting, sending and parsing SSDP/SOAP XML data (among other things...) 59 | class upnp: 60 | ip = False 61 | port = False 62 | completer = False 63 | msearchHeaders = { 64 | 'MAN' : '"ssdp:discover"', 65 | 'MX' : '2' 66 | } 67 | DEFAULT_IP = "239.255.255.250" 68 | DEFAULT_PORT = 1900 69 | UPNP_VERSION = '1.0' 70 | MAX_RECV = 8192 71 | HTTP_HEADERS = [] 72 | ENUM_HOSTS = {} 73 | VERBOSE = False 74 | UNIQ = False 75 | DEBUG = False 76 | LOG_FILE = False 77 | IFACE = None 78 | STARS = '****************************************************************' 79 | csock = False 80 | ssock = False 81 | 82 | def __init__(self,ip,port,iface,appCommands): 83 | if appCommands: 84 | self.completer = cmdCompleter(appCommands) 85 | if self.initSockets(ip,port,iface) == False: 86 | print 'UPNP class initialization failed!' 87 | print 'Bye!' 88 | sys.exit(1) 89 | else: 90 | self.soapEnd = re.compile('<\/.*:envelope>') 91 | 92 | #Initialize default sockets 93 | def initSockets(self,ip,port,iface): 94 | if self.csock: 95 | self.csock.close() 96 | if self.ssock: 97 | self.ssock.close() 98 | 99 | if iface != None: 100 | self.IFACE = iface 101 | if not ip: 102 | ip = self.DEFAULT_IP 103 | if not port: 104 | port = self.DEFAULT_PORT 105 | self.port = port 106 | self.ip = ip 107 | 108 | try: 109 | #This is needed to join a multicast group 110 | self.mreq = struct.pack("4sl",inet_aton(ip),INADDR_ANY) 111 | 112 | #Set up client socket 113 | self.csock = socket(AF_INET,SOCK_DGRAM) 114 | self.csock.setsockopt(IPPROTO_IP,IP_MULTICAST_TTL,2) 115 | 116 | #Set up server socket 117 | self.ssock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) 118 | self.ssock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 119 | 120 | #Only bind to this interface 121 | if self.IFACE != None: 122 | print '\nBinding to interface',self.IFACE,'...\n' 123 | self.ssock.setsockopt(SOL_SOCKET,IN.SO_BINDTODEVICE,struct.pack("%ds" % (len(self.IFACE)+1,), self.IFACE)) 124 | self.csock.setsockopt(SOL_SOCKET,IN.SO_BINDTODEVICE,struct.pack("%ds" % (len(self.IFACE)+1,), self.IFACE)) 125 | 126 | try: 127 | self.ssock.bind(('',self.port)) 128 | except Exception, e: 129 | print "WARNING: Failed to bind %s:%d: %s" , (self.ip,self.port,e) 130 | try: 131 | self.ssock.setsockopt(IPPROTO_IP,IP_ADD_MEMBERSHIP,self.mreq) 132 | except Exception, e: 133 | print 'WARNING: Failed to join multicast group:',e 134 | except Exception, e: 135 | print "Failed to initialize UPNP sockets:",e 136 | return False 137 | return True 138 | 139 | #Clean up file/socket descriptors 140 | def cleanup(self): 141 | if self.LOG_FILE != False: 142 | self.LOG_FILE.close() 143 | self.csock.close() 144 | self.ssock.close() 145 | 146 | #Send network data 147 | def send(self,data,socket): 148 | #By default, use the client socket that's part of this class 149 | if socket == False: 150 | socket = self.csock 151 | try: 152 | socket.sendto(data,(self.ip,self.port)) 153 | return True 154 | except Exception, e: 155 | print "SendTo method failed for %s:%d : %s" % (self.ip,self.port,e) 156 | return False 157 | 158 | #Listen for network data 159 | def listen(self,size,socket): 160 | if socket == False: 161 | socket = self.ssock 162 | 163 | try: 164 | return socket.recv(size) 165 | except: 166 | return False 167 | 168 | #Create new UDP socket on ip, bound to port 169 | def createNewListener(self,ip,port): 170 | try: 171 | newsock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) 172 | newsock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 173 | newsock.bind((ip,port)) 174 | return newsock 175 | except: 176 | return False 177 | 178 | #Return the class's primary server socket 179 | def listener(self): 180 | return self.ssock 181 | 182 | #Return the class's primary client socket 183 | def sender(self): 184 | return self.csock 185 | 186 | #Parse a URL, return the host and the page 187 | def parseURL(self,url): 188 | delim = '://' 189 | host = False 190 | page = False 191 | 192 | #Split the host and page 193 | try: 194 | (host,page) = url.split(delim)[1].split('/',1) 195 | page = f'/{page}' 196 | except: 197 | #If '://' is not in the url, then it's not a full URL, so assume that it's just a relative path 198 | page = url 199 | 200 | return (host,page) 201 | 202 | #Pull the name of the device type from a device type string 203 | #The device type string looks like: 'urn:schemas-upnp-org:device:WANDevice:1' 204 | def parseDeviceTypeName(self,string): 205 | delim1 = 'device:' 206 | if delim1 in string and not string.endswith(delim1): 207 | delim2 = ':' 208 | 209 | return string.split(delim1)[1].split(delim2,1)[0] 210 | return False 211 | 212 | #Pull the name of the service type from a service type string 213 | #The service type string looks like: 'urn:schemas-upnp-org:service:Layer3Forwarding:1' 214 | def parseServiceTypeName(self,string): 215 | delim1 = 'service:' 216 | if delim1 in string and not string.endswith(delim1): 217 | delim2 = ':' 218 | 219 | return string.split(delim1)[1].split(delim2,1)[0] 220 | return False 221 | 222 | #Pull the header info for the specified HTTP header - case insensitive 223 | def parseHeader(self,data,header): 224 | delimiter = "%s:" % header 225 | defaultRet = False 226 | 227 | lowerDelim = delimiter.lower() 228 | dataArray = data.split("\r\n") 229 | 230 | #Loop through each line of the headers 231 | for line in dataArray: 232 | lowerLine = line.lower() 233 | #Does this line start with the header we're looking for? 234 | if lowerLine.startswith(lowerDelim): 235 | try: 236 | return line.split(':',1)[1].strip() 237 | except: 238 | print "Failure parsing header data for %s" % header 239 | return defaultRet 240 | 241 | #Extract the contents of a single XML tag from the data 242 | def extractSingleTag(self,data,tag): 243 | startTag = f"<{tag}" 244 | endTag = f"" 245 | 246 | try: 247 | tmp = data.split(startTag)[1] 248 | index = tmp.find('>') 249 | if index != -1: 250 | index += 1 251 | return tmp[index:].split(endTag)[0].strip() 252 | except: 253 | pass 254 | return None 255 | 256 | #Parses SSDP notify and reply packets, and populates the ENUM_HOSTS dict 257 | def parseSSDPInfo(self,data,showUniq,verbose): 258 | hostFound = False 259 | foundLocation = False 260 | messageType = False 261 | xmlFile = False 262 | host = False 263 | page = False 264 | upnpType = None 265 | knownHeaders = { 266 | 'NOTIFY' : 'notification', 267 | 'HTTP/1.1 200 OK' : 'reply' 268 | } 269 | 270 | #Use the class defaults if these aren't specified 271 | if showUniq == False: 272 | showUniq = self.UNIQ 273 | if verbose == False: 274 | verbose = self.VERBOSE 275 | 276 | #Is the SSDP packet a notification, a reply, or neither? 277 | for text,messageType in knownHeaders.iteritems(): 278 | if data.upper().startswith(text): 279 | break 280 | else: 281 | messageType = False 282 | 283 | #If this is a notification or a reply message... 284 | if messageType != False: 285 | #Get the host name and location of it's main UPNP XML file 286 | xmlFile = self.parseHeader(data,"LOCATION") 287 | upnpType = self.parseHeader(data,"SERVER") 288 | (host,page) = self.parseURL(xmlFile) 289 | 290 | #Sanity check to make sure we got all the info we need 291 | if xmlFile == False or host == False or page == False: 292 | print 'ERROR parsing recieved header:' 293 | print self.STARS 294 | print data 295 | print self.STARS 296 | print '' 297 | return False 298 | 299 | #Get the protocol in use (i.e., http, https, etc) 300 | protocol = xmlFile.split('://')[0]+'://' 301 | 302 | #Check if we've seen this host before; add to the list of hosts if: 303 | # 1. This is a new host 304 | # 2. We've already seen this host, but the uniq hosts setting is disabled 305 | for hostID,hostInfo in self.ENUM_HOSTS.iteritems(): 306 | if hostInfo['name'] == host: 307 | hostFound = True 308 | if self.UNIQ: 309 | return False 310 | 311 | if (hostFound and not self.UNIQ) or not hostFound: 312 | #Get the new host's index number and create an entry in ENUM_HOSTS 313 | index = len(self.ENUM_HOSTS) 314 | self.ENUM_HOSTS[index] = { 315 | 'name' : host, 316 | 'dataComplete' : False, 317 | 'proto' : protocol, 318 | 'xmlFile' : xmlFile, 319 | 'serverType' : None, 320 | 'upnpServer' : upnpType, 321 | 'deviceList' : {} 322 | } 323 | #Be sure to update the command completer so we can tab complete through this host's data structure 324 | self.updateCmdCompleter(self.ENUM_HOSTS) 325 | 326 | #Print out some basic device info 327 | print self.STARS 328 | print "SSDP %s message from %s" % (messageType,host) 329 | 330 | if xmlFile: 331 | foundLocation = True 332 | print "XML file is located at %s" % xmlFile 333 | 334 | if upnpType: 335 | print "Device is running %s"% upnpType 336 | 337 | print self.STARS 338 | print '' 339 | 340 | #Send GET request for a UPNP XML file 341 | def getXML(self,url): 342 | 343 | headers = { 344 | 'USER-AGENT':'uPNP/'+self.UPNP_VERSION, 345 | 'CONTENT-TYPE':'text/xml; charset="utf-8"' 346 | } 347 | 348 | try: 349 | #Use urllib2 for the request, it's awesome 350 | req = urllib2.Request(url, None, headers) 351 | response = urllib2.urlopen(req) 352 | output = response.read() 353 | headers = response.info() 354 | return (headers,output) 355 | except Exception, e: 356 | print "Request for '%s' failed: %s" % (url,e) 357 | return (False,False) 358 | 359 | #Send SOAP request 360 | def sendSOAP(self,hostName,serviceType,controlURL,actionName,actionArguments): 361 | argList = '' 362 | soapResponse = '' 363 | 364 | if '://' in controlURL: 365 | urlArray = controlURL.split('/',3) 366 | if len(urlArray) < 4: 367 | controlURL = '/' 368 | else: 369 | controlURL = '/' + urlArray[3] 370 | 371 | 372 | soapRequest = 'POST %s HTTP/1.1\r\n' % controlURL 373 | 374 | #Check if a port number was specified in the host name; default is port 80 375 | if ':' in hostName: 376 | hostNameArray = hostName.split(':') 377 | host = hostNameArray[0] 378 | try: 379 | port = int(hostNameArray[1]) 380 | except: 381 | print 'Invalid port specified for host connection:',hostName[1] 382 | return False 383 | else: 384 | host = hostName 385 | port = 80 386 | 387 | #Create a string containing all of the SOAP action's arguments and values 388 | for arg,(val,dt) in actionArguments.iteritems(): 389 | argList += '<%s>%s' % (arg,val,arg) 390 | 391 | #Create the SOAP request 392 | soapBody = '\n'\ 393 | '\n'\ 394 | '\n'\ 395 | '\t\n'\ 396 | '%s\n'\ 397 | '\t\n'\ 398 | '\n'\ 399 | '' % (actionName,serviceType,argList,actionName) 400 | 401 | #Specify the headers to send with the request 402 | headers = { 403 | 'Host':hostName, 404 | 'Content-Length':len(soapBody), 405 | 'Content-Type':'text/xml', 406 | 'SOAPAction':'"%s#%s"' % (serviceType,actionName) 407 | } 408 | 409 | #Generate the final payload 410 | for head,value in headers.iteritems(): 411 | soapRequest += '%s: %s\r\n' % (head,value) 412 | soapRequest += '\r\n%s' % soapBody 413 | 414 | #Send data and go into recieve loop 415 | try: 416 | sock = socket(AF_INET,SOCK_STREAM) 417 | sock.connect((host,port)) 418 | sock.send(soapRequest) 419 | while True: 420 | data = sock.recv(self.MAX_RECV) 421 | if not data: 422 | break 423 | else: 424 | soapResponse += data 425 | if self.soapEnd.search(soapResponse.lower()) != None: 426 | break 427 | sock.close() 428 | 429 | (header,body) = soapResponse.split('\r\n\r\n',1) 430 | if not header.upper().startswith('HTTP/1.1 200'): 431 | print 'SOAP request failed with error code:',header.split('\r\n')[0].split(' ',1)[1] 432 | errorMsg = self.extractSingleTag(body,'errorDescription') 433 | if errorMsg: 434 | print 'SOAP error message:',errorMsg 435 | return False 436 | else: 437 | return body 438 | except Exception, e: 439 | print 'Caught socket exception:',e 440 | sock.close() 441 | return False 442 | except KeyboardInterrupt: 443 | sock.close() 444 | return False 445 | 446 | 447 | #Display all info for a given host 448 | def showCompleteHostInfo(self,index,fp): 449 | na = 'N/A' 450 | serviceKeys = ['controlURL','eventSubURL','serviceId','SCPDURL','fullName'] 451 | if fp == False: 452 | fp = sys.stdout 453 | 454 | if index < 0 or index >= len(self.ENUM_HOSTS): 455 | fp.write('Specified host does not exist...\n') 456 | return 457 | try: 458 | hostInfo = self.ENUM_HOSTS[index] 459 | if hostInfo['dataComplete'] == False: 460 | print "Cannot show all host info because we don't have it all yet. Try running 'host info %d' first...\n" % index 461 | fp.write('Host name: %s\n' % hostInfo['name']) 462 | fp.write('UPNP XML File: %s\n\n' % hostInfo['xmlFile']) 463 | 464 | fp.write('\nDevice information:\n') 465 | for deviceName,deviceStruct in hostInfo['deviceList'].iteritems(): 466 | fp.write('\tDevice Name: %s\n' % deviceName) 467 | for serviceName,serviceStruct in deviceStruct['services'].iteritems(): 468 | fp.write('\t\tService Name: %s\n' % serviceName) 469 | for key in serviceKeys: 470 | fp.write('\t\t\t%s: %s\n' % (key,serviceStruct[key])) 471 | fp.write('\t\t\tServiceActions:\n') 472 | for actionName,actionStruct in serviceStruct['actions'].iteritems(): 473 | fp.write('\t\t\t\t%s\n' % actionName) 474 | for argName,argStruct in actionStruct['arguments'].iteritems(): 475 | fp.write('\t\t\t\t\t%s \n' % argName) 476 | for key,val in argStruct.iteritems(): 477 | if key == 'relatedStateVariable': 478 | fp.write('\t\t\t\t\t\t%s:\n' % val) 479 | for k,v in serviceStruct['serviceStateVariables'][val].iteritems(): 480 | fp.write('\t\t\t\t\t\t\t%s: %s\n' % (k,v)) 481 | else: 482 | fp.write('\t\t\t\t\t\t%s: %s\n' % (key,val)) 483 | 484 | except Exception, e: 485 | print 'Caught exception while showing host info:',e 486 | 487 | #Wrapper function... 488 | def getHostInfo(self,xmlData,xmlHeaders,index): 489 | if self.ENUM_HOSTS[index]['dataComplete'] == True: 490 | return 491 | 492 | if index >= 0 and index < len(self.ENUM_HOSTS): 493 | try: 494 | xmlRoot = minidom.parseString(xmlData) 495 | self.parseDeviceInfo(xmlRoot,index) 496 | self.ENUM_HOSTS[index]['serverType'] = xmlHeaders.getheader('Server') 497 | self.ENUM_HOSTS[index]['dataComplete'] = True 498 | return True 499 | except Exception, e: 500 | print 'Caught exception while getting host info:',e 501 | return False 502 | 503 | #Parse device info from the retrieved XML file 504 | def parseDeviceInfo(self,xmlRoot,index): 505 | deviceEntryPointer = False 506 | devTag = "device" 507 | deviceType = "deviceType" 508 | deviceListEntries = "deviceList" 509 | deviceTags = ["friendlyName","modelDescription","modelName","modelNumber","modelURL","presentationURL","UDN","UPC","manufacturer","manufacturerURL"] 510 | 511 | #Find all device entries listed in the XML file 512 | for device in xmlRoot.getElementsByTagName(devTag): 513 | try: 514 | #Get the deviceType string 515 | deviceTypeName = str(device.getElementsByTagName(deviceType)[0].childNodes[0].data) 516 | except: 517 | continue 518 | 519 | #Pull out the action device name from the deviceType string 520 | deviceDisplayName = self.parseDeviceTypeName(deviceTypeName) 521 | if not deviceDisplayName: 522 | continue 523 | 524 | #Create a new device entry for this host in the ENUM_HOSTS structure 525 | deviceEntryPointer = self.ENUM_HOSTS[index][deviceListEntries][deviceDisplayName] = {} 526 | deviceEntryPointer['fullName'] = deviceTypeName 527 | 528 | #Parse out all the device tags for that device 529 | for tag in deviceTags: 530 | try: 531 | deviceEntryPointer[tag] = str(device.getElementsByTagName(tag)[0].childNodes[0].data) 532 | except Exception, e: 533 | if self.VERBOSE: 534 | print 'Device',deviceEntryPointer['fullName'],'does not have a',tag 535 | continue 536 | #Get a list of all services for this device listing 537 | self.parseServiceList(device,deviceEntryPointer,index) 538 | 539 | return 540 | 541 | #Parse the list of services specified in the XML file 542 | def parseServiceList(self,xmlRoot,device,index): 543 | serviceEntryPointer = False 544 | dictName = "services" 545 | serviceListTag = "serviceList" 546 | serviceTag = "service" 547 | serviceNameTag = "serviceType" 548 | serviceTags = ["serviceId","controlURL","eventSubURL","SCPDURL"] 549 | 550 | try: 551 | device[dictName] = {} 552 | #Get a list of all services offered by this device 553 | for service in xmlRoot.getElementsByTagName(serviceListTag)[0].getElementsByTagName(serviceTag): 554 | #Get the full service descriptor 555 | serviceName = str(service.getElementsByTagName(serviceNameTag)[0].childNodes[0].data) 556 | 557 | #Get the service name from the service descriptor string 558 | serviceDisplayName = self.parseServiceTypeName(serviceName) 559 | if not serviceDisplayName: 560 | continue 561 | 562 | #Create new service entry for the device in ENUM_HOSTS 563 | serviceEntryPointer = device[dictName][serviceDisplayName] = {} 564 | serviceEntryPointer['fullName'] = serviceName 565 | 566 | #Get all of the required service info and add it to ENUM_HOSTS 567 | for tag in serviceTags: 568 | serviceEntryPointer[tag] = str(service.getElementsByTagName(tag)[0].childNodes[0].data) 569 | 570 | #Get specific service info about this service 571 | self.parseServiceInfo(serviceEntryPointer,index) 572 | except Exception, e: 573 | print 'Caught exception while parsing device service list:',e 574 | 575 | #Parse details about each service (arguements, variables, etc) 576 | def parseServiceInfo(self,service,index): 577 | argIndex = 0 578 | argTags = ['direction','relatedStateVariable'] 579 | actionList = 'actionList' 580 | actionTag = 'action' 581 | nameTag = 'name' 582 | argumentList = 'argumentList' 583 | argumentTag = 'argument' 584 | 585 | #Get the full path to the service's XML file 586 | xmlFile = self.ENUM_HOSTS[index]['proto'] + self.ENUM_HOSTS[index]['name'] 587 | if not xmlFile.endswith('/') and not service['SCPDURL'].startswith('/'): 588 | xmlFile += '/' 589 | if self.ENUM_HOSTS[index]['proto'] in service['SCPDURL']: 590 | xmlFile = service['SCPDURL'] 591 | else: 592 | xmlFile += service['SCPDURL'] 593 | service['actions'] = {} 594 | 595 | #Get the XML file that describes this service 596 | (xmlHeaders,xmlData) = self.getXML(xmlFile) 597 | if not xmlData: 598 | print 'Failed to retrieve service descriptor located at:',xmlFile 599 | return False 600 | 601 | try: 602 | xmlRoot = minidom.parseString(xmlData) 603 | 604 | #Get a list of actions for this service 605 | try: 606 | actionList = xmlRoot.getElementsByTagName(actionList)[0] 607 | except: 608 | print 'Failed to retrieve action list for service %s!' % service['fullName'] 609 | return False 610 | actions = actionList.getElementsByTagName(actionTag) 611 | if actions == []: 612 | print 'Failed to retrieve actions from service actions list for service %s!' % service['fullName'] 613 | return False 614 | 615 | #Parse all actions in the service's action list 616 | for action in actions: 617 | #Get the action's name 618 | try: 619 | actionName = str(action.getElementsByTagName(nameTag)[0].childNodes[0].data).strip() 620 | except: 621 | print 'Failed to obtain service action name (%s)!' % service['fullName'] 622 | continue 623 | 624 | #Add the action to the ENUM_HOSTS dictonary 625 | service['actions'][actionName] = {} 626 | service['actions'][actionName]['arguments'] = {} 627 | 628 | #Parse all of the action's arguments 629 | try: 630 | argList = action.getElementsByTagName(argumentList)[0] 631 | except: 632 | #Some actions may take no arguments, so continue without raising an error here... 633 | continue 634 | 635 | #Get all the arguments in this action's argument list 636 | arguments = argList.getElementsByTagName(argumentTag) 637 | if arguments == []: 638 | if self.VERBOSE: 639 | print 'Action',actionName,'has no arguments!' 640 | continue 641 | 642 | #Loop through the action's arguments, appending them to the ENUM_HOSTS dictionary 643 | for argument in arguments: 644 | try: 645 | argName = str(argument.getElementsByTagName(nameTag)[0].childNodes[0].data) 646 | except: 647 | print 'Failed to get argument name for',actionName 648 | continue 649 | service['actions'][actionName]['arguments'][argName] = {} 650 | 651 | #Get each required argument tag value and add them to ENUM_HOSTS 652 | for tag in argTags: 653 | try: 654 | service['actions'][actionName]['arguments'][argName][tag] = str(argument.getElementsByTagName(tag)[0].childNodes[0].data) 655 | except: 656 | print 'Failed to find tag %s for argument %s!' % (tag,argName) 657 | continue 658 | 659 | #Parse all of the state variables for this service 660 | self.parseServiceStateVars(xmlRoot,service) 661 | 662 | except Exception, e: 663 | print 'Caught exception while parsing Service info for service %s: %s' % (service['fullName'],str(e)) 664 | return False 665 | 666 | return True 667 | 668 | #Get info about a service's state variables 669 | def parseServiceStateVars(self,xmlRoot,servicePointer): 670 | 671 | na = 'N/A' 672 | varVals = ['sendEvents','dataType','defaultValue','allowedValues'] 673 | serviceStateTable = 'serviceStateTable' 674 | stateVariable = 'stateVariable' 675 | nameTag = 'name' 676 | dataType = 'dataType' 677 | sendEvents = 'sendEvents' 678 | allowedValueList = 'allowedValueList' 679 | allowedValue = 'allowedValue' 680 | allowedValueRange = 'allowedValueRange' 681 | minimum = 'minimum' 682 | maximum = 'maximum' 683 | 684 | #Create the serviceStateVariables entry for this service in ENUM_HOSTS 685 | servicePointer['serviceStateVariables'] = {} 686 | 687 | #Get a list of all state variables associated with this service 688 | try: 689 | stateVars = xmlRoot.getElementsByTagName(serviceStateTable)[0].getElementsByTagName(stateVariable) 690 | except: 691 | #Don't necessarily want to throw an error here, as there may be no service state variables 692 | return False 693 | 694 | #Loop through all state variables 695 | for var in stateVars: 696 | for tag in varVals: 697 | #Get variable name 698 | try: 699 | varName = str(var.getElementsByTagName(nameTag)[0].childNodes[0].data) 700 | except: 701 | print 'Failed to get service state variable name for service %s!' % servicePointer['fullName'] 702 | continue 703 | 704 | servicePointer['serviceStateVariables'][varName] = {} 705 | try: 706 | servicePointer['serviceStateVariables'][varName]['dataType'] = str(var.getElementsByTagName(dataType)[0].childNodes[0].data) 707 | except: 708 | servicePointer['serviceStateVariables'][varName]['dataType'] = na 709 | try: 710 | servicePointer['serviceStateVariables'][varName]['sendEvents'] = str(var.getElementsByTagName(sendEvents)[0].childNodes[0].data) 711 | except: 712 | servicePointer['serviceStateVariables'][varName]['sendEvents'] = na 713 | 714 | servicePointer['serviceStateVariables'][varName][allowedValueList] = [] 715 | 716 | #Get a list of allowed values for this variable 717 | try: 718 | vals = var.getElementsByTagName(allowedValueList)[0].getElementsByTagName(allowedValue) 719 | except: 720 | pass 721 | else: 722 | #Add the list of allowed values to the ENUM_HOSTS dictionary 723 | for val in vals: 724 | servicePointer['serviceStateVariables'][varName][allowedValueList].append(str(val.childNodes[0].data)) 725 | 726 | #Get allowed value range for this variable 727 | try: 728 | valList = var.getElementsByTagName(allowedValueRange)[0] 729 | except: 730 | pass 731 | else: 732 | #Add the max and min values to the ENUM_HOSTS dictionary 733 | servicePointer['serviceStateVariables'][varName][allowedValueRange] = [] 734 | try: 735 | servicePointer['serviceStateVariables'][varName][allowedValueRange].append(str(valList.getElementsByTagName(minimum)[0].childNodes[0].data)) 736 | servicePointer['serviceStateVariables'][varName][allowedValueRange].append(str(valList.getElementsByTagName(maximum)[0].childNodes[0].data)) 737 | except: 738 | pass 739 | return True 740 | 741 | #Update the command completer 742 | def updateCmdCompleter(self,struct): 743 | indexOnlyList = { 744 | 'host' : ['get','details','summary'], 745 | 'save' : ['info'] 746 | } 747 | hostCommand = 'host' 748 | subCommandList = ['info'] 749 | sendCommand = 'send' 750 | 751 | try: 752 | structPtr = {} 753 | topLevelKeys = {} 754 | for key,val in struct.iteritems(): 755 | structPtr[str(key)] = val 756 | topLevelKeys[str(key)] = None 757 | 758 | #Update the subCommandList 759 | for subcmd in subCommandList: 760 | self.completer.commands[hostCommand][subcmd] = None 761 | self.completer.commands[hostCommand][subcmd] = structPtr 762 | 763 | #Update the indexOnlyList 764 | for cmd,data in indexOnlyList.iteritems(): 765 | for subcmd in data: 766 | self.completer.commands[cmd][subcmd] = topLevelKeys 767 | 768 | #This is for updating the sendCommand key 769 | structPtr = {} 770 | for hostIndex,hostData in struct.iteritems(): 771 | host = str(hostIndex) 772 | structPtr[host] = {} 773 | if hostData.has_key('deviceList'): 774 | for device,deviceData in hostData['deviceList'].iteritems(): 775 | structPtr[host][device] = {} 776 | if deviceData.has_key('services'): 777 | for service,serviceData in deviceData['services'].iteritems(): 778 | structPtr[host][device][service] = {} 779 | if serviceData.has_key('actions'): 780 | for action,actionData in serviceData['actions'].iteritems(): 781 | structPtr[host][device][service][action] = None 782 | self.completer.commands[hostCommand][sendCommand] = structPtr 783 | except Exception,e: 784 | print "Error updating command completer structure; some command completion features might not work..." 785 | return 786 | 787 | 788 | 789 | 790 | ################## Action Functions ###################### 791 | #These functions handle user commands from the shell 792 | 793 | #Actively search for UPNP devices 794 | def msearch(argc,argv,hp): 795 | defaultST = "upnp:rootdevice" 796 | st = "schemas-upnp-org" 797 | myip = '' 798 | lport = hp.port 799 | 800 | if argc >= 3: 801 | if argc == 4: 802 | st = argv[1] 803 | searchType = argv[2] 804 | searchName = argv[3] 805 | else: 806 | searchType = argv[1] 807 | searchName = argv[2] 808 | st = "urn:%s:%s:%s:%s" % (st,searchType,searchName,hp.UPNP_VERSION.split('.')[0]) 809 | else: 810 | st = defaultST 811 | 812 | #Build the request 813 | request = "M-SEARCH * HTTP/1.1\r\n"\ 814 | "HOST:%s:%d\r\n"\ 815 | "ST:%s\r\n" % (hp.ip,hp.port,st) 816 | for header,value in hp.msearchHeaders.iteritems(): 817 | request += header + ':' + value + "\r\n" 818 | request += "\r\n" 819 | 820 | print "Entering discovery mode for '%s', Ctl+C to stop..." % st 821 | print '' 822 | 823 | #Have to create a new socket since replies will be sent directly to our IP, not the multicast IP 824 | server = hp.createNewListener(myip,lport) 825 | if server == False: 826 | print 'Failed to bind port %d' % lport 827 | return 828 | 829 | hp.send(request,server) 830 | while True: 831 | try: 832 | hp.parseSSDPInfo(hp.listen(1024,server),False,False) 833 | except Exception, e: 834 | print 'Discover mode halted...' 835 | break 836 | 837 | #Passively listen for UPNP NOTIFY packets 838 | def pcap(argc,argv,hp): 839 | print 'Entering passive mode, Ctl+C to stop...' 840 | print '' 841 | while True: 842 | try: 843 | hp.parseSSDPInfo(hp.listen(1024,False),False,False) 844 | except Exception, e: 845 | print "Passive mode halted..." 846 | break 847 | 848 | #Manipulate M-SEARCH header values 849 | def head(argc,argv,hp): 850 | if argc >= 2: 851 | action = argv[1] 852 | #Show current headers 853 | if action == 'show': 854 | for header,value in hp.msearchHeaders.iteritems(): 855 | print header,':',value 856 | return 857 | #Delete the specified header 858 | elif action == 'del': 859 | if argc == 3: 860 | header = argv[2] 861 | if hp.msearchHeaders.has_key(header): 862 | del hp.msearchHeaders[header] 863 | print '%s removed from header list' % header 864 | return 865 | else: 866 | print '%s is not in the current header list' % header 867 | return 868 | #Create/set a headers 869 | elif action == 'set': 870 | if argc == 4: 871 | header = argv[2] 872 | value = argv[3] 873 | hp.msearchHeaders[header] = value 874 | print "Added header: '%s:%s" % (header,value) 875 | return 876 | 877 | showHelp(argv[0]) 878 | 879 | #Manipulate application settings 880 | def seti(argc,argv,hp): 881 | if argc >= 2: 882 | action = argv[1] 883 | if action == 'uniq': 884 | hp.UNIQ = toggleVal(hp.UNIQ) 885 | print "Show unique hosts set to: %s" % hp.UNIQ 886 | return 887 | elif action == 'debug': 888 | hp.DEBUG = toggleVal(hp.DEBUG) 889 | print "Debug mode set to: %s" % hp.DEBUG 890 | return 891 | elif action == 'verbose': 892 | hp.VERBOSE = toggleVal(hp.VERBOSE) 893 | print "Verbose mode set to: %s" % hp.VERBOSE 894 | return 895 | elif action == 'version': 896 | if argc == 3: 897 | hp.UPNP_VERSION = argv[2] 898 | print 'UPNP version set to: %s' % hp.UPNP_VERSION 899 | else: 900 | showHelp(argv[0]) 901 | return 902 | elif action == 'iface': 903 | if argc == 3: 904 | hp.IFACE = argv[2] 905 | print 'Interface set to %s, re-binding sockets...' % hp.IFACE 906 | if hp.initSockets(hp.ip,hp.port,hp.IFACE): 907 | print 'Interface change successful!' 908 | else: 909 | print 'Failed to bind new interface - are you sure you have root privilages??' 910 | hp.IFACE = None 911 | return 912 | elif action == 'socket': 913 | if argc == 3: 914 | try: 915 | (ip,port) = argv[2].split(':') 916 | port = int(port) 917 | hp.ip = ip 918 | hp.port = port 919 | hp.cleanup() 920 | if hp.initSockets(ip,port,hp.IFACE) == False: 921 | print "Setting new socket %s:%d failed!" % (ip,port) 922 | else: 923 | print "Using new socket: %s:%d" % (ip,port) 924 | except Exception, e: 925 | print 'Caught exception setting new socket:',e 926 | return 927 | elif action == 'show': 928 | print 'Multicast IP: ',hp.ip 929 | print 'Multicast Port: ',hp.port 930 | print 'Network Interface: ',hp.IFACE 931 | print 'Number of known hosts: ',len(hp.ENUM_HOSTS) 932 | print 'UPNP Version: ',hp.UPNP_VERSION 933 | print 'Debug mode: ',hp.DEBUG 934 | print 'Verbose mode: ',hp.VERBOSE 935 | print 'Show only unique hosts:',hp.UNIQ 936 | print 'Using log file: ',hp.LOG_FILE 937 | return 938 | 939 | showHelp(argv[0]) 940 | return 941 | 942 | #Host command. It's kind of big. 943 | def host(argc,argv,hp): 944 | 945 | indexList = [] 946 | indexError = "Host index out of range. Try the 'host list' command to get a list of known hosts" 947 | if argc >= 2: 948 | action = argv[1] 949 | if action == 'list': 950 | if len(hp.ENUM_HOSTS) == 0: 951 | print "No known hosts - try running the 'msearch' or 'pcap' commands" 952 | return 953 | for index,hostInfo in hp.ENUM_HOSTS.iteritems(): 954 | print "\t[%d] %s" % (index,hostInfo['name']) 955 | return 956 | elif action == 'details': 957 | hostInfo = False 958 | if argc == 3: 959 | try: 960 | index = int(argv[2]) 961 | except Exception, e: 962 | print indexError 963 | return 964 | 965 | if index < 0 or index >= len(hp.ENUM_HOSTS): 966 | print indexError 967 | return 968 | hostInfo = hp.ENUM_HOSTS[index] 969 | 970 | try: 971 | #If this host data is already complete, just display it 972 | if hostInfo['dataComplete'] == True: 973 | hp.showCompleteHostInfo(index,False) 974 | else: 975 | print "Can't show host info because I don't have it. Please run 'host get %d'" % index 976 | except KeyboardInterrupt, e: 977 | pass 978 | return 979 | 980 | elif action == 'summary': 981 | if argc == 3: 982 | 983 | try: 984 | index = int(argv[2]) 985 | hostInfo = hp.ENUM_HOSTS[index] 986 | except: 987 | print indexError 988 | return 989 | 990 | print 'Host:',hostInfo['name'] 991 | print 'XML File:',hostInfo['xmlFile'] 992 | for deviceName,deviceData in hostInfo['deviceList'].iteritems(): 993 | print deviceName 994 | for k,v in deviceData.iteritems(): 995 | try: 996 | v.has_key(False) 997 | except: 998 | print "\t%s: %s" % (k,v) 999 | print '' 1000 | return 1001 | 1002 | elif action == 'info': 1003 | output = hp.ENUM_HOSTS 1004 | dataStructs = [] 1005 | for arg in argv[2:]: 1006 | try: 1007 | arg = int(arg) 1008 | except: 1009 | pass 1010 | output = output[arg] 1011 | try: 1012 | for k,v in output.iteritems(): 1013 | try: 1014 | v.has_key(False) 1015 | dataStructs.append(k) 1016 | except: 1017 | print k,':',v 1018 | continue 1019 | except: 1020 | print output 1021 | 1022 | for struct in dataStructs: 1023 | print struct,': {}' 1024 | return 1025 | 1026 | elif action == 'get': 1027 | hostInfo = False 1028 | if argc == 3: 1029 | try: 1030 | index = int(argv[2]) 1031 | except: 1032 | print indexError 1033 | return 1034 | if index < 0 or index >= len(hp.ENUM_HOSTS): 1035 | print "Host index out of range. Try the 'host list' command to get a list of known hosts" 1036 | return 1037 | else: 1038 | hostInfo = hp.ENUM_HOSTS[index] 1039 | 1040 | #If this host data is already complete, just display it 1041 | if hostInfo['dataComplete'] == True: 1042 | print 'Data for this host has already been enumerated!' 1043 | return 1044 | 1045 | try: 1046 | #Get extended device and service information 1047 | if hostInfo != False: 1048 | print "Requesting device and service info for %s (this could take a few seconds)..." % hostInfo['name'] 1049 | print '' 1050 | if hostInfo['dataComplete'] == False: 1051 | (xmlHeaders,xmlData) = hp.getXML(hostInfo['xmlFile']) 1052 | if xmlData == False: 1053 | print 'Failed to request host XML file:',hostInfo['xmlFile'] 1054 | return 1055 | if hp.getHostInfo(xmlData,xmlHeaders,index) == False: 1056 | print "Failed to get device/service info for %s..." % hostInfo['name'] 1057 | return 1058 | print 'Host data enumeration complete!' 1059 | hp.updateCmdCompleter(hp.ENUM_HOSTS) 1060 | return 1061 | except KeyboardInterrupt, e: 1062 | return 1063 | 1064 | elif action == 'send': 1065 | #Send SOAP requests 1066 | index = False 1067 | inArgCounter = 0 1068 | 1069 | if argc != 6: 1070 | showHelp(argv[0]) 1071 | return 1072 | else: 1073 | try: 1074 | index = int(argv[2]) 1075 | except: 1076 | print indexError 1077 | return 1078 | deviceName = argv[3] 1079 | serviceName = argv[4] 1080 | actionName = argv[5] 1081 | hostInfo = hp.ENUM_HOSTS[index] 1082 | actionArgs = False 1083 | sendArgs = {} 1084 | retTags = [] 1085 | controlURL = False 1086 | fullServiceName = False 1087 | 1088 | #Get the service control URL and full service name 1089 | try: 1090 | controlURL = hostInfo['proto'] + hostInfo['name'] 1091 | controlURL2 = hostInfo['deviceList'][deviceName]['services'][serviceName]['controlURL'] 1092 | if not controlURL.endswith('/') and not controlURL2.startswith('/'): 1093 | controlURL += '/' 1094 | controlURL += controlURL2 1095 | except Exception,e: 1096 | print 'Caught exception:',e 1097 | print "Are you sure you've run 'host get %d' and specified the correct service name?" % index 1098 | return False 1099 | 1100 | #Get action info 1101 | try: 1102 | actionArgs = hostInfo['deviceList'][deviceName]['services'][serviceName]['actions'][actionName]['arguments'] 1103 | fullServiceName = hostInfo['deviceList'][deviceName]['services'][serviceName]['fullName'] 1104 | except Exception,e: 1105 | print 'Caught exception:',e 1106 | print "Are you sure you've specified the correct action?" 1107 | return False 1108 | 1109 | for argName,argVals in actionArgs.iteritems(): 1110 | actionStateVar = argVals['relatedStateVariable'] 1111 | stateVar = hostInfo['deviceList'][deviceName]['services'][serviceName]['serviceStateVariables'][actionStateVar] 1112 | 1113 | if argVals['direction'].lower() == 'in': 1114 | print "Required argument:" 1115 | print "\tArgument Name: ",argName 1116 | print "\tData Type: ",stateVar['dataType'] 1117 | if stateVar.has_key('allowedValueList'): 1118 | print "\tAllowed Values:",stateVar['allowedValueList'] 1119 | if stateVar.has_key('allowedValueRange'): 1120 | print "\tValue Min: ",stateVar['allowedValueRange'][0] 1121 | print "\tValue Max: ",stateVar['allowedValueRange'][1] 1122 | if stateVar.has_key('defaultValue'): 1123 | print "\tDefault Value: ",stateVar['defaultValue'] 1124 | prompt = "\tSet %s value to: " % argName 1125 | try: 1126 | #Get user input for the argument value 1127 | (argc,argv) = getUserInput(hp,prompt) 1128 | if argv == None: 1129 | print 'Stopping send request...' 1130 | return 1131 | uInput = '' 1132 | 1133 | if argc > 0: 1134 | inArgCounter += 1 1135 | 1136 | for val in argv: 1137 | uInput += val + ' ' 1138 | 1139 | uInput = uInput.strip() 1140 | if stateVar['dataType'] == 'bin.base64' and uInput: 1141 | uInput = base64.encodestring(uInput) 1142 | 1143 | sendArgs[argName] = (uInput.strip(),stateVar['dataType']) 1144 | except KeyboardInterrupt: 1145 | return 1146 | print '' 1147 | else: 1148 | retTags.append((argName,stateVar['dataType'])) 1149 | 1150 | #Remove the above inputs from the command history 1151 | while inArgCounter: 1152 | readline.remove_history_item(readline.get_current_history_length()-1) 1153 | inArgCounter -= 1 1154 | 1155 | #print 'Requesting',controlURL 1156 | soapResponse = hp.sendSOAP(hostInfo['name'],fullServiceName,controlURL,actionName,sendArgs) 1157 | if soapResponse != False: 1158 | #It's easier to just parse this ourselves... 1159 | for (tag,dataType) in retTags: 1160 | tagValue = hp.extractSingleTag(soapResponse,tag) 1161 | if dataType == 'bin.base64' and tagValue != None: 1162 | tagValue = base64.decodestring(tagValue) 1163 | print tag,':',tagValue 1164 | return 1165 | 1166 | 1167 | showHelp(argv[0]) 1168 | return 1169 | 1170 | #Save data 1171 | def save(argc,argv,hp): 1172 | suffix = '%s_%s.mir' 1173 | uniqName = '' 1174 | saveType = '' 1175 | fnameIndex = 3 1176 | 1177 | if argc >= 2: 1178 | if argv[1] == 'help': 1179 | showHelp(argv[0]) 1180 | return 1181 | elif argv[1] == 'data': 1182 | saveType = 'struct' 1183 | if argc == 3: 1184 | index = argv[2] 1185 | else: 1186 | index = 'data' 1187 | elif argv[1] == 'info': 1188 | saveType = 'info' 1189 | fnameIndex = 4 1190 | if argc >= 3: 1191 | try: 1192 | index = int(argv[2]) 1193 | except Exception, e: 1194 | print 'Host index is not a number!' 1195 | showHelp(argv[0]) 1196 | return 1197 | else: 1198 | showHelp(argv[0]) 1199 | return 1200 | 1201 | if argc == fnameIndex: 1202 | uniqName = argv[fnameIndex-1] 1203 | else: 1204 | uniqName = index 1205 | else: 1206 | showHelp(argv[0]) 1207 | return 1208 | 1209 | fileName = suffix % (saveType,uniqName) 1210 | if os.path.exists(fileName): 1211 | print "File '%s' already exists! Please try again..." % fileName 1212 | return 1213 | if saveType == 'struct': 1214 | try: 1215 | fp = open(fileName,'w') 1216 | pickle.dump(hp.ENUM_HOSTS,fp) 1217 | fp.close() 1218 | print "Host data saved to '%s'" % fileName 1219 | except Exception, e: 1220 | print 'Caught exception saving host data:',e 1221 | elif saveType == 'info': 1222 | try: 1223 | fp = open(fileName,'w') 1224 | hp.showCompleteHostInfo(index,fp) 1225 | fp.close() 1226 | print "Host info for '%s' saved to '%s'" % (hp.ENUM_HOSTS[index]['name'],fileName) 1227 | except Exception, e: 1228 | print 'Failed to save host info:',e 1229 | return 1230 | else: 1231 | showHelp(argv[0]) 1232 | 1233 | return 1234 | 1235 | #Load data 1236 | def load(argc,argv,hp): 1237 | if argc == 2 and argv[1] != 'help': 1238 | loadFile = argv[1] 1239 | 1240 | try: 1241 | fp = open(loadFile,'r') 1242 | hp.ENUM_HOSTS = {} 1243 | hp.ENUM_HOSTS = pickle.load(fp) 1244 | fp.close() 1245 | hp.updateCmdCompleter(hp.ENUM_HOSTS) 1246 | print 'Host data restored:' 1247 | print '' 1248 | host(2,['host','list'],hp) 1249 | return 1250 | except Exception, e: 1251 | print 'Caught exception while restoring host data:',e 1252 | 1253 | showHelp(argv[0]) 1254 | 1255 | #Open log file 1256 | def log(argc,argv,hp): 1257 | if argc == 2: 1258 | logFile = argv[1] 1259 | try: 1260 | fp = open(logFile,'a') 1261 | except Exception, e: 1262 | print 'Failed to open %s for logging: %s' % (logFile,e) 1263 | return 1264 | try: 1265 | hp.LOG_FILE = fp 1266 | ts = [] 1267 | for x in time.localtime(): 1268 | ts.append(x) 1269 | theTime = "%d-%d-%d, %d:%d:%d" % (ts[0],ts[1],ts[2],ts[3],ts[4],ts[5]) 1270 | hp.LOG_FILE.write("\n### Logging started at: %s ###\n" % theTime) 1271 | except Exception, e: 1272 | print "Cannot write to file '%s': %s" % (logFile,e) 1273 | hp.LOG_FILE = False 1274 | return 1275 | print "Commands will be logged to: '%s'" % logFile 1276 | return 1277 | showHelp(argv[0]) 1278 | 1279 | #Show help 1280 | def help(argc,argv,hp): 1281 | showHelp(False) 1282 | 1283 | #Debug, disabled by default 1284 | def debug(argc,argv,hp): 1285 | command = '' 1286 | if hp.DEBUG == False: 1287 | print 'Debug is disabled! To enable, try the seti command...' 1288 | return 1289 | if argc == 1: 1290 | showHelp(argv[0]) 1291 | else: 1292 | for cmd in argv[1:]: 1293 | command += cmd + ' ' 1294 | command = command.strip() 1295 | print eval(command) 1296 | return 1297 | #Quit! 1298 | def exit(argc,argv,hp): 1299 | quit(argc,argv,hp) 1300 | 1301 | #Quit! 1302 | def quit(argc,argv,hp): 1303 | if argc == 2 and argv[1] == 'help': 1304 | showHelp(argv[0]) 1305 | return 1306 | print 'Bye!' 1307 | print '' 1308 | hp.cleanup() 1309 | sys.exit(0) 1310 | 1311 | ################ End Action Functions ###################### 1312 | 1313 | #Show command help 1314 | def showHelp(command): 1315 | #Detailed help info for each command 1316 | helpInfo = { 1317 | 'help' : { 1318 | 'longListing': 1319 | 'Description:\n'\ 1320 | '\tLists available commands and command descriptions\n\n'\ 1321 | 'Usage:\n'\ 1322 | '\t%s\n'\ 1323 | '\t help', 1324 | 'quickView': 1325 | 'Show program help' 1326 | }, 1327 | 'quit' : { 1328 | 'longListing' : 1329 | 'Description:\n'\ 1330 | '\tQuits the interactive shell\n\n'\ 1331 | 'Usage:\n'\ 1332 | '\t%s', 1333 | 'quickView' : 1334 | 'Exit this shell' 1335 | }, 1336 | 'exit' : { 1337 | 1338 | 'longListing' : 1339 | 'Description:\n'\ 1340 | '\tExits the interactive shell\n\n'\ 1341 | 'Usage:\n'\ 1342 | '\t%s', 1343 | 'quickView' : 1344 | 'Exit this shell' 1345 | }, 1346 | 'save' : { 1347 | 'longListing' : 1348 | 'Description:\n'\ 1349 | '\tSaves current host information to disk.\n\n'\ 1350 | 'Usage:\n'\ 1351 | '\t%s > [file prefix]\n'\ 1352 | "\tSpecifying 'data' will save the raw host data to a file suitable for importing later via 'load'\n"\ 1353 | "\tSpecifying 'info' will save data for the specified host in a human-readable format\n"\ 1354 | "\tSpecifying a file prefix will save files in for format of 'struct_[prefix].mir' and info_[prefix].mir\n\n"\ 1355 | 'Example:\n'\ 1356 | '\t> save data wrt54g\n'\ 1357 | '\t> save info 0 wrt54g\n\n'\ 1358 | 'Notes:\n'\ 1359 | "\to Data files are saved as 'struct_[prefix].mir'; info files are saved as 'info_[prefix].mir.'\n"\ 1360 | "\to If no prefix is specified, the host index number will be used for the prefix.\n"\ 1361 | "\to The data saved by the 'save info' command is the same as the output of the 'host details' command.", 1362 | 'quickView' : 1363 | 'Save current host data to file' 1364 | }, 1365 | 'seti' : { 1366 | 'longListing' : 1367 | 'Description:\n'\ 1368 | '\tAllows you to view and edit application settings.\n\n'\ 1369 | 'Usage:\n'\ 1370 | '\t%s | iface | socket >\n'\ 1371 | "\t'show' displays the current program settings\n"\ 1372 | "\t'uniq' toggles the show-only-uniq-hosts setting when discovering UPNP devices\n"\ 1373 | "\t'debug' toggles debug mode\n"\ 1374 | "\t'verbose' toggles verbose mode\n"\ 1375 | "\t'version' changes the UPNP version used\n"\ 1376 | "\t'iface' changes the network interface in use\n"\ 1377 | "\t'socket' re-sets the multicast IP address and port number used for UPNP discovery\n\n"\ 1378 | 'Example:\n'\ 1379 | '\t> seti socket 239.255.255.250:1900\n'\ 1380 | '\t> seti uniq\n\n'\ 1381 | 'Notes:\n'\ 1382 | "\tIf given no options, 'seti' will display the current application settings", 1383 | 'quickView' : 1384 | 'Show/define application settings' 1385 | }, 1386 | 'head' : { 1387 | 'longListing' : 1388 | 'Description:\n'\ 1389 | '\tAllows you to view, set, add and delete the SSDP header values used in SSDP transactions\n\n'\ 1390 | 'Usage:\n'\ 1391 | '\t%s | set
>\n'\ 1392 | "\t'set' allows you to set SSDP headers used when sending M-SEARCH queries with the 'msearch' command\n"\ 1393 | "\t'del' deletes a current header from the list\n"\ 1394 | "\t'show' displays all current header info\n\n"\ 1395 | 'Example:\n'\ 1396 | '\t> head show\n'\ 1397 | '\t> head set MX 3', 1398 | 'quickView' : 1399 | 'Show/define SSDP headers' 1400 | }, 1401 | 'host' : { 1402 | 'longListing' : 1403 | 'Description:\n'\ 1404 | "\tAllows you to query host information and iteract with a host's actions/services.\n\n"\ 1405 | 'Usage:\n'\ 1406 | '\t%s [host index #]\n'\ 1407 | "\t'list' displays an index of all known UPNP hosts along with their respective index numbers\n"\ 1408 | "\t'get' gets detailed information about the specified host\n"\ 1409 | "\t'details' gets and displays detailed information about the specified host\n"\ 1410 | "\t'summary' displays a short summary describing the specified host\n"\ 1411 | "\t'info' allows you to enumerate all elements of the hosts object\n"\ 1412 | "\t'send' allows you to send SOAP requests to devices and services *\n\n"\ 1413 | 'Example:\n'\ 1414 | '\t> host list\n'\ 1415 | '\t> host get 0\n'\ 1416 | '\t> host summary 0\n'\ 1417 | '\t> host info 0 deviceList\n'\ 1418 | '\t> host send 0 \n\n'\ 1419 | 'Notes:\n'\ 1420 | "\to All host commands support full tab completion of enumerated arguments\n"\ 1421 | "\to All host commands EXCEPT for the 'host send', 'host info' and 'host list' commands take only one argument: the host index number.\n"\ 1422 | "\to The host index number can be obtained by running 'host list', which takes no futher arguments.\n"\ 1423 | "\to The 'host send' command requires that you also specify the host's device name, service name, and action name that you wish to send,\n\t in that order (see the last example in the Example section of this output). This information can be obtained by viewing the\n\t 'host details' listing, or by querying the host information via the 'host info' command.\n"\ 1424 | "\to The 'host info' command allows you to selectively enumerate the host information data structure. All data elements and their\n\t corresponding values are displayed; a value of '{}' indicates that the element is a sub-structure that can be further enumerated\n\t (see the 'host info' example in the Example section of this output).", 1425 | 'quickView' : 1426 | 'View and send host list and host information' 1427 | }, 1428 | 'pcap' : { 1429 | 'longListing' : 1430 | 'Description:\n'\ 1431 | '\tPassively listens for SSDP NOTIFY messages from UPNP devices\n\n'\ 1432 | 'Usage:\n'\ 1433 | '\t%s', 1434 | 'quickView' : 1435 | 'Passively listen for UPNP hosts' 1436 | }, 1437 | 'msearch' : { 1438 | 'longListing' : 1439 | 'Description:\n'\ 1440 | '\tActively searches for UPNP hosts using M-SEARCH queries\n\n'\ 1441 | 'Usage:\n'\ 1442 | "\t%s [device | service] [ | ]\n"\ 1443 | "\tIf no arguments are specified, 'msearch' searches for upnp:rootdevices\n"\ 1444 | "\tSpecific device/services types can be searched for using the 'device' or 'service' arguments\n\n"\ 1445 | 'Example:\n'\ 1446 | '\t> msearch\n'\ 1447 | '\t> msearch service WANIPConnection\n'\ 1448 | '\t> msearch device InternetGatewayDevice', 1449 | 'quickView' : 1450 | 'Actively locate UPNP hosts' 1451 | }, 1452 | 'load' : { 1453 | 'longListing' : 1454 | 'Description:\n'\ 1455 | "\tLoads host data from a struct file previously saved with the 'save data' command\n\n"\ 1456 | 'Usage:\n'\ 1457 | '\t%s ', 1458 | 'quickView' : 1459 | 'Restore previous host data from file' 1460 | }, 1461 | 'log' : { 1462 | 'longListing' : 1463 | 'Description:\n'\ 1464 | '\tLogs user-supplied commands to a log file\n\n'\ 1465 | 'Usage:\n'\ 1466 | '\t%s ', 1467 | 'quickView' : 1468 | 'Logs user-supplied commands to a log file' 1469 | } 1470 | } 1471 | 1472 | 1473 | try: 1474 | print helpInfo[command]['longListing'] % command 1475 | except: 1476 | for command,cmdHelp in helpInfo.iteritems(): 1477 | print "%s\t\t%s" % (command,cmdHelp['quickView']) 1478 | 1479 | #Display usage 1480 | def usage(): 1481 | print ''' 1482 | Command line usage: %s [OPTIONS] 1483 | 1484 | -s Load previous host data from struct file 1485 | -l Log user-supplied commands to log file 1486 | -i Specify the name of the interface to use (Linux only, requires root) 1487 | -u Disable show-uniq-hosts-only option 1488 | -d Enable debug mode 1489 | -v Enable verbose mode 1490 | -h Show help 1491 | ''' % sys.argv[0] 1492 | sys.exit(1) 1493 | 1494 | #Check command line options 1495 | def parseCliOpts(argc,argv,hp): 1496 | try: 1497 | opts,args = getopt.getopt(argv[1:],'s:l:i:udvh') 1498 | except getopt.GetoptError, e: 1499 | print 'Usage Error:',e 1500 | usage() 1501 | else: 1502 | for (opt,arg) in opts: 1503 | if opt == '-s': 1504 | print '' 1505 | load(2,['load',arg],hp) 1506 | print '' 1507 | elif opt == '-l': 1508 | print '' 1509 | log(2,['log',arg],hp) 1510 | print '' 1511 | elif opt == '-u': 1512 | hp.UNIQ = toggleVal(hp.UNIQ) 1513 | elif opt == '-d': 1514 | hp.DEBUG = toggleVal(hp.DEBUG) 1515 | print 'Debug mode enabled!' 1516 | elif opt == '-v': 1517 | hp.VERBOSE = toggleVal(hp.VERBOSE) 1518 | print 'Verbose mode enabled!' 1519 | elif opt == '-h': 1520 | usage() 1521 | elif opt == '-i': 1522 | networkInterfaces = [] 1523 | requestedInterface = arg 1524 | interfaceName = None 1525 | found = False 1526 | 1527 | #Get a list of network interfaces. This only works on unix boxes. 1528 | try: 1529 | if thisSystem() != 'Windows': 1530 | fp = open('/proc/net/dev','r') 1531 | for line in fp.readlines(): 1532 | if ':' in line: 1533 | interfaceName = line.split(':')[0].strip() 1534 | if interfaceName == requestedInterface: 1535 | found = True 1536 | break 1537 | else: 1538 | networkInterfaces.append(line.split(':')[0].strip()) 1539 | fp.close() 1540 | else: 1541 | networkInterfaces.append('Run ipconfig to get a list of available network interfaces!') 1542 | except Exception,e: 1543 | print 'Error opening file:',e 1544 | print "If you aren't running Linux, this file may not exist!" 1545 | 1546 | if not found and len(networkInterfaces) > 0: 1547 | print "Failed to find interface '%s'; try one of these:\n" % requestedInterface 1548 | for iface in networkInterfaces: 1549 | print iface 1550 | print '' 1551 | sys.exit(1) 1552 | else: 1553 | if not hp.initSockets(False,False,interfaceName): 1554 | print 'Binding to interface %s failed; are you sure you have root privilages??' % interfaceName 1555 | 1556 | #Toggle boolean values 1557 | def toggleVal(val): 1558 | return not val 1559 | 1560 | #Prompt for user input 1561 | def getUserInput(hp,shellPrompt): 1562 | defaultShellPrompt = 'upnp> ' 1563 | if shellPrompt == False: 1564 | shellPrompt = defaultShellPrompt 1565 | 1566 | try: 1567 | uInput = raw_input(shellPrompt).strip() 1568 | argv = uInput.split() 1569 | argc = len(argv) 1570 | except KeyboardInterrupt, e: 1571 | print '\n' 1572 | if shellPrompt == defaultShellPrompt: 1573 | quit(0,[],hp) 1574 | return (0,None) 1575 | if hp.LOG_FILE != False: 1576 | try: 1577 | hp.LOG_FILE.write("%s\n" % uInput) 1578 | except: 1579 | print 'Failed to log data to log file!' 1580 | 1581 | return (argc,argv) 1582 | 1583 | #Main 1584 | def main(argc,argv): 1585 | #Table of valid commands - all primary commands must have an associated function 1586 | appCommands = { 1587 | 'help' : { 1588 | 'help' : None 1589 | }, 1590 | 'quit' : { 1591 | 'help' : None 1592 | }, 1593 | 'exit' : { 1594 | 'help' : None 1595 | }, 1596 | 'save' : { 1597 | 'data' : None, 1598 | 'info' : None, 1599 | 'help' : None 1600 | }, 1601 | 'load' : { 1602 | 'help' : None 1603 | }, 1604 | 'seti' : { 1605 | 'uniq' : None, 1606 | 'socket' : None, 1607 | 'show' : None, 1608 | 'iface' : None, 1609 | 'debug' : None, 1610 | 'version' : None, 1611 | 'verbose' : None, 1612 | 'help' : None 1613 | }, 1614 | 'head' : { 1615 | 'set' : None, 1616 | 'show' : None, 1617 | 'del' : None, 1618 | 'help': None 1619 | }, 1620 | 'host' : { 1621 | 'list' : None, 1622 | 'info' : None, 1623 | 'get' : None, 1624 | 'details' : None, 1625 | 'send' : None, 1626 | 'summary' : None, 1627 | 'help' : None 1628 | }, 1629 | 'pcap' : { 1630 | 'help' : None 1631 | }, 1632 | 'msearch' : { 1633 | 'device' : None, 1634 | 'service' : None, 1635 | 'help' : None 1636 | }, 1637 | 'log' : { 1638 | 'help' : None 1639 | }, 1640 | 'debug': { 1641 | 'command' : None, 1642 | 'help' : None 1643 | } 1644 | } 1645 | 1646 | #The load command should auto complete on the contents of the current directory 1647 | for file in os.listdir(os.getcwd()): 1648 | appCommands['load'][file] = None 1649 | 1650 | #Initialize upnp class 1651 | hp = upnp(False,False,None,appCommands); 1652 | 1653 | #Set up tab completion and command history 1654 | readline.parse_and_bind("tab: complete") 1655 | readline.set_completer(hp.completer.complete) 1656 | 1657 | #Set some default values 1658 | hp.UNIQ = True 1659 | hp.VERBOSE = False 1660 | action = False 1661 | funPtr = False 1662 | 1663 | #Check command line options 1664 | parseCliOpts(argc,argv,hp) 1665 | 1666 | #Main loop 1667 | while True: 1668 | #Drop user into shell 1669 | (argc,argv) = getUserInput(hp,False) 1670 | if argc == 0: 1671 | continue 1672 | action = argv[0] 1673 | funcPtr = False 1674 | 1675 | print '' 1676 | #Parse actions 1677 | try: 1678 | if appCommands.has_key(action): 1679 | funcPtr = eval(action) 1680 | except: 1681 | funcPtr = False 1682 | action = False 1683 | 1684 | if callable(funcPtr): 1685 | if argc == 2 and argv[1] == 'help': 1686 | showHelp(argv[0]) 1687 | else: 1688 | try: 1689 | funcPtr(argc,argv,hp) 1690 | except KeyboardInterrupt: 1691 | print 'Action interrupted by user...' 1692 | print '' 1693 | continue 1694 | print 'Invalid command. Valid commands are:' 1695 | print '' 1696 | showHelp(False) 1697 | print '' 1698 | 1699 | 1700 | if __name__ == "__main__": 1701 | try: 1702 | main(len(sys.argv),sys.argv) 1703 | except Exception, e: 1704 | print 'Caught main exception:',e 1705 | sys.exit(1) 1706 | 1707 | --------------------------------------------------------------------------------