Skip to content

Commit e3c4c12

Browse files
committed
Working background listener API and commands
Needs more testing, but is functioning currently.
1 parent 1fda114 commit e3c4c12

File tree

7 files changed

+444
-14
lines changed

7 files changed

+444
-14
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and simply didn't have the time to go back and retroactively create one.
1414
- Added query-string arguments to connection strings for both the entrypoint
1515
and the `connect` command.
1616
- Added Enumeration States to allow session-bound enumerations
17+
- Added background listener API and commands ([#43](https://github.com/calebstewart/pwncat/issues/43))
1718

1819
## [0.4.3] - 2021-06-18
1920
Patch fix release. Major fixes are the correction of file IO for LinuxWriters and

Diff for: pwncat/commands/__init__.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -793,9 +793,14 @@ def build(cls, commands: List["CommandDefinition"]) -> Type["CommandLexer"]:
793793
"""Build the RegexLexer token list from the command definitions"""
794794

795795
root = []
796-
for command in commands:
796+
sorted_commands = sorted(commands, key=lambda cmd: len(cmd.PROG), reverse=True)
797+
for command in sorted_commands:
797798
root.append(
798-
("^" + re.escape(command.PROG), token.Name.Function, command.PROG)
799+
(
800+
"^" + re.escape(command.PROG) + "( |$)",
801+
token.Name.Function,
802+
command.PROG,
803+
)
799804
)
800805
mode = []
801806
if command.ARGS is not None:

Diff for: pwncat/commands/listener_new.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
from rich.prompt import Confirm
3+
4+
import pwncat
5+
from pwncat.util import console
6+
from pwncat.manager import ListenerState
7+
from pwncat.commands import Complete, Parameter, CommandDefinition
8+
9+
10+
class Command(CommandDefinition):
11+
"""
12+
Create a new background listener. Background listeners will continue
13+
listening while you do other things in pwncat. When a connection is
14+
established, the listener will either queue the new channel for
15+
future initialization or construct a full session.
16+
17+
If a platform is provided, a session will automatically be established
18+
for any new incoming connections. If no platform is provided, the
19+
channels will be queued, and can be initialized with the 'listeners'
20+
command.
21+
22+
If the drop_duplicate option is provided, sessions which connect to
23+
a host which already has an active session with the same user will
24+
be automatically dropped. This facilitates an infinite callback implant
25+
which you don't want to pollute the active session list.
26+
"""
27+
28+
PROG = "listen"
29+
ARGS = {
30+
"--count,-c": Parameter(
31+
Complete.NONE,
32+
type=int,
33+
help="Number of sessions a listener should accept before automatically stopping (default: infinite)",
34+
),
35+
"--platform,-m": Parameter(
36+
Complete.NONE,
37+
type=str,
38+
help="Name of the platform used to automatically construct a session for a new connection",
39+
),
40+
"--ssl": Parameter(
41+
Complete.NONE,
42+
action="store_true",
43+
default=False,
44+
help="Wrap a new listener in an SSL context",
45+
),
46+
"--ssl-cert": Parameter(
47+
Complete.LOCAL_FILE,
48+
help="SSL Server Certificate for SSL wrapped listeners",
49+
),
50+
"--ssl-key": Parameter(
51+
Complete.LOCAL_FILE,
52+
help="SSL Server Private Key for SSL wrapped listeners",
53+
),
54+
"--host,-H": Parameter(
55+
Complete.NONE,
56+
help="Host address on which to bind (default: 0.0.0.0)",
57+
default="0.0.0.0",
58+
),
59+
"port": Parameter(
60+
Complete.NONE,
61+
type=int,
62+
help="Port on which to listen for new listeners",
63+
),
64+
"--drop-duplicate,-D": Parameter(
65+
Complete.NONE,
66+
action="store_true",
67+
help="Automatically drop sessions with hosts that are already active",
68+
),
69+
}
70+
LOCAL = True
71+
72+
def _drop_duplicate(self, session: "pwncat.manager.Session"):
73+
74+
for other in session.manager.sessions.values():
75+
if (
76+
other is not session
77+
and session.hash == other.hash
78+
and session.platform.getuid() == other.platform.getuid()
79+
):
80+
session.log("dropping duplicate session")
81+
return False
82+
83+
return True
84+
85+
def run(self, manager: "pwncat.manager.Manager", args):
86+
87+
if args.drop_duplicate:
88+
established = self._drop_duplicate
89+
90+
if args.platform is None:
91+
manager.print(
92+
"You have not specified a platform. Connections will be queued until initialized with the 'listeners' command."
93+
)
94+
if not Confirm.ask("Are you sure?"):
95+
return
96+
97+
with console.status("creating listener..."):
98+
listener = manager.create_listener(
99+
protocol="socket",
100+
platform=args.platform,
101+
host=args.host,
102+
port=args.port,
103+
ssl=args.ssl,
104+
ssl_cert=args.ssl_cert,
105+
ssl_key=args.ssl_key,
106+
established=established,
107+
count=args.count,
108+
)
109+
110+
while listener.state is ListenerState.STOPPED:
111+
pass
112+
113+
if listener.state is ListenerState.FAILED:
114+
manager.log(
115+
f"[red]error[/red]: listener startup failed: {listener.failure_exception}"
116+
)
117+
else:
118+
manager.log(f"new listener created for {listener}")

Diff for: pwncat/commands/listeners.py

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env python3
2+
from rich import box
3+
from rich.table import Table
4+
from rich.prompt import Prompt
5+
6+
import pwncat
7+
from pwncat.util import console
8+
from pwncat.manager import Listener, ListenerError, ListenerState
9+
from pwncat.commands import Complete, Parameter, CommandDefinition
10+
11+
12+
class Command(CommandDefinition):
13+
"""
14+
Manage active or stopped background listeners. This command
15+
is only used to interact with established listeners. For
16+
creating new listeners, use the "listen" command instead.
17+
"""
18+
19+
PROG = "listeners"
20+
ARGS = {
21+
"--all,-a": Parameter(
22+
Complete.NONE,
23+
action="store_true",
24+
help="Show all listeners when listing (default: hide stopped)",
25+
),
26+
"--kill,-k": Parameter(
27+
Complete.NONE, action="store_true", help="Stop the given listener"
28+
),
29+
"--init,-i": Parameter(
30+
Complete.NONE, action="store_true", help="Initialize pending channels"
31+
),
32+
"id": Parameter(
33+
Complete.NONE,
34+
type=int,
35+
nargs="?",
36+
help="The specific listener to interact with",
37+
),
38+
}
39+
LOCAL = True
40+
41+
def _init_channel(self, manager: pwncat.manager.Manager, listener: Listener):
42+
"""Initialize pending channel"""
43+
44+
# Grab list of pending channels
45+
channels = list(listener.iter_channels())
46+
if not channels:
47+
manager.log("no pending channels")
48+
return
49+
50+
manager.print(f"Pending Channels for {listener}:")
51+
for ident, channel in enumerate(channels):
52+
manager.print(f"{ident}. {channel}")
53+
54+
manager.print("\nPress C-c to stop initializing channels.")
55+
56+
platform = "linux"
57+
58+
try:
59+
while True:
60+
if not any(chan is not None for chan in channels):
61+
manager.log("all pending channels configured")
62+
break
63+
64+
ident = int(
65+
Prompt.ask(
66+
"Channel Index",
67+
choices=[
68+
str(x)
69+
for x in range(len(channels))
70+
if channels[x] is not None
71+
],
72+
)
73+
)
74+
if channels[ident] is None:
75+
manager.print("[red]error[/red]: channel already initialized.")
76+
continue
77+
78+
platform = Prompt.ask(
79+
"Platform Name",
80+
default=platform,
81+
choices=["linux", "windows", "drop"],
82+
show_default=True,
83+
)
84+
85+
if platform == "drop":
86+
manager.log(f"dropping channel: {channels[ident]}")
87+
channels[ident].close()
88+
channels[ident] = None
89+
continue
90+
91+
try:
92+
listener.bootstrap_session(channels[ident], platform)
93+
channels[ident] = None
94+
except ListenerError as exc:
95+
manager.log(f"channel bootstrap failed: {exc}")
96+
channels[ident].close()
97+
channels[ident] = None
98+
except KeyboardInterrupt:
99+
manager.print("")
100+
pass
101+
finally:
102+
for channel in channels:
103+
if channel is not None:
104+
listener.bootstrap_session(channel, platform=None)
105+
106+
def _show_listener(self, manager: pwncat.manager.Manager, listener: Listener):
107+
"""Show detailed information on a listener"""
108+
109+
# Makes printing the variables a little more straightforward
110+
def dump_var(name, value):
111+
manager.print(f"[yellow]{name}[/yellow] = {value}")
112+
113+
# Dump common state
114+
dump_var("address", str(listener))
115+
116+
state_color = "green"
117+
if listener.state is ListenerState.FAILED:
118+
state_color = "red"
119+
elif listener.state is ListenerState.STOPPED:
120+
state_color = "yellow"
121+
122+
dump_var(
123+
"state",
124+
f"[{state_color}]"
125+
+ str(listener.state).split(".")[1]
126+
+ f"[/{state_color}]",
127+
)
128+
129+
# If the listener failed, show the failure message
130+
if listener.state is ListenerState.FAILED:
131+
dump_var("[red]error[/red]", repr(str(listener.failure_exception)))
132+
133+
dump_var("protocol", repr(listener.protocol))
134+
dump_var("platform", repr(listener.platform))
135+
136+
# A count of None means infinity, annotate that
137+
if listener.count is not None:
138+
dump_var("remaining", listener.count)
139+
else:
140+
dump_var("remaining", "[red]infinite[/red]")
141+
142+
# Number of pending channels
143+
dump_var("pending", listener.pending)
144+
145+
# SSL settings
146+
dump_var("ssl", repr(listener.ssl))
147+
if listener.ssl:
148+
dump_var("ssl_cert", repr(listener.ssl_cert))
149+
dump_var("ssl_key", repr(listener.ssl_key))
150+
151+
def run(self, manager: "pwncat.manager.Manager", args):
152+
153+
if (args.kill or args.init) and args.id is None:
154+
self.parser.error("missing argument: id")
155+
156+
if args.kill and args.init:
157+
self.parser.error("cannot use both kill and init arguments")
158+
159+
if args.id is not None and (args.id < 0 or args.id >= len(manager.listeners)):
160+
self.parser.error(f"invalid listener id: {args.id}")
161+
162+
if args.kill:
163+
# Kill the specified listener
164+
with console.status("stopping listener..."):
165+
manager.listeners[args.id].stop()
166+
manager.log(f"stopped listener on {str(manager.listeners[args.id])}")
167+
return
168+
169+
if args.init:
170+
self._init_channel(manager, manager.listeners[args.id])
171+
return
172+
173+
if args.id is not None:
174+
self._show_listener(manager, manager.listeners[args.id])
175+
return
176+
177+
table = Table(
178+
"ID",
179+
"State",
180+
"Address",
181+
"Platform",
182+
"Remaining",
183+
"Pending",
184+
title="Listeners",
185+
box=box.MINIMAL_DOUBLE_HEAD,
186+
)
187+
188+
for ident, listener in enumerate(manager.listeners):
189+
190+
if listener.state is ListenerState.STOPPED and not args.all:
191+
continue
192+
193+
if listener.count is None:
194+
count = "[red]inf[/red]"
195+
else:
196+
count = str(listener.count)
197+
198+
table.add_row(
199+
str(ident),
200+
str(listener.state).split(".")[1],
201+
f"[blue]{listener.address[0]}[/blue]:[cyan]{listener.address[1]}[/cyan]",
202+
str(listener.platform),
203+
count,
204+
str(listener.pending),
205+
)
206+
207+
console.print(table)

0 commit comments

Comments
 (0)