Commit b4532632 authored by Michal Horejsek's avatar Michal Horejsek

Draft

parents
__pycache__
*.pyc
*.pyo
.cache
.eggs
haas_proxy.egg-info
build
dist
*.deb
*.rpm
*.tar
# Copyright (C) 2017 CZ.NIC, z.s.p.o. <https://www.nic.cz>
image: debian:8
before_script:
- apt-get --quiet update
- apt-get --quiet upgrade
- make prepare-dev
stages:
- test
- build
test:
stage: test
script:
- make test
lint:
stage: test
script:
- make lint
build:
stage: build
script:
- make build
artifacts:
paths:
- *.deb
- *.rpm
- *.tar
.PHONY: all prepare-dev test lint run-py2 run-py3 build clean
FPM_CMD=fpm -f -d sshpass -m 'haas@nic.cz' -s python
FPM_CMD_PY2=${FPM_CMD} --python-bin /usr/bin/python2 --python-package-name-prefix python
FPM_CMD_PY3=${FPM_CMD} --python-bin /usr/bin/python3 --python-package-name-prefix python3
all:
@echo "make prepare-dev"
@echo "make test"
@echo "make lint"
@echo "make run"
@echo "make build"
@echo "make clean"
prepare-dev:
apt-get install python python-pip pyhton3 python3-pip sshpass
pip2 install twisted>=16.0
pip3 install twisted>=16.6
# Test dependencies
pip2 install pylint pytest mock
pip3 install pylint pytest
# Build dependencies
apt-get install ruby ruby-dev rubygems build-essential rpm
gem install --no-ri --no-rdoc fpm
test:
which python2 && python2 -m pytest test_honeypot_proxy.py || true
which python3 && python3 -m pytest test_honeypot_proxy.py || true
lint:
python3 -m pylint --rcfile=pylintrc honeypot_proxy.py
run-py2:
sudo python2 honeypot_proxy.py --user-id 42
run-py3:
sudo python3 honeypot_proxy.py --user-id 42
build:
# Debian packages
${FPM_CMD_PY2} -t deb setup.py
${FPM_CMD_PY3} -t deb setup.py
# Red Hat packages
${FPM_CMD_PY2} -t rpm setup.py
${FPM_CMD_PY3} -t rpm setup.py
# Just archive, no deps
${FPM_CMD} -t tar setup.py
clean:
python setup.py clean
rm -rf *.deb *.rpm *.tar
\ No newline at end of file
# Honeypot Proxy
Honeypot proxy is tool for redirectiong SSH session from local computer
to server of HaaS with additional information.
More information on https://haas.nic.cz
## Development
To prepare your dev envrionment run this command:
make prepare-dev
To run tests or lint use this command:
make test
make lint
#!/usr/bin/env python
"""
Proxy for project Honepot as a Service by CZ.NIC. This proxy is needed to
tag SSH activity with your account ID so you can watch your log online.
Script has hardcoded address of honeypot running at CZ.NIC. It shouldn't
be changed but if does or you need to use proxy or send it to own honeypot,
use optional arguments honeypot-host and honeypot-port.
Script contains one pre-generated key. If you want to use own, create one
with following command:
$ ssh-keygen -t rsa -b 4096
Store it in some path and then pass it as:
--public-key "$(< /path/id_rsa.pub)" --private-key "$(< /path/id_rsa)"
"""
import argparse
import fcntl
import json
import os
import pwd
import struct
import sys
import tty
from twisted import cred
from twisted.conch.avatar import ConchUser
from twisted.conch.ssh import factory, keys, userauth, connection, session
from twisted.conch.unix import SSHSessionForUnixConchUser
from twisted.internet import reactor, defer
from twisted.python import log
from twisted.python import components
DEFAULT_PORT = 5022
DEFAULT_HONEYPOT_HOST = 'localhost'
DEFAULT_HONEYPOT_PORT = 2222
# pylint: disable=line-too-long
DEFAULT_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC2jdAE4EAAKikW6W/dDmWS/0lQ1jWM6c6Ef+KpGr+jW83/XIR2reWXeeDTIEluL20JV/P2+2bvVShNr4w8SWitcYKTpwkSgGYHo2vAQvXArx/CsRnTAP6NwrxuZoLNO52fMXQWSrqs0tEvkzYXR3PcR6Cq07RN7QkYNWctCYJxdw=='
DEFAULT_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC2jdAE4EAAKikW6W/dDmWS/0lQ1jWM6c6Ef+KpGr+jW83/XIR2
reWXeeDTIEluL20JV/P2+2bvVShNr4w8SWitcYKTpwkSgGYHo2vAQvXArx/CsRnT
AP6NwrxuZoLNO52fMXQWSrqs0tEvkzYXR3PcR6Cq07RN7QkYNWctCYJxdwIDAQAB
AoGAMOy4v1XKUUD7WiSd0kS1fDvmzj9agrV2n5QWjvOYQJOuFa4Z4iSgz4PeeTbB
90HGmyZzP9IIuEO+VXOixdV2s/DQ9fGjaBUb95+tYu94KM3tIq9B3kETQwtl+TxE
FywSM9kQGt65ob26K6BbOIZPnF2e6rMEy0pD1UJ2vKDs1wECQQDsEmXZtqh7ktC6
MIKmXegADwEiwnQN+lnboAXDNVQCMWKiWg/Ih4NpDoG9x+OIuVRRz5jEHYyRz8nt
/yvnsRZhAkEAxfbfWWZT+TjwbiSj2/rHg0+2W0LxiJhJJNZDaL/Ad0KcW702CoAc
xWk5uC4dzS9xq9fULN0IYXmPe/5vNZ5m1wJAD3E4pmAzbznwW22W7kkQRwi0O1Db
BJsOy7YRCm7vmuEeIZ6gj66Foxam2AI+WRA+eseIp7ODIXqlK+NYPOSxoQJAYbMt
F5oA54bKYhGDLRXfUVcN0IyBV8CQmLWGHzRDcJhXQo9nFFeV23fLHLLl0lYP65dh
B6Mud6zeu3set3+tkQJBAK8bVknHYkapijQNoM7slRZqgeBUImktJI0qq+YTEspr
za4ElES2AJye2cxYTx8zn59ppadHV2GIJZpj+hJvkzU=
-----END RSA PRIVATE KEY-----"""
def main():
"""
Main endpoint to run script.
"""
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
'-u', '--user-id',
dest='user_id',
required=True,
help='Your ID at honeypot.labs.nic.cz. If you don\'t have one, sign up first.',
)
parser.add_argument(
'-p', '--port',
dest='port',
type=int,
default=DEFAULT_PORT,
help='Port to listen to, default {}.'.format(DEFAULT_PORT),
)
parser.add_argument('--honeypot-host', dest='honeypot_host', default=DEFAULT_HONEYPOT_HOST)
parser.add_argument('--honeypot-port', dest='honeypot_port', default=DEFAULT_HONEYPOT_PORT)
parser.add_argument('--public-key', dest='public_key', default=DEFAULT_PUBLIC_KEY)
parser.add_argument('--private-key', dest='private_key', default=DEFAULT_PRIVATE_KEY)
args = parser.parse_args()
log.startLogging(sys.stderr)
reactor.listenTCP(args.port, ProxySSHFactory(args)) # pylint: disable=no-member
reactor.run() # pylint: disable=no-member
def force_text(value):
"""
Helper to deal with bytes and str in Python 2 vs. Python 3. Needed to have
always username and password as a string (i Python 3 it's bytes).
"""
if issubclass(type(value), str):
return value
if isinstance(value, bytes):
return str(value, 'utf-8')
return str(value)
# pylint: disable=abstract-method
class ProxySSHFactory(factory.SSHFactory):
"""
Factory putting together all pieces of SSH proxy to honeypot together.
"""
def __init__(self, cmd_args):
self.publicKeys = {b'ssh-rsa': keys.Key.fromString(data=cmd_args.public_key)}
self.privateKeys = {b'ssh-rsa': keys.Key.fromString(data=cmd_args.private_key)}
self.services = {
b'ssh-userauth': userauth.SSHUserAuthServer,
b'ssh-connection': connection.SSHConnection,
}
self.portal = cred.portal.Portal(ProxySSHRealm(), checkers=[ProxyPasswordChecker()])
ProxySSHSession.cmd_args = cmd_args
components.registerAdapter(ProxySSHSession, ProxySSHUser, session.ISession)
class ProxyPasswordChecker:
"""
Simple object checking credentials. For this SSH proxy we allow only passwords
because we need to pass some information to session and the easiest way is to
send it mangled in password.
"""
credentialInterfaces = (cred.credentials.IUsernamePassword,)
# pylint: disable=invalid-name
def requestAvatarId(self, credentials):
"""
Proxy allows any password. Honeypot decide what will accept later.
"""
return defer.succeed(credentials)
class ProxySSHRealm:
"""
Simple object to implement getting avatar used in :py:any:`portal.Portal`
after checking credentials.
"""
# pylint: disable=invalid-name,unused-argument
def requestAvatar(self, avatarId, mind, *interfaces):
"""
Normaly :py:any:`ProxyPasswordChecker` should return only username but
we need also password so we unwrap it here.
"""
avatar = ProxySSHUser(avatarId.username, avatarId.password)
return interfaces[0], avatar, lambda: None
class ProxySSHUser(ConchUser):
"""
Avatar returned by :py:any:`ProxySSHRealm`. It stores username and password
for later usage in :py:any:`ProxySSHSession`.
"""
def __init__(self, username, password):
ConchUser.__init__(self)
self.username = username
self.password = password
self.channelLookup.update({b'session': session.SSHSession})
class ProxySSHSession(SSHSessionForUnixConchUser):
"""
Main function of SSH proxy - connects to honeypot and change password
to JSON with more information needed to tag activity with user's account.
"""
cmd_args = None # Will inject ProxySSHFactory.
# pylint: disable=invalid-name
def openShell(self, proto):
"""
Custom implementation of shell - proxy to real SSH to honeypot.
"""
user = pwd.getpwuid(os.getuid())
# pylint: disable=no-member
self.pty = reactor.spawnProcess(
proto,
executable='/usr/bin/sshpass',
args=self.honeypot_ssh_arguments,
env=self.environ,
path='/',
uid=user.pw_uid,
gid=user.pw_gid,
usePTY=self.ptyTuple,
)
fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack('4H', *self.winSize))
self.avatar.conn.transport.transport.setTcpNoDelay(1)
@property
def honeypot_ssh_arguments(self):
"""
Command line arguments to call SSH to honeypot. Uses sshpass to be able
pass password from command line.
"""
return [
'sshpass',
'-p', self.mangled_password,
'ssh',
'-p', str(self.cmd_args.honeypot_port),
'{}@{}'.format(force_text(self.avatar.username), self.cmd_args.honeypot_host),
]
@property
def mangled_password(self):
"""
Password as JSON string containing more information needed to
tag activity with user's account.
"""
peer = self.avatar.conn.transport.transport.getPeer()
password_data = {
'password': force_text(self.avatar.password),
'user_id': self.cmd_args.user_id,
'remote': peer.host,
'remote_port': peer.port,
}
return json.dumps(password_data)
if __name__ == '__main__':
main()
[MESSAGES CONTROL]
disable=no-self-use
[REPORTS]
output-format=colorized
[VARIABLES]
init-import=no
dummy-variables-rgx=_|dummy
[TYPECHECK]
ignore-mixin-members=yes
[BASIC]
no-docstring-rgx=_.*
good-names=_,dt,e,ex,f,i,r,t,q
bad-names=foo,bar,baz,foobar
[DESIGN]
min-public-methods=1
max-public-methods=20
[FORMAT]
max-line-length=120
[MISCELLANEOUS]
notes=FIXME,XXX,TODO
#!/usr/bin/env python
import sys
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
# You need at least Twisted version 16.0 but version for Python 3
# has some bugs which are solved in 16.6.
TWISTED_VERSION = '16.0' if sys.version_info < (3, 0) else '16.6'
setup(
name='haas-proxy',
version='1.0',
packages=[],
scripts=['honeypot_proxy.py'],
install_requires=['twisted>={}'.format(TWISTED_VERSION)],
url='https://haas.nic.cz',
author='CZ.NIC Labs',
author_email='haas@nic.cz',
description='Honeypot proxy is tool for redirectiong SSH session from local computer to server of HaaS with additional information.',
license='GPLv2',
classifiers=[
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'License :: OSI Approved :: GPLv2',
'Operating System :: OS Independent',
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
],
)
# -*- encoding: utf-8 -*-
import json
try:
from unittest import mock
except ImportError:
import mock
import pytest
from honeypot_proxy import force_text, ProxySSHSession
@pytest.fixture
def cmd_args():
return mock.Mock(
user_id=42,
honeypot_host='localhost',
honeypot_port=2222,
)
@pytest.fixture(params=(
('user', 'pass'),
(b'user', b'pass'),
))
def avatar(request):
avatar = mock.Mock(
username=request.param[0],
password=request.param[1],
)
avatar.conn.transport.transport.getPeer.return_value = mock.Mock(host='hacker', port=12345)
return avatar
@pytest.fixture
def proxy_ssh_session(cmd_args, avatar):
session = ProxySSHSession(avatar)
session.cmd_args = cmd_args
return session
@pytest.mark.parametrize('value, expected', (
('abc', 'abc'),
(b'abc', 'abc'),
('háčkyčárky', 'háčkyčárky'),
(b'h\xc3\xa1\xc4\x8dky\xc4\x8d\xc3\xa1rky', 'háčkyčárky'),
))
def test_force_text(value, expected):
assert force_text(value) == expected
def test_honeypot_ssh_arguments(proxy_ssh_session):
assert proxy_ssh_session.honeypot_ssh_arguments[3:] == ['ssh', '-p', '2222', 'user@localhost']
def test_mangle_password(proxy_ssh_session):
assert json.loads(proxy_ssh_session.mangled_password) == {
'password': 'pass',
'user_id': 42,
'remote': 'hacker',
'remote_port': 12345,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment