├── requirements.txt ├── README.md ├── LICENSE.md └── rdpscraper.py /requirements.txt: -------------------------------------------------------------------------------- 1 | pyscreenshot==0.4.2 2 | pytesseract==0.1.7 3 | Twisted==17.9.0 4 | Pillow==5.0.0 5 | qt4reactor==1.6 6 | rdpy==1.3.2 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RDP Scraper 2 | 3 | Created by: Steven Laura/@steven1664 && Jacob Robles/@shellfail && Shane Young/@x90skysn3k 4 | 5 | #### Version - 0.4 6 | 7 | # Installation 8 | 9 | ```pip install -r requirements.txt``` 10 | 11 | # Usage 12 | 13 | First do an nmap scan with ```-oG nmap.gnmap``` or ```-oX nmap.xml```. 14 | 15 | Command: ```./rdpscraper.py --file nmap.gnmap``` 16 | 17 | Command: ```./rdpscraper.py --file nmap.xml``` 18 | 19 | # Changelog 20 | * v0.4 21 | * false positive workarounds 22 | * fixed reading output 23 | * v0.3 24 | * write usernames to files (wip) 25 | * added output option 26 | * added verbosity option 27 | * v0.2 28 | * reading and writing through temp directories 29 | * output text to directory 30 | * tune image taking 31 | * v0.1 32 | * initial commit and code for screenshot and reading images 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2017] [Shane Young] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rdpscraper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | import sys, time, os 4 | import tempfile 5 | import re 6 | import argparse 7 | import xml.dom.minidom 8 | from PIL import Image 9 | from PIL import ImageEnhance 10 | import pytesseract 11 | from io import BytesIO 12 | import pyscreenshot as ImageGrab 13 | import getopt 14 | from PyQt4 import QtCore, QtGui 15 | from rdpy.protocol.rdp import rdp 16 | from rdpy.ui.qt4 import RDPBitmapToQtImage 17 | import rdpy.core.log as log 18 | from rdpy.core.error import RDPSecurityNegoFail 19 | from twisted.internet import task 20 | import threading 21 | import itertools 22 | 23 | 24 | class colors: 25 | white = "\033[1;37m" 26 | normal = "\033[0;00m" 27 | red = "\033[1;31m" 28 | blue = "\033[1;34m" 29 | green = "\033[1;32m" 30 | lightblue = "\033[0;34m" 31 | 32 | services = {} 33 | 34 | banner = colors.red + r""" 35 | 36 | ▄████████ ████████▄ ▄███████▄ ▄████████ ▄████████ ▄████████ ▄████████ ▄███████▄ ▄████████ ▄████████ 37 | ███ ███ ███ ▀███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ 38 | ███ ███ ███ ███ ███ ███ ███ █▀ ███ █▀ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ 39 | ▄███▄▄▄▄██▀ ███ ███ ███ ███ ███ ███ ▄███▄▄▄▄██▀ ███ ███ ███ ███ ▄███▄▄▄ ▄███▄▄▄▄██▀ 40 | ▀▀███▀▀▀▀▀ ███ ███ ▀█████████▀ ▀███████████ ███ ▀▀███▀▀▀▀▀ ▀███████████ ▀█████████▀ ▀▀███▀▀▀ ▀▀███▀▀▀▀▀ 41 | ▀███████████ ███ ███ ███ ███ ███ █▄ ▀███████████ ███ ███ ███ ███ █▄ ▀███████████ 42 | ███ ███ ███ ▄███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ 43 | ███ ███ ████████▀ ▄████▀ ▄████████▀ ████████▀ ███ ███ ███ █▀ ▄████▀ ██████████ ███ ███ 44 | ███ ███ ███ ███ ███ ███ 45 | """+'\n' \ 46 | + '\n rdpscraper.py v0.4'\ 47 | + '\n Created by: Steven Laura/@steven1664 && Jacob Robles/@shellfail && Shane Young/@x90skysn3k\n' + colors.normal 48 | 49 | def make_dic_gnmap(): 50 | global services 51 | port = None 52 | with open(args.file, 'r') as nmap_file: 53 | for line in nmap_file: 54 | supported = ['ms-wbt-server'] 55 | for name in supported: 56 | matches = re.compile(r'([0-9][0-9]*)/open/[a-z][a-z]*//' + name) 57 | try: 58 | port = matches.findall(line)[0] 59 | except: 60 | continue 61 | 62 | ip = re.findall( r'[0-9]+(?:\.[0-9]+){3}', line) 63 | tmp_ports = matches.findall(line) 64 | for tmp_port in tmp_ports: 65 | if name in services: 66 | if tmp_port in services[name]: 67 | services[name][tmp_port] += ip 68 | else: 69 | services[name][tmp_port] = ip 70 | else: 71 | services[name] = {tmp_port:ip} 72 | 73 | 74 | def make_dic_xml(): 75 | global loading 76 | global services 77 | doc = xml.dom.minidom.parse(args.file) 78 | supported = ['ms-wbt-server'] 79 | for host in doc.getElementsByTagName("host"): 80 | try: 81 | address = host.getElementsByTagName("address")[0] 82 | ip = address.getAttribute("addr") 83 | eip = ip.encode("utf8") 84 | iplist = eip.split(',') 85 | except: 86 | # move to the next host 87 | continue 88 | try: 89 | status = host.getElementsByTagName("status")[0] 90 | state = status.getAttribute("state") 91 | except: 92 | state = "" 93 | try: 94 | ports = host.getElementsByTagName("ports")[0] 95 | ports = ports.getElementsByTagName("port") 96 | except: 97 | continue 98 | 99 | for port in ports: 100 | pn = port.getAttribute("portid") 101 | state_el = port.getElementsByTagName("state")[0] 102 | state = state_el.getAttribute("state") 103 | if state == "open": 104 | try: 105 | service = port.getElementsByTagName("service")[0] 106 | port_name = service.getAttribute("name") 107 | except: 108 | service = "" 109 | port_name = "" 110 | product_descr = "" 111 | product_ver = "" 112 | product_extra = "" 113 | name = port_name.encode("utf-8") 114 | tmp_port = pn.encode("utf-8") 115 | if name in services: 116 | if tmp_port in services[name]: 117 | services[name][tmp_port] += iplist 118 | else: 119 | services[name][tmp_port] = iplist 120 | else: 121 | services[name] = {tmp_port:iplist} 122 | 123 | def loading(): 124 | for c in itertools.cycle(['|', '/', '-', '\\']): 125 | if loading == True: 126 | break 127 | sys.stdout.write('\rTaking Screenshots Please Wait: ' + c) 128 | sys.stdout.flush() 129 | time.sleep(0.01) 130 | 131 | # set log level 132 | log._LOG_LEVEL = log.Level.WARNING 133 | 134 | 135 | class RDPScreenShotFactory(rdp.ClientFactory): 136 | """ 137 | @summary: Factory for screenshot exemple 138 | """ 139 | __INSTANCE__ = 0 140 | __STATE__ = [] 141 | 142 | def __init__(self, reactor, app, width, height, path, timeout): 143 | """ 144 | @param reactor: twisted reactor 145 | @param width: {integer} width of screen 146 | @param height: {integer} height of screen 147 | @param path: {str} path of output screenshot 148 | @param timeout: {float} close connection after timeout s without any updating 149 | """ 150 | RDPScreenShotFactory.__INSTANCE__ += 1 151 | self._reactor = reactor 152 | self._app = app 153 | self._width = width 154 | self._height = height 155 | self._path = path 156 | self._timeout = timeout 157 | #NLA server can't be screenshooting 158 | self._security = rdp.SecurityLevel.RDP_LEVEL_SSL 159 | 160 | def clientConnectionLost(self, connector, reason): 161 | """ 162 | @summary: Connection lost event 163 | @param connector: twisted connector use for rdp connection (use reconnect to restart connection) 164 | @param reason: str use to advertise reason of lost connection 165 | """ 166 | if reason.type == RDPSecurityNegoFail and self._security != "rdp": 167 | log.info("due to RDPSecurityNegoFail try standard security layer") 168 | self._security = rdp.SecurityLevel.RDP_LEVEL_RDP 169 | connector.connect() 170 | return 171 | 172 | log.info("connection lost : %s" % reason) 173 | RDPScreenShotFactory.__STATE__.append((connector.host, connector.port, reason)) 174 | RDPScreenShotFactory.__INSTANCE__ -= 1 175 | if(RDPScreenShotFactory.__INSTANCE__ == 0): 176 | self._reactor.stop() 177 | self._app.exit() 178 | 179 | def clientConnectionFailed(self, connector, reason): 180 | """ 181 | @summary: Connection failed event 182 | @param connector: twisted connector use for rdp connection (use reconnect to restart connection) 183 | @param reason: str use to advertise reason of lost connection 184 | """ 185 | log.info("connection failed : %s"%reason) 186 | RDPScreenShotFactory.__STATE__.append((connector.host, connector.port, reason)) 187 | RDPScreenShotFactory.__INSTANCE__ -= 1 188 | if(RDPScreenShotFactory.__INSTANCE__ == 0): 189 | self._reactor.stop() 190 | self._app.exit() 191 | 192 | def buildObserver(self, controller, addr): 193 | """ 194 | @summary: build ScreenShot observer 195 | @param controller: RDPClientController 196 | @param addr: address of target 197 | """ 198 | class ScreenShotObserver(rdp.RDPClientObserver): 199 | """ 200 | @summary: observer that connect, cache every image received and save at deconnection 201 | """ 202 | def __init__(self, controller, width, height, path, timeout, reactor): 203 | """ 204 | @param controller: {RDPClientController} 205 | @param width: {integer} width of screen 206 | @param height: {integer} height of screen 207 | @param path: {str} path of output screenshot 208 | @param timeout: {float} close connection after timeout s without any updating 209 | @param reactor: twisted reactor 210 | """ 211 | rdp.RDPClientObserver.__init__(self, controller) 212 | self._buffer = QtGui.QImage(width, height, QtGui.QImage.Format_RGB32) 213 | self._path = path 214 | self._timeout = timeout 215 | self._startTimeout = 5 216 | self._reactor = reactor 217 | 218 | def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): 219 | """ 220 | @summary: callback use when bitmap is received 221 | """ 222 | image = RDPBitmapToQtImage(width, height, bitsPerPixel, isCompress, data); 223 | with QtGui.QPainter(self._buffer) as qp: 224 | # draw image 225 | qp.drawImage(destLeft, destTop, image, 0, 0, destRight - destLeft + 1, destBottom - destTop + 1) 226 | if not self._startTimeout: 227 | self._startTimeout = False 228 | self._reactor.callLater(self._timeout, self.checkUpdate) 229 | 230 | def onReady(self): 231 | """ 232 | @summary: callback use when RDP stack is connected (just before received bitmap) 233 | """ 234 | log.info("connected %s" % addr) 235 | 236 | def onSessionReady(self): 237 | """ 238 | @summary: Windows session is ready 239 | @see: rdp.RDPClientObserver.onSessionReady 240 | """ 241 | pass 242 | 243 | def onClose(self): 244 | """ 245 | @summary: callback use when RDP stack is closed 246 | """ 247 | log.info("save screenshot into %s" % self._path) 248 | self._buffer.save(self._path) 249 | 250 | def checkUpdate(self): 251 | self._controller.close(); 252 | 253 | controller.setScreen(self._width, self._height); 254 | controller.setSecurityLevel(self._security) 255 | return ScreenShotObserver(controller, self._width, self._height, self._path, self._timeout, self._reactor) 256 | 257 | def main(width, height, path, timeout): 258 | """ 259 | @summary: main algorithm 260 | @param height: {integer} height of screenshot 261 | @param width: {integer} width of screenshot 262 | @param timeout: {float} in sec 263 | @param hosts: {list(str(ip[:port]))} 264 | @return: {list(tuple(ip, port, Failure instance)} list of connection state 265 | """ 266 | #create application 267 | app = QtGui.QApplication(sys.argv) 268 | 269 | #add qt4 reactor 270 | import qt4reactor 271 | qt4reactor.install() 272 | 273 | from twisted.internet import reactor 274 | 275 | 276 | with open(fname, 'r') as f: 277 | for ips in f: 278 | if ':' in ips: 279 | ip, port = ips.split(':') 280 | 281 | print "\nTaking Screenshot for: " + ip 282 | reactor.connectTCP(ip, int(port), RDPScreenShotFactory(reactor, app, width, height, path + "%s.jpg" % ip, timeout)) 283 | 284 | reactor.runReturn() 285 | app.exec_() 286 | return RDPScreenShotFactory.__STATE__ 287 | f.close() 288 | 289 | 290 | 291 | 292 | def parse_args(): 293 | 294 | parser = argparse.ArgumentParser(description=\ 295 | 296 | "Usage: python rdpscraper.py \n") 297 | 298 | menu_group = parser.add_argument_group(colors.lightblue + 'Menu Options' + colors.normal) 299 | 300 | menu_group.add_argument('-f', '--file', help="GNMAP or XML file to parse", required=True) 301 | menu_group.add_argument('-o', '--output', help="specifiy output direcotry", default="rdpscraper-output") 302 | menu_group.add_argument('-v', '--verbose', help="show screenshot output while running", default=False, action='store_true') 303 | 304 | 305 | args = parser.parse_args() 306 | 307 | return args 308 | 309 | print(banner) 310 | 311 | args = parse_args() 312 | 313 | 314 | try: 315 | tmppath = tempfile.mkdtemp(prefix="rdpscraper-tmp") 316 | except: 317 | sys.stderr.write("\nError while creating rdpscaper temp directory.") 318 | exit(4) 319 | 320 | width = 3072 321 | height = 1536 322 | path = tmppath + "/" 323 | timeout = 10.0 324 | bitsPerPixel = 24 325 | Loading = False 326 | 327 | try: 328 | doc = xml.dom.minidom.parse(args.file) 329 | make_dic_xml() 330 | except: 331 | make_dic_gnmap() 332 | if services is None: 333 | sys.exit(0) 334 | 335 | t = threading.Thread(target=loading) 336 | t.start() 337 | 338 | for service in services: 339 | for port in services[service]: 340 | fname = tmppath + "/" + service + '-' + port 341 | iplist = services[service][port] 342 | f = open(fname, 'w+') 343 | for ip in iplist: 344 | f.write(ip + ':' + port + '\n') 345 | f.close() 346 | 347 | main(width, height, path, timeout) 348 | 349 | outputpath = args.output + "/" 350 | if not os.path.exists(outputpath): 351 | os.mkdir(outputpath) 352 | 353 | 354 | loading = True 355 | 356 | with open(fname, 'r') as fn: 357 | for fns in fn: 358 | ip, port = fns.split(':') 359 | output = None 360 | if not os.path.exists(tmppath + "/" + ip +'.jpg'): 361 | print "\nScreenshot Unsuccessful for " + ip 362 | continue 363 | 364 | img = Image.open(tmppath + "/" + ip +'.jpg') 365 | string = pytesseract.image_to_string(img) 366 | if '2012' in string: 367 | img = img.resize([int(2.4 * s) for s in img.size]) 368 | enhancer = ImageEnhance.Sharpness(img) 369 | img = enhancer.enhance(0.7) 370 | contrast = ImageEnhance.Contrast(img) 371 | img = contrast.enhance(0.9) 372 | #color = ImageEnhance.Color(img) 373 | #img = color.enhance(0.) 374 | string = pytesseract.image_to_string(img) 375 | # output = "test.txt" 376 | # with open(output, 'w+') as f: 377 | # for line in string: 378 | # f.write('\n'.join(line)) 379 | # f.write('\n') 380 | #print "found" 381 | else: 382 | img = img.resize([int(2.2 * s) for s in img.size]) 383 | enhancer = ImageEnhance.Sharpness(img) 384 | img = enhancer.enhance(0.2) 385 | color = ImageEnhance.Color(img) 386 | img = color.enhance(0) 387 | #bright = ImageEnhance.Brightness(img) 388 | #img = bright.enhance(0.5) 389 | #contrast = ImageEnhance.Contrast(img) 390 | #img = contrast.enhance(0.2) 391 | string = pytesseract.image_to_string(img) 392 | #output = "test.txt" 393 | #with open(output, 'w+') as f: 394 | # for line in string: 395 | # f.write('\n'.join(line)) 396 | # f.write('\n') 397 | #print "found" 398 | #print(pytesseract.image_to_string(img)) 399 | #print "------------------------------------------------------------------------------------------------------------\n" 400 | output = pytesseract.image_to_string(img) 401 | 402 | if args.verbose is True: 403 | print "\nIP Address: " + ip + ":\n\n" 404 | print output 405 | print "-----------------------------------------------------------------------------\n" 406 | if output: 407 | f = open(outputpath + "output-" + ip + ".txt", 'w+') 408 | try: 409 | f.write(output + '\n') 410 | except: 411 | continue 412 | f.close() 413 | 414 | try: 415 | for line in f: 416 | print line + '\n' 417 | except: 418 | continue 419 | 420 | 421 | with open(fname, 'r') as fn: 422 | username = [] 423 | exclude = ['Other','options','Server','Standard','Logged','Windows', 'Update', 'Important', 'updates', 'are', 'available', 'Go', 'to', 'PC', 'settings', 'install', 'them','Professional','Cancel'] 424 | for fns in fn: 425 | ip, port = fns.split(':') 426 | if not os.path.exists(tmppath + "/" + ip +'.jpg'): 427 | continue 428 | try: 429 | f = open(outputpath + "output-" + ip + ".txt").read().split() 430 | except: 431 | continue 432 | for line in f: 433 | if any(s in line for s in exclude): 434 | if 'administrator' in line or 'Administrator' in line: 435 | username.append(line) 436 | continue 437 | else: 438 | if "\\" in line: 439 | #print '\n' + line 440 | username.append(line) 441 | if re.match(r'^[a-zA-Z0-9](_(?!(\.|_))|\.(?!(_|\.))|[a-zA-Z0-9]){3,18}[a-zA-Z0-9]$', line): 442 | #print '\n' + line 443 | username.append(line) 444 | 445 | outputfile = outputpath + args.file + "-usernames.txt" 446 | 447 | with open(outputfile, "w+") as u: 448 | finaluser = set(username) 449 | u.write('\n'.join(finaluser)) 450 | #u.write('\n'.join(username)) 451 | u.write('\n') 452 | 453 | print "\nUsername output written to: " + colors.green + outputfile + colors.normal 454 | --------------------------------------------------------------------------------