1#!/usr/bin/env python 2 3"""Back door shell server 4 5This exposes an shell terminal on a socket. 6 7 --hostname : sets the remote host name to open an ssh connection to. 8 --username : sets the user name to login with 9 --password : (optional) sets the password to login with 10 --port : set the local port for the server to listen on 11 --watch : show the virtual screen after each client request 12""" 13 14# Having the password on the command line is not a good idea, but 15# then this entire project is probably not the most security concious thing 16# I've ever built. This should be considered an experimental tool -- at best. 17import pxssh, pexpect, ANSI 18import time, sys, os, getopt, getpass, traceback, threading, socket 19 20def exit_with_usage(exit_code=1): 21 22 print globals()['__doc__'] 23 os._exit(exit_code) 24 25class roller (threading.Thread): 26 27 """This runs a function in a loop in a thread.""" 28 29 def __init__(self, interval, function, args=[], kwargs={}): 30 31 """The interval parameter defines time between each call to the function. 32 """ 33 34 threading.Thread.__init__(self) 35 self.interval = interval 36 self.function = function 37 self.args = args 38 self.kwargs = kwargs 39 self.finished = threading.Event() 40 41 def cancel(self): 42 43 """Stop the roller.""" 44 45 self.finished.set() 46 47 def run(self): 48 49 while not self.finished.isSet(): 50 # self.finished.wait(self.interval) 51 self.function(*self.args, **self.kwargs) 52 53def endless_poll (child, prompt, screen, refresh_timeout=0.1): 54 55 """This keeps the screen updated with the output of the child. This runs in 56 a separate thread. See roller(). """ 57 58 #child.logfile_read = screen 59 try: 60 s = child.read_nonblocking(4000, 0.1) 61 screen.write(s) 62 except: 63 pass 64 #while True: 65 # #child.prompt (timeout=refresh_timeout) 66 # try: 67 # #child.read_nonblocking(1,timeout=refresh_timeout) 68 # child.read_nonblocking(4000, 0.1) 69 # except: 70 # pass 71 72def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): 73 74 '''This forks the current process into a daemon. Almost none of this is 75 necessary (or advisable) if your daemon is being started by inetd. In that 76 case, stdin, stdout and stderr are all set up for you to refer to the 77 network connection, and the fork()s and session manipulation should not be 78 done (to avoid confusing inetd). Only the chdir() and umask() steps remain 79 as useful. 80 81 References: 82 UNIX Programming FAQ 83 1.7 How do I get my program to act like a daemon? 84 http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 85 86 Advanced Programming in the Unix Environment 87 W. Richard Stevens, 1992, Addison-Wesley, ISBN 0-201-56317-7. 88 89 The stdin, stdout, and stderr arguments are file names that will be opened 90 and be used to replace the standard file descriptors in sys.stdin, 91 sys.stdout, and sys.stderr. These arguments are optional and default to 92 /dev/null. Note that stderr is opened unbuffered, so if it shares a file 93 with stdout then interleaved output may not appear in the order that you 94 expect. ''' 95 96 # Do first fork. 97 try: 98 pid = os.fork() 99 if pid > 0: 100 sys.exit(0) # Exit first parent. 101 except OSError, e: 102 sys.stderr.write ("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror) ) 103 sys.exit(1) 104 105 # Decouple from parent environment. 106 os.chdir("/") 107 os.umask(0) 108 os.setsid() 109 110 # Do second fork. 111 try: 112 pid = os.fork() 113 if pid > 0: 114 sys.exit(0) # Exit second parent. 115 except OSError, e: 116 sys.stderr.write ("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror) ) 117 sys.exit(1) 118 119 # Now I am a daemon! 120 121 # Redirect standard file descriptors. 122 si = open(stdin, 'r') 123 so = open(stdout, 'a+') 124 se = open(stderr, 'a+', 0) 125 os.dup2(si.fileno(), sys.stdin.fileno()) 126 os.dup2(so.fileno(), sys.stdout.fileno()) 127 os.dup2(se.fileno(), sys.stderr.fileno()) 128 129 # I now return as the daemon 130 return 0 131 132def add_cursor_blink (response, row, col): 133 134 i = (row-1) * 80 + col 135 return response[:i]+'<img src="http://www.noah.org/cursor.gif">'+response[i:] 136 137def main (): 138 139 try: 140 optlist, args = getopt.getopt(sys.argv[1:], 'h?d', ['help','h','?', 'hostname=', 'username=', 'password=', 'port=', 'watch']) 141 except Exception, e: 142 print str(e) 143 exit_with_usage() 144 145 command_line_options = dict(optlist) 146 options = dict(optlist) 147 # There are a million ways to cry for help. These are but a few of them. 148 if [elem for elem in command_line_options if elem in ['-h','--h','-?','--?','--help']]: 149 exit_with_usage(0) 150 151 hostname = "127.0.0.1" 152 port = 1664 153 username = os.getenv('USER') 154 password = "" 155 daemon_mode = False 156 if '-d' in options: 157 daemon_mode = True 158 if '--watch' in options: 159 watch_mode = True 160 else: 161 watch_mode = False 162 if '--hostname' in options: 163 hostname = options['--hostname'] 164 if '--port' in options: 165 port = int(options['--port']) 166 if '--username' in options: 167 username = options['--username'] 168 print "Login for %s@%s:%s" % (username, hostname, port) 169 if '--password' in options: 170 password = options['--password'] 171 else: 172 password = getpass.getpass('password: ') 173 174 if daemon_mode: 175 print "daemonizing server" 176 daemonize() 177 #daemonize('/dev/null','/tmp/daemon.log','/tmp/daemon.log') 178 179 sys.stdout.write ('server started with pid %d\n' % os.getpid() ) 180 181 virtual_screen = ANSI.ANSI (24,80) 182 child = pxssh.pxssh() 183 child.login (hostname, username, password) 184 print 'created shell. command line prompt is', child.PROMPT 185 #child.sendline ('stty -echo') 186 #child.setecho(False) 187 virtual_screen.write (child.before) 188 virtual_screen.write (child.after) 189 190 if os.path.exists("/tmp/mysock"): os.remove("/tmp/mysock") 191 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 192 localhost = '127.0.0.1' 193 s.bind('/tmp/mysock') 194 os.chmod('/tmp/mysock',0777) 195 print 'Listen' 196 s.listen(1) 197 print 'Accept' 198 #s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 199 #localhost = '127.0.0.1' 200 #s.bind((localhost, port)) 201 #print 'Listen' 202 #s.listen(1) 203 204 r = roller (0.01, endless_poll, (child, child.PROMPT, virtual_screen)) 205 r.start() 206 print "screen poll updater started in background thread" 207 sys.stdout.flush() 208 209 try: 210 while True: 211 conn, addr = s.accept() 212 print 'Connected by', addr 213 data = conn.recv(1024) 214 if data[0]!=':': 215 cmd = ':sendline' 216 arg = data.strip() 217 else: 218 request = data.split(' ', 1) 219 if len(request)>1: 220 cmd = request[0].strip() 221 arg = request[1].strip() 222 else: 223 cmd = request[0].strip() 224 if cmd == ':exit': 225 r.cancel() 226 break 227 elif cmd == ':sendline': 228 child.sendline (arg) 229 #child.prompt(timeout=2) 230 time.sleep(0.2) 231 shell_window = str(virtual_screen) 232 elif cmd == ':send' or cmd==':xsend': 233 if cmd==':xsend': 234 arg = arg.decode("hex") 235 child.send (arg) 236 time.sleep(0.2) 237 shell_window = str(virtual_screen) 238 elif cmd == ':cursor': 239 shell_window = '%x%x' % (virtual_screen.cur_r, virtual_screen.cur_c) 240 elif cmd == ':refresh': 241 shell_window = str(virtual_screen) 242 243 response = [] 244 response.append (shell_window) 245 #response = add_cursor_blink (response, row, col) 246 sent = conn.send('\n'.join(response)) 247 if watch_mode: print '\n'.join(response) 248 if sent < len (response): 249 print "Sent is too short. Some data was cut off." 250 conn.close() 251 finally: 252 r.cancel() 253 print "cleaning up socket" 254 s.close() 255 if os.path.exists("/tmp/mysock"): os.remove("/tmp/mysock") 256 print "done!" 257 258def pretty_box (rows, cols, s): 259 260 """This puts an ASCII text box around the given string, s. 261 """ 262 263 top_bot = '+' + '-'*cols + '+\n' 264 return top_bot + '\n'.join(['|'+line+'|' for line in s.split('\n')]) + '\n' + top_bot 265 266def error_response (msg): 267 268 response = [] 269 response.append ("""All commands start with : 270:{REQUEST} {ARGUMENT} 271{REQUEST} may be one of the following: 272 :sendline: Run the ARGUMENT followed by a line feed. 273 :send : send the characters in the ARGUMENT without a line feed. 274 :refresh : Use to catch up the screen with the shell if state gets out of sync. 275Example: 276 :sendline ls -l 277You may also leave off :command and it will be assumed. 278Example: 279 ls -l 280is equivalent to: 281 :sendline ls -l 282""") 283 response.append (msg) 284 return '\n'.join(response) 285 286def parse_host_connect_string (hcs): 287 288 """This parses a host connection string in the form 289 username:password@hostname:port. All fields are options expcet hostname. A 290 dictionary is returned with all four keys. Keys that were not included are 291 set to empty strings ''. Note that if your password has the '@' character 292 then you must backslash escape it. """ 293 294 if '@' in hcs: 295 p = re.compile (r'(?P<username>[^@:]*)(:?)(?P<password>.*)(?!\\)@(?P<hostname>[^:]*):?(?P<port>[0-9]*)') 296 else: 297 p = re.compile (r'(?P<username>)(?P<password>)(?P<hostname>[^:]*):?(?P<port>[0-9]*)') 298 m = p.search (hcs) 299 d = m.groupdict() 300 d['password'] = d['password'].replace('\\@','@') 301 return d 302 303if __name__ == "__main__": 304 305 try: 306 start_time = time.time() 307 print time.asctime() 308 main() 309 print time.asctime() 310 print "TOTAL TIME IN MINUTES:", 311 print (time.time() - start_time) / 60.0 312 except Exception, e: 313 print str(e) 314 tb_dump = traceback.format_exc() 315 print str(tb_dump) 316 317