| Status | 46 |Command | 47 |Options | 48 |
|---|---|---|
| 54 | | {{cmd.Command.RequestType}} | 55 |56 | |
Choose a command to send to one or more enrolled devices.
34 |python server.py.
161 |
162 | On the device navigate to: **https://YOUR_HOST:8080/**
163 | Once there you need to, in order:
164 | 1. Tap *here* to install the CA Cert (for Server/Identity)
165 | 2. Tap *here* to enroll in MDM (the device should appear after this step)
166 | 3. Select Command (DeviceLock is a good one to test) and check your device. Click Submit to send the command.
167 | 4. If everything works, the device should lock and you're good to go! As of right now some of the commands aren't fully implemented. Feel free to experiment with different commands!
168 |
169 | ---
170 | 
171 | ---
172 |
173 | You can now run those commands from any web browser, a successfull command will often looks something like the following:
174 |
175 | ---
176 | 
177 | ---
178 |
179 | Click the "Response" button to see the plist response from apple. Click the pencil to edit the device name, device owner, and device location.
180 |
181 |
182 | When stopping the server, the standard control-c doesn't usually work. Instead use control-z to suspend the process and then use a kill command to end the process.
183 |
184 | ^z
185 | [1]+ Stopped python server.py
186 | user:~/mdm-server/server$ kill %1
187 | [1]+ Terminated python server.py
188 | user:~/mdm-server/server$
189 |
190 | The server uses the pickle library to save devices. When the device class is updated, the pickle format may be invalidated, causing the server to error. In order to fix this, remove the devicelist.pickle file (make a backup just in case!) and re-enroll all devices.
191 |
192 | # Client Reporting
193 |
194 | The MDM server also has REST endpoints for reporting issues and geolocation data from the enrolled clients. This functionality may be used at a later point in time by a security app. The API can be imported into any project as follows:
195 |
196 | * Click on the top level Project item and add files ("option-command-a")
197 | * Navigate to client-reporting/
198 | * Highlight the client-reporting subdirectory
199 | * Click the Add button
200 |
201 | The library provides the following functions:
202 |
203 | +(void) setHostAddress: (NSString*) host; // Set where the MDM server lives
204 | +(void) setPause : (BOOL) toggle; // Toggle whether to add a thread execution pause to allow requests to finish
205 | +(void) reportJailbreak; // Report that the device has been jailbroken
206 | +(void) reportDebugger; // Report that the application has a debugger attached
207 | +(void) reportLocation : (CLLocationCoordinate2D*) coords; // Report the lat/lon location of the device
208 |
209 | "setHostAddress" and "setPause" are meant to be set once only, and effect all "report" calls. An example usage may look like:
210 |
211 | // Code in application init
212 | [client_reporting setHostAddress:@"192.168.0.0"];
213 | [client_reporting setPause:YES];
214 |
215 | // Later code during execution
216 | [client_reporting reportDebugger]
217 |
218 | This client API can be coupled with the [iMAS security-check controls](git@github.com:project-imas/security-check.git) to provide accurate reporting of jailbreak and debugger detection.
219 |
220 |
221 | Apologies for the long and complex setup, we hope to eventually make things easier and simpler. Please post questions to github if you get stuck and we'll do our best to help. Enjoy!
222 |
223 |
224 |
225 | # LICENSE AND ATTRIBUTION
226 |
227 | Copyright 2013-2014 The MITRE Corporation, All Rights Reserved.
228 |
229 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License at
230 |
231 | http://www.apache.org/licenses/LICENSE-2.0
232 |
233 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
234 |
235 |
236 | This project also uses code from various sources under various licenses.
237 |
238 | [The original code from the Intrepidus Group's python server is under the BSD License found here.](server/LICENSE)
239 |
240 | [The python vendor signing code is located here and is under the MIT license.](https://github.com/grinich/mdmvendorsign)
241 |
242 | [The Softhinker certificate signing code is under the Apache License found here.](vendor-signing/LICENSE)
243 |
244 | [The website's Bootstrap code is under the MIT License found here.](server/static/dist/LICENSE)
245 |
246 | The certificate setup instructions were based on [this blog post](http://www.blueboxmoon.com/wordpress/?p=877). Our thanks to Daniel.
247 |
248 | Finally we use some free [glyphicons](http://glyphicons.com/) that are included with bootstrap.
249 |
--------------------------------------------------------------------------------
/server/static/dist/css/bootstrap-theme.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.1.1 (http://getbootstrap.com)
3 | * Copyright 2011-2014 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | .btn-default,
8 | .btn-primary,
9 | .btn-success,
10 | .btn-info,
11 | .btn-warning,
12 | .btn-danger {
13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
16 | }
17 | .btn-default:active,
18 | .btn-primary:active,
19 | .btn-success:active,
20 | .btn-info:active,
21 | .btn-warning:active,
22 | .btn-danger:active,
23 | .btn-default.active,
24 | .btn-primary.active,
25 | .btn-success.active,
26 | .btn-info.active,
27 | .btn-warning.active,
28 | .btn-danger.active {
29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
31 | }
32 | .btn:active,
33 | .btn.active {
34 | background-image: none;
35 | }
36 | .btn-default {
37 | text-shadow: 0 1px 0 #fff;
38 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
39 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
40 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
41 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
42 | background-repeat: repeat-x;
43 | border-color: #dbdbdb;
44 | border-color: #ccc;
45 | }
46 | .btn-default:hover,
47 | .btn-default:focus {
48 | background-color: #e0e0e0;
49 | background-position: 0 -15px;
50 | }
51 | .btn-default:active,
52 | .btn-default.active {
53 | background-color: #e0e0e0;
54 | border-color: #dbdbdb;
55 | }
56 | .btn-primary {
57 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
58 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
59 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
60 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
61 | background-repeat: repeat-x;
62 | border-color: #2b669a;
63 | }
64 | .btn-primary:hover,
65 | .btn-primary:focus {
66 | background-color: #2d6ca2;
67 | background-position: 0 -15px;
68 | }
69 | .btn-primary:active,
70 | .btn-primary.active {
71 | background-color: #2d6ca2;
72 | border-color: #2b669a;
73 | }
74 | .btn-success {
75 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
76 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
79 | background-repeat: repeat-x;
80 | border-color: #3e8f3e;
81 | }
82 | .btn-success:hover,
83 | .btn-success:focus {
84 | background-color: #419641;
85 | background-position: 0 -15px;
86 | }
87 | .btn-success:active,
88 | .btn-success.active {
89 | background-color: #419641;
90 | border-color: #3e8f3e;
91 | }
92 | .btn-info {
93 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
94 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
95 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
96 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
97 | background-repeat: repeat-x;
98 | border-color: #28a4c9;
99 | }
100 | .btn-info:hover,
101 | .btn-info:focus {
102 | background-color: #2aabd2;
103 | background-position: 0 -15px;
104 | }
105 | .btn-info:active,
106 | .btn-info.active {
107 | background-color: #2aabd2;
108 | border-color: #28a4c9;
109 | }
110 | .btn-warning {
111 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
112 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
113 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
114 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
115 | background-repeat: repeat-x;
116 | border-color: #e38d13;
117 | }
118 | .btn-warning:hover,
119 | .btn-warning:focus {
120 | background-color: #eb9316;
121 | background-position: 0 -15px;
122 | }
123 | .btn-warning:active,
124 | .btn-warning.active {
125 | background-color: #eb9316;
126 | border-color: #e38d13;
127 | }
128 | .btn-danger {
129 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
130 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
131 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
132 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
133 | background-repeat: repeat-x;
134 | border-color: #b92c28;
135 | }
136 | .btn-danger:hover,
137 | .btn-danger:focus {
138 | background-color: #c12e2a;
139 | background-position: 0 -15px;
140 | }
141 | .btn-danger:active,
142 | .btn-danger.active {
143 | background-color: #c12e2a;
144 | border-color: #b92c28;
145 | }
146 | .thumbnail,
147 | .img-thumbnail {
148 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
149 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
150 | }
151 | .dropdown-menu > li > a:hover,
152 | .dropdown-menu > li > a:focus {
153 | background-color: #e8e8e8;
154 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
155 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
156 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
157 | background-repeat: repeat-x;
158 | }
159 | .dropdown-menu > .active > a,
160 | .dropdown-menu > .active > a:hover,
161 | .dropdown-menu > .active > a:focus {
162 | background-color: #357ebd;
163 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
164 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
165 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
166 | background-repeat: repeat-x;
167 | }
168 | .navbar-default {
169 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
170 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
171 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
172 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
173 | background-repeat: repeat-x;
174 | border-radius: 4px;
175 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
176 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
177 | }
178 | .navbar-default .navbar-nav > .active > a {
179 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
180 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
182 | background-repeat: repeat-x;
183 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
184 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
185 | }
186 | .navbar-brand,
187 | .navbar-nav > li > a {
188 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
189 | }
190 | .navbar-inverse {
191 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
192 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
193 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
194 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
195 | background-repeat: repeat-x;
196 | }
197 | .navbar-inverse .navbar-nav > .active > a {
198 | background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
199 | background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
201 | background-repeat: repeat-x;
202 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
203 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
204 | }
205 | .navbar-inverse .navbar-brand,
206 | .navbar-inverse .navbar-nav > li > a {
207 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
208 | }
209 | .navbar-static-top,
210 | .navbar-fixed-top,
211 | .navbar-fixed-bottom {
212 | border-radius: 0;
213 | }
214 | .alert {
215 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
216 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
217 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
218 | }
219 | .alert-success {
220 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
221 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
222 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
223 | background-repeat: repeat-x;
224 | border-color: #b2dba1;
225 | }
226 | .alert-info {
227 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
228 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
230 | background-repeat: repeat-x;
231 | border-color: #9acfea;
232 | }
233 | .alert-warning {
234 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
235 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
236 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
237 | background-repeat: repeat-x;
238 | border-color: #f5e79e;
239 | }
240 | .alert-danger {
241 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
242 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
243 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
244 | background-repeat: repeat-x;
245 | border-color: #dca7a7;
246 | }
247 | .progress {
248 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
249 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
250 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
251 | background-repeat: repeat-x;
252 | }
253 | .progress-bar {
254 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
255 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
256 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
257 | background-repeat: repeat-x;
258 | }
259 | .progress-bar-success {
260 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
261 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
262 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
263 | background-repeat: repeat-x;
264 | }
265 | .progress-bar-info {
266 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
267 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
268 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
269 | background-repeat: repeat-x;
270 | }
271 | .progress-bar-warning {
272 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
273 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
274 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
275 | background-repeat: repeat-x;
276 | }
277 | .progress-bar-danger {
278 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
279 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
281 | background-repeat: repeat-x;
282 | }
283 | .list-group {
284 | border-radius: 4px;
285 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
286 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
287 | }
288 | .list-group-item.active,
289 | .list-group-item.active:hover,
290 | .list-group-item.active:focus {
291 | text-shadow: 0 -1px 0 #3071a9;
292 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
293 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
295 | background-repeat: repeat-x;
296 | border-color: #3278b3;
297 | }
298 | .panel {
299 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
300 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
301 | }
302 | .panel-default > .panel-heading {
303 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
304 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
305 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
306 | background-repeat: repeat-x;
307 | }
308 | .panel-primary > .panel-heading {
309 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
310 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
311 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
312 | background-repeat: repeat-x;
313 | }
314 | .panel-success > .panel-heading {
315 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
316 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
317 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
318 | background-repeat: repeat-x;
319 | }
320 | .panel-info > .panel-heading {
321 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
322 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
323 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
324 | background-repeat: repeat-x;
325 | }
326 | .panel-warning > .panel-heading {
327 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
328 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
329 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
330 | background-repeat: repeat-x;
331 | }
332 | .panel-danger > .panel-heading {
333 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
334 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
336 | background-repeat: repeat-x;
337 | }
338 | .well {
339 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
340 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
341 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
342 | background-repeat: repeat-x;
343 | border-color: #dcdcdc;
344 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
345 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
346 | }
347 | /*# sourceMappingURL=bootstrap-theme.css.map */
348 |
--------------------------------------------------------------------------------
/server/server.py:
--------------------------------------------------------------------------------
1 | import web, os, json, uuid, sys
2 | import cPickle as pickle
3 | from device import device # Custom device class
4 | from plistlib import *
5 | from APNSWrapper import *
6 | from problems import *
7 | from datetime import datetime
8 | # needed to handle verification of signed messages from devices
9 | from M2Crypto import SMIME, X509, BIO
10 |
11 | #
12 | # Simple, basic, bare-bones example test server
13 | # Implements Apple's Mobile Device Management (MDM) protocol
14 | # Compatible with iOS 4.x devices
15 | #
16 | #
17 | # David Schuetz, Senior Consultant, Intrepidus Group
18 | #
19 | # Copyright 2011, Intrepidus Group
20 | # http://intrepidusgroup.com
21 |
22 | # Reuse permitted under terms of BSD License (see LICENSE file).
23 | # No warranties, expressed or implied.
24 | # This is experimental software, for research only. Use at your own risk.
25 |
26 | #
27 | # Revision History:
28 | #
29 | # * August 2011 - initial release, Black Hat USA
30 | # * January 2012 - minor tweaks, including favicon, useful README, and
31 | # scripts to create certs, log file, etc.
32 | # * January 2012 - Added support for some iOS 5 functions. ShmooCon 8.
33 | # * February 2012 - Can now verify signed messages from devices
34 | # - Tweaks to CherryPy startup to avoid errors on console
35 | # * January 2014 - Support for multiple enrollments
36 | # - Supports reporting problems
37 | # * April 2014 - Support for new front end
38 | # - Tweaks and bug fixes
39 | # * May 2014 - New device class
40 | # - Rework server to use device class
41 | # - Fixes a number of problems with using multiple devices
42 | # - Support for new device-based front end
43 |
44 |
45 | # Global variable setup
46 | LOGFILE = 'xactn.log'
47 |
48 | # Dummy socket to get the hostname
49 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
50 | s.connect(('8.8.8.8', 0))
51 |
52 | # NOTE: Will need to overwrite this if behind a firewall
53 | MY_ADDR = s.getsockname()[0] + ":8080"
54 |
55 | # Set up some smime objects to verify signed messages coming from devices
56 | sm_obj = SMIME.SMIME()
57 | x509 = X509.load_cert('identity.crt')
58 | sk = X509.X509_Stack()
59 | sk.push(x509)
60 | sm_obj.set_x509_stack(sk)
61 |
62 | st = X509.X509_Store()
63 | st.load_info('CA.crt')
64 | sm_obj.set_x509_store(st)
65 |
66 |
67 | ###########################################################################
68 | # Update this to match the UUID in the test provisioning profiles, in order
69 | # to demonstrate removal of the profile
70 |
71 | my_test_provisioning_uuid = 'REPLACE-ME-WITH-REAL-UUIDSTRING'
72 |
73 | from web.wsgiserver import CherryPyWSGIServer
74 |
75 | # Python 2.7 requires the PyOpenSSL library
76 | # Python 3.x should use be able to use the default python SSL
77 | try:
78 | from OpenSSL import SSL
79 | from OpenSSL import crypto
80 | except ImportError:
81 | SSL = None
82 |
83 |
84 | CherryPyWSGIServer.ssl_certificate = "Server.crt"
85 | CherryPyWSGIServer.ssl_private_key = "Server.key"
86 |
87 | ###########################################################################
88 |
89 | device_list = dict()
90 |
91 | global mdm_commands
92 |
93 | urls = (
94 | '/', 'root',
95 | '/queue', 'queue_cmd_post',
96 | '/checkin', 'do_mdm',
97 | '/server', 'do_mdm',
98 | '/ServerURL', 'do_mdm',
99 | '/CheckInURL', 'do_mdm',
100 | '/enroll', 'enroll_profile',
101 | '/ca', 'mdm_ca',
102 | '/favicon.ico', 'favicon',
103 | '/manifest', 'app_manifest',
104 | '/app', 'app_ipa',
105 | '/problem', 'do_problem',
106 | '/problemjb', 'do_problem',
107 | '/poll', 'poll',
108 | '/getcommands', 'get_commands',
109 | '/devices', 'dev_tab',
110 | '/response', 'get_response',
111 | '/metadata', 'metadata',
112 | )
113 |
114 |
115 |
116 | def setup_commands():
117 | # Function to generate dictionary of valid commands
118 | global my_test_provisioning_uuid
119 |
120 | ret_list = dict()
121 |
122 | for cmd in ['DeviceLock', 'ProfileList', 'Restrictions',
123 | 'CertificateList', 'InstalledApplicationList',
124 | 'ProvisioningProfileList',
125 | # new for iOS 5:
126 | 'ManagedApplicationList',]:
127 | ret_list[cmd] = dict( Command = dict( RequestType = cmd ))
128 |
129 | ret_list['SecurityInfo'] = dict(
130 | Command = dict(
131 | RequestType = 'SecurityInfo',
132 | Queries = [
133 | 'HardwareEncryptionCaps', 'PasscodePresent',
134 | 'PasscodeCompliant', 'PasscodeCompliantWithProfiles',
135 | ]
136 | )
137 | )
138 |
139 | ret_list['DeviceInformation'] = dict(
140 | Command = dict(
141 | RequestType = 'DeviceInformation',
142 | Queries = [
143 | 'AvailableDeviceCapacity', 'BluetoothMAC', 'BuildVersion',
144 | 'CarrierSettingsVersion', 'CurrentCarrierNetwork',
145 | 'CurrentMCC', 'CurrentMNC', 'DataRoamingEnabled',
146 | 'DeviceCapacity', 'DeviceName', 'ICCID', 'IMEI', 'IsRoaming',
147 | 'Model', 'ModelName', 'ModemFirmwareVersion', 'OSVersion',
148 | 'PhoneNumber', 'Product', 'ProductName', 'SIMCarrierNetwork',
149 | 'SIMMCC', 'SIMMNC', 'SerialNumber', 'UDID', 'WiFiMAC', 'UDID',
150 | 'UnlockToken', 'MEID', 'CellularTechnology', 'BatteryLevel',
151 | 'SubscriberCarrierNetwork', 'VoiceRoamingEnabled',
152 | 'SubscriberMCC', 'SubscriberMNC', 'DataRoaming', 'VoiceRoaming',
153 | 'JailbreakDetected'
154 | ]
155 | )
156 | )
157 |
158 | ret_list['ClearPasscode'] = dict(
159 | Command = dict(
160 | RequestType = 'ClearPasscode',
161 | # When ClearPasscode is used, the device specific unlock token needs to be added
162 | # UnlockToken = Data(my_UnlockToken)
163 | )
164 | )
165 |
166 | # commented out, and command string changed, to avoid accidentally
167 | # erasing test devices.
168 | #
169 | # ret_list['EraseDevice'] = dict(
170 | # Command = dict(
171 | # RequestType = 'DONT_EraseDevice',
172 | # )
173 | # )
174 | #
175 | if 'Example.mobileconfig' in os.listdir('.'):
176 | my_test_cfg_profile = open('Example.mobileconfig', 'rb').read()
177 | pl = readPlistFromString(my_test_cfg_profile)
178 |
179 | ret_list['InstallProfile'] = dict(
180 | Command = dict(
181 | RequestType = 'InstallProfile',
182 | Payload = Data(my_test_cfg_profile)
183 | )
184 | )
185 |
186 | ret_list['RemoveProfile'] = dict(
187 | Command = dict(
188 | RequestType = 'RemoveProfile',
189 | Identifier = pl['PayloadIdentifier']
190 | )
191 | )
192 |
193 | else:
194 | print "Can't find Example.mobileconfig in current directory."
195 |
196 |
197 | if 'MyApp.mobileprovision' in os.listdir('.'):
198 | my_test_prov_profile = open('MyApp.mobileprovision', 'rb').read()
199 |
200 | ret_list['InstallProvisioningProfile'] = dict(
201 | Command = dict(
202 | RequestType = 'InstallProvisioningProfile',
203 | ProvisioningProfile = Data(my_test_prov_profile)
204 | )
205 | )
206 |
207 | ret_list['RemoveProvisioningProfile'] = dict(
208 | Command = dict(
209 | RequestType = 'RemoveProvisioningProfile',
210 | # need an ASN.1 parser to snarf the UUID out of the signed profile
211 | UUID = my_test_provisioning_uuid
212 | )
213 | )
214 |
215 | else:
216 | print "Can't find MyApp.mobileprovision in current directory."
217 |
218 | #
219 | # iOS 5:
220 | #
221 | ret_list['InstallApplication'] = dict(
222 | Command = dict(
223 | RequestType = 'InstallApplication',
224 | ManagementFlags = 4, # do not delete app when unenrolling from MDM
225 | iTunesStoreID=471966214, # iTunes Movie Trailers
226 | ))
227 |
228 | if ('MyApp.ipa' in os.listdir('.')) and ('Manifest.plist' in os.listdir('.')):
229 | ret_list['InstallCustomApp'] = dict(
230 | Command = dict(
231 | RequestType = 'InstallApplication',
232 | ManifestURL = 'https://%s/manifest' % MY_ADDR,
233 | ManagementFlags = 1, # delete app when unenrolling from MDM
234 | ))
235 | print ret_list['InstallCustomApp']
236 | else:
237 | print "Need both MyApp.ipa and Manifest.plist to enable InstallCustomApp."
238 |
239 |
240 | ret_list['RemoveApplication'] = dict(
241 | Command = dict(
242 | RequestType = 'RemoveApplication',
243 | Identifier = 'com.apple.movietrailers',
244 | ))
245 |
246 | ret_list['RemoveCustomApplication'] = dict(
247 | Command = dict(
248 | RequestType = 'RemoveApplication',
249 | Identifier = 'mitre.managedTest',
250 | ))
251 |
252 | #
253 | # on an ipad, you'll likely get errors for the "VoiceRoaming" part.
254 | # Since, you know...it's not a phone.
255 | #
256 | ret_list['Settings'] = dict(
257 | Command = dict(
258 | RequestType = 'Settings',
259 | Settings = [
260 | dict(
261 | Item = 'DataRoaming',
262 | Enabled = False,
263 | ),
264 | dict(
265 | Item = 'VoiceRoaming',
266 | Enabled = True,
267 | ),
268 | ]
269 | ))
270 |
271 | #
272 | # haven't figured out how to make this one work yet. :(
273 | #
274 | # ret_list['ApplyRedemptionCode'] = dict(
275 | # Command = dict(
276 | # RequestType = 'ApplyRedemptionCode',
277 | ## do I maybe need to add an iTunesStoreID in here?
278 | # RedemptionCode = '3WABCDEFGXXX',
279 | # iTunesStoreID=471966214, # iTunes Movie Trailers
280 | # ManagementFlags = 1,
281 | # ))
282 |
283 |
284 |
285 | return ret_list
286 |
287 | class root:
288 | def GET(self):
289 | return web.redirect("/static/index.html")
290 |
291 | def queue(cmd, dev_UDID):
292 | # Function to add a command to a device queue
293 | global device_list, mdm_commands
294 |
295 | mylocal_PushMagic, mylocal_DeviceToken = device_list[dev_UDID].getQueueInfo()
296 |
297 | cmd_data = mdm_commands[cmd]
298 | cmd_data['CommandUUID'] = str(uuid.uuid4())
299 |
300 | # Have to search through device_list using pushmagic or devtoken to get UDID
301 | for key in device_list:
302 | if device_list[key].UDID == dev_UDID:
303 | device_list[key].addCommand(cmd_data)
304 | print "*Adding CMD:", cmd_data['CommandUUID'], "to device:", key
305 | break
306 |
307 | store_devices()
308 |
309 |
310 | # Send request to Apple
311 | wrapper = APNSNotificationWrapper('PushCert.pem', False)
312 | message = APNSNotification()
313 | message.token(mylocal_DeviceToken)
314 | message.appendProperty(APNSProperty('mdm', mylocal_PushMagic))
315 | wrapper.append(message)
316 | wrapper.notify()
317 |
318 |
319 |
320 | class queue_cmd_post:
321 | def POST(self):
322 | global device_list
323 |
324 | i = json.loads(web.data())
325 | cmd = i.pop("cmd", [])
326 | dev = i.pop("dev[]", [])
327 |
328 | for UDID in dev:
329 | queue(cmd, UDID)
330 |
331 | # Update page - currently not using update()
332 | #return update()
333 | return
334 |
335 | class do_mdm:
336 | def PUT(self):
337 | global sm_obj, device_list
338 | HIGH='[1;31m'
339 | LOW='[0;32m'
340 | NORMAL='[0;39m'
341 |
342 | i = web.data()
343 | pl = readPlistFromString(i)
344 |
345 | if 'HTTP_MDM_SIGNATURE' in web.ctx.environ:
346 | raw_sig = web.ctx.environ['HTTP_MDM_SIGNATURE']
347 | cooked_sig = '\n'.join(raw_sig[pos:pos+76] for pos in xrange(0, len(raw_sig), 76))
348 |
349 | signature = '\n-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----\n' % cooked_sig
350 |
351 | # Verify client signature - necessary?
352 | buf = BIO.MemoryBuffer(signature)
353 | p7 = SMIME.load_pkcs7_bio(buf)
354 | data_bio = BIO.MemoryBuffer(i)
355 | try:
356 | v = sm_obj.verify(p7, data_bio)
357 | if v:
358 | print "Client signature verified."
359 | except:
360 | print "*** INVALID CLIENT MESSAGE SIGNATURE ***"
361 |
362 | print "%sReceived %4d bytes: %s" % (HIGH, len(web.data()), NORMAL),
363 |
364 | if pl.get('Status') == 'Idle':
365 | print HIGH + "Idle Status" + NORMAL
366 |
367 | print "*FETCHING CMD TO BE SENT FROM DEVICE:", pl['UDID']
368 | rd = device_list[pl['UDID']].sendCommand()
369 |
370 | # If no commands in queue, return empty string to avoid infinite idle loop
371 | if(not rd):
372 | return ''
373 |
374 | print "%sSent: %s%s" % (HIGH, rd['Command']['RequestType'], NORMAL)
375 |
376 | elif pl.get('MessageType') == 'TokenUpdate':
377 | print HIGH+"Token Update"+NORMAL
378 | rd = do_TokenUpdate(pl)
379 | print HIGH+"Device Enrolled!"+NORMAL
380 |
381 | elif pl.get('Status') == 'Acknowledged':
382 | print HIGH+"Acknowledged"+NORMAL
383 | rd = dict()
384 | # A command has returned a response
385 | # Add the response to the given device
386 | print "*CALLING ADD RESPONSE TO CMD:", pl['CommandUUID']
387 | device_list[pl['UDID']].addResponse(pl['CommandUUID'], pl)
388 |
389 | # If we grab device information, we should also update the device info
390 | if pl.get('QueryResponses'):
391 | print "DeviceInformation should update here..."
392 | p = pl['QueryResponses']
393 | device_list[pl['UDID']].updateInfo(p['DeviceName'], p['ModelName'], p['OSVersion'])
394 |
395 | # Update pickle file with new response
396 | store_devices()
397 | else:
398 | rd = dict()
399 | if pl.get('MessageType') == 'Authenticate':
400 | print HIGH+"Authenticate"+NORMAL
401 | elif pl.get('MessageType') == 'CheckOut':
402 | print HIGH+"Device leaving MDM"+ NORMAL
403 | elif pl.get('Status') == 'Error':
404 | print "*CALLING ADD RESPONSE WITH ERROR TO CMD:", pl['CommandUUID']
405 | device_list[pl['UDID']].addResponse(pl['CommandUUID'], pl)
406 | else:
407 | print HIGH+"(other)"+NORMAL
408 | print HIGH, pl, NORMAL
409 | log_data(pl)
410 | log_data(rd)
411 |
412 | out = writePlistToString(rd)
413 | #print LOW, out, NORMAL
414 |
415 | return out
416 |
417 | class get_commands:
418 | def POST(self):
419 | # Function to return static list of commands to the front page
420 | # Should be called once by document.ready
421 | global mdm_commands
422 |
423 | drop_list = []
424 | for key in sorted(mdm_commands.iterkeys()):
425 | drop_list.append([key, key])
426 | return json.dumps(drop_list)
427 |
428 | def update():
429 |
430 | # DEPRICATED
431 | # Current polling endpoint is /devices
432 | # May be updated later on for intelligent updating of devices
433 |
434 | # Function to update devices on the frontend
435 | # Is called on page load and polling
436 |
437 | global problems, device_list
438 |
439 | # Create list of devices
440 | dev_list_out = []
441 | for UDID in device_list:
442 | dev_list_out.append([device_list[UDID].IP, device_list[UDID].pushMagic])
443 |
444 | # Format output as a dict and then return as JSON
445 | out = dict()
446 | out['dev_list'] = dev_list_out
447 | out['problems'] = '